Black Friday, 14:02. Traffic steigt auf das Fünffache. Response Times klettern. 200ms. 800ms. 2 Sekunden. 8 Sekunden. Timeout.
Dein Service bedient niemanden mehr. Nicht weil er crashed ist, er läuft noch. Alle Cores bei 100%, Connection Pool voll, jeder Request wartet auf den Request davor. Die Queue wird länger und länger. Am Frontend sehen 100% der User einen Spinner. Keiner bekommt seine Bestellung durch. Revenue pro Minute: null.
Die Ironie: du hast versucht, alle zu bedienen. Und genau deshalb bedienst du niemanden.
Dreh den Traffic hoch und schau was passiert — links ohne Schutz, rechts mit Load Shedding:
Schau dir den Unterschied bei 800 req/s an. Links: 12 Sekunden Latenz, 15% Success Rate. Rechts: 55ms Latenz, 50% Success Rate, der Rest bekommt ein sofortiges 503. Die Hälfte der User bekommt eine schnelle Antwort. Das ist besser als null User mit einer langsamen.
Die Intuition ist falsch
Der natürliche Instinkt: “Wir müssen jeden Request bedienen.” Klingt richtig. Ist falsch.
Wenn ein System über Kapazität geht, degradiert es nicht linear. Es kollabiert. Queues füllen sich, Garbage Collection kickt rein, Timeouts kaskadieren, Threads blockieren sich gegenseitig. Bei 2x Kapazität hast du nicht 50% der Performance. Du hast 5%.
Das nennt sich “Congestion Collapse”, ein Begriff aus der Netzwerktheorie. TCP hat das gleiche Problem wenn zu viele Pakete gleichzeitig durchs Netz geschickt werden. Die Lösung dort: Pakete droppen. Klingt kontraintuitiv, funktioniert seit 40 Jahren.
Load Shedding ist die Konsequenz: wenn du nicht alle bedienen kannst, bedien die die du bedienen kannst, und sag dem Rest sofort Bescheid.
Wie entscheiden, wen du ablehnst?
Nicht alle Requests sind gleich. Drei Strategien:
Random Drop: der simpelste Ansatz. Über Kapazität? Wirf eine Münze. Einfach zu implementieren, fair, braucht keinen State. Für die meisten Services reicht das.
LIFO (Newest First): nimm den neuesten Request aus der Queue, verwirf die ältesten. Warum? Weil der User, der vor 8 Sekunden geklickt hat, wahrscheinlich schon aufgegeben hat. Der, der gerade geklickt hat, wartet noch. Serviere ihn.
Priority-Based: VIP-Kunden, Health Checks, interne Requests bekommen Vorrang. Alles andere wird geshedded. Braucht Kontextinformation im Request (Header, Token-Typ).
const MAX_INFLIGHT = 200;
let inflight = 0;
async function loadSheddingMiddleware(
req: Request,
next: () => Promise<Response>
): Promise<Response> {
if (inflight >= MAX_INFLIGHT) {
return new Response('Service Unavailable', {
status: 503,
headers: {
'Retry-After': '2',
},
});
}
inflight++;
try {
return await next();
} finally {
inflight--;
}
}
Drei Dinge die auffallen: Erstens, der Counter ist simpel: inflight ist ein Integer, kein Distributed Lock. Zweitens, Retry-After sagt dem Client wann er es nochmal versuchen soll, ohne das bekommst du sofortige Retry-Storms. Drittens, das finally ist kritisch, denn ohne das läuft dein Counter bei Exceptions über und du sheddest für immer.
Wie wählst du MAX_INFLIGHT? Little’s Law: L = λ × W. Wenn dein Service bei gesunder Last 400 req/s bedient und jeder Request 50ms braucht, hast du im Schnitt 20 in-flight Requests. Setz dein Limit bei ~200 für Headroom. Wenn du deutlich drüber bist, stimmt was Grundsätzliches nicht.
Graceful Degradation
Load Shedding ist die aggressive Variante: Request rein, 503 raus. Graceful Degradation ist der Mittelweg: du antwortest, aber mit weniger.
Produktseite? Zeig das Produkt, aber lade Empfehlungen nicht. Suchseite? Gib Ergebnisse aus dem Cache, auch wenn sie 5 Minuten alt sind. Dashboard? Zeig die letzte bekannte Metrik statt live zu queryen.
async function getProductPage(productId: string): Promise<ProductPage> {
const product = await productService.get(productId); // immer
if (loadMonitor.isHealthy()) {
const [reviews, recommendations] = await Promise.all([
reviewService.getForProduct(productId),
recommendationService.get(productId),
]);
return { product, reviews, recommendations };
}
// Under pressure: nur das Wesentliche
return { product, reviews: [], recommendations: [] };
}
Der User merkt vielleicht, dass die Empfehlungen fehlen. Aber er kann kaufen. Das ist der Unterschied zwischen “degraded” und “down.”
Wann nicht shedden
Nicht jeder Request ist gleich opferbar. Health Checks: nie shedden, sonst denkt der Load Balancer du bist tot und routet noch mehr weg. Interne Admin-Requests: nie shedden, sonst kannst du das Problem nicht debuggen während es passiert. Idempotente Writes die schon einen Side Effect hatten: nie shedden, sonst hast du inkonsistenten State.
Ein schnelles 503 ist besser als ein langsames 200 das sowieso timed out.