System designCore14 min
Load shedding & backpressure
When more arrives than you can serve, some requests will fail either way. Load shedding chooses which ones and fails them fast; backpressure tells the senders to slow down.
1 · The problem
An overloaded server stays busy and stops being useful
Friday, 7 p.m. A food delivery app's order service can finish 2,000 requests a second, and on a normal evening 1,500 arrive. Then a discount lands on every phone at once, and for half a minute 3,000 a second arrive. The extra 1,000 a second have to go somewhere. With nothing to stop them, they join a queue that grows by 1,000 every second.
The phone app gives up after 2 seconds. Follow the queue (Throughput, utilization & queueing has the full arithmetic):
- Every second of the surge, the requests being answered have waited a third of a second longer.
- Six seconds in, the wait passes 2 s. From then on, every answer reaches a phone that has stopped listening.
- The server is still 100% busy, finishing 2,000 requests a second. Not one of them is still wanted.
Useful work has its own name: goodput, the requests answered correctly and soon enough to matter. Throughput counts everything the server finishes, wanted or not. Under overload the two split apart:
- Arriving
- Useful answers, no shedding
- Useful answers, with shedding
A simple model: capacity 2,000/s, 1,500/s arriving normally and 3,000/s from 10 s to 40 s; phones give up after 2 s and don't retry. Without shedding, useful answers stop at 16 s and come back only at 94 s, 54 s after the surge ended, because a backlog of 30,000 has to drain first. With shedding, 2,000/s stay useful throughout and 1,000/s are refused at once.
Real systems fall further than the chart, because overload feeds itself:
- Retries. A phone that gave up tries again, so the surge grows.
- Crashes. Waiting requests hold memory until a server dies. Its load moves to the others, which then die sooner.
- Recovery is harder than the fall. Every crash cuts capacity. A service that was healthy at 10,000 requests a second and began crashing at 11,000 will almost certainly keep crashing at 9,000.
A failure that outlives the thing that started it is called a metastable failure: the trigger is gone, but retries, crashes or a cold cache keep the load above what is left. The way out is to cut the load well below what the service used to handle. The cheaper way is never to let the queue grow in the first place.
2 · The fix
Say no early and cheaply, so every yes stays fast
Load shedding means refusing part of the traffic on purpose when a server nears its limit. Each refused request gets an immediate 503 Service Unavailable, ideally with a Retry-After header saying when to come back. The phone can show "Busy, try again in a moment" at once, instead of a spinner that ends in a timeout two seconds later.
In the chart, one customer in three sees that message during the surge. That sounds bad until you compare it with the alternative, where everyone waits and nobody's order goes through.
Refusing has to be cheap, or it adds to the overload:
- Refuse early: at the gateway, or in the first lines of the service, before reading the body, checking the token or touching the database.
- Refuse at more than one layer: the gateway protects everything behind it, and each service knows its own state and protects itself.
- Remember that refusing is never free: for a tiny request, saying no costs nearly as much as answering. Flood a server with enough of them and it spends most of its CPU refusing.
Two places to shed. The gateway refuses before any service has done work; the order service refuses whatever would push it past its own limit, whoever sent it.
When should a server start saying no? Not at a fixed number of requests per second. Requests differ in cost, and a fixed number goes stale every time you add servers or the database slows down. Better signals measure the server itself:
Signal | What it tells you | Watch out for |
|---|---|---|
Requests in flight | How much work is inside right now; the simplest limit to set | Pick the number with Little's law, below |
Time spent queued | Whether requests wait longer than callers are willing to | The queue must record when each request arrived |
CPU or memory use | How close the machine is to its own limit | Says nothing about a slow database the server is waiting on |
The in-flight limit comes from Little's law: in flight = throughput × time inside. The order service finishes 2,000 requests a second at about 50 ms each, so at full speed 2,000 × 0.05 = 100 requests are inside it. Set the limit near 100 and refuse the 101st at once.
The limit also adjusts itself. If the database slows and requests take 100 ms, the same 100 slots let in only 100 ÷ 0.1 = 1,000 a second: exactly what the service can now finish. A fixed limit of 2,000 a second would keep admitting twice too many. Adaptive concurrency limits go one step further: like TCP's congestion control, they raise the limit while latency stays flat and cut it as soon as latency starts to climb.
Shedding is not the same thing as rate limiting, and you need both:
Aspect | Rate limiting | Load shedding |
|---|---|---|
Protects | Fairness between clients | The server itself |
Triggered by | One client going over its quota | The server running out of room, whoever is asking |
Refuses a client within its quota? | No | Yes |
Usual answer | 429 Too Many Requests | 503 Service Unavailable |
A rate limit can't save a server that thousands of clients, each within their quota, push past capacity together. Shedding can't tell a runaway script from a paying customer. Rate limiting algorithms covers the first; the rest of this page covers the second.
3 · Who goes first
Shed the requests nobody will miss
Refusing at random treats a customer pressing Pay the same as the app quietly fetching "restaurants you might like". They are not equal. Give every request a priority (also called criticality) where it enters the system, and shed from the bottom up:
Priority | In the delivery app | Under overload |
|---|---|---|
Critical | Placing an order, paying, signing in | Kept until nothing else is left to shed |
Degraded | Menus, search, the live tracking map | Shed after background work; show cached results instead |
Background | Recommendations, prefetching the next screen, analytics events | Shed first |
Health checks | The load balancer asking "are you alive?" | Never shed |
Health checks get their own row for a reason. A busy server that ignores the load balancer's check is marked dead and taken out. Its traffic moves to the others, which are just as busy, and they fail their checks too. Shedding meant to save the fleet ends up shrinking it.
Three details make priorities work:
- The priority travels with the request. When the order service calls payments, the call keeps the order's priority, so payments sheds in the same order.
- Shed in order. Lower priorities are refused first and at lower load; critical requests only when nothing else is left.
- Degrade before you refuse. Graceful degradation answers with something cheaper instead of nothing: popular restaurants from a cache instead of a personal ranking, or a tracking map that updates every 30 s instead of every 5.
It works at scale. Netflix gives every request a priority, both at its API gateway and inside its services. During one infrastructure outage, with prefetch traffic up about 12 times, requests from people actually pressing play stayed above 99.4% available.
4 · Wasted work
Drop work whose caller has already given up
The worst stretch of the chart was a server working flat out for nobody. A server can't see a phone give up, unless the request carries that moment with it: a deadline, the time after which the caller no longer wants the answer.
Each hop passes on the time that is left, not a fresh timeout. If the gateway receives 2 s and spends 0.1 s, it sends 1.9 s onward. Before starting any expensive step, a server checks what is left, and refuses at once if the work can't finish in time:
Payments skips a charge whose answer would reach a phone that had stopped listening, and spends that half second on a request that can still succeed.
Inside one server, the queue itself can drop stale work:
- Bound the queue by age. A request that has waited longer than its caller will wait is thrown away unanswered.
- Serve the newest first once a queue forms. Requests normally go in arrival order. When a queue builds, the oldest are the likeliest to be abandoned, so adaptive LIFO (last in, first out) spends the server on requests that can still succeed.
A third trick gives a standing queue little patience. Facebook runs a variant of the CoDel algorithm: a request may queue for 100 ms if the queue was empty recently, but only 5 ms if it hasn't been empty for 100 ms. Short bursts still queue; a queue that never drains is cut down fast.
LIFO is unfair on purpose: under a long overload the oldest requests may never be served. That is right for people staring at a screen, and wrong for work that must happen eventually, like sending a receipt. That kind of work needs the next idea.
5 · Push back
Backpressure slows the sender to the receiver's pace
Shedding throws work away. Some work must not be lost: every placed order needs its receipt email. Here the answer is backpressure: the overloaded stage tells the stage before it to slow down, and the work waits instead of disappearing. There are three ways to say it:
Way | How it works | Where you meet it |
|---|---|---|
Block | A full, bounded buffer makes the producer wait until there is room | A thread pool's work queue; stages inside one program |
Credits or pull | The receiver says how much it can take, and the sender sends no more | TCP and HTTP/2 flow-control windows; Reactive Streams; Kafka consumers, which pull |
Refuse with a signal | The receiver answers 429 or 503 with | HTTP APIs; client-side throttling, below |
The delivery app sends receipts through an event log. The email provider accepts 50 emails a second, but at the dinner peak orders create 120 receipts a second. The receipt worker pulls only what it can send, so the other 70 a second wait in the log:
After a 10-minute peak, 70 × 600 = 42,000 receipts are waiting, and the last one of the peak goes out 42,000 ÷ 50 = 840 s, 14 minutes, late. Late, but none lost. Alert on the age of the oldest message, not only on the count.
Backpressure has to end somewhere. It can end at a durable buffer built to hold a backlog, like the log above. Or it ends at a person, as a 503 and a "try again". What must not happen is a chain of services that each simply block: every waiting caller holds a thread and memory, so the slowdown climbs back up the chain until the front door stops too.
Callers can push back on themselves too. In client-side throttling, each client counts, over the last two minutes, the requests it sent and the ones the server accepted. Once requests pass K times the accepts (K = 2 is typical), the client refuses some new requests itself, before they cost the server anything. It refuses each new request with probability:
- p = max(0, (requests − K × accepts) ÷ (requests + 1))
A client that sent 1,000 requests and had 300 accepted refuses (1,000 − 600) ÷ 1,001 ≈ 40% of its new ones locally. As the server recovers, accepts rise and the client lets traffic back through. Retries need the same discipline: a retry budget, and an "overloaded, don't retry" answer, stop refused requests from coming straight back (see Timeouts, retries, backoff & jitter).
6 · The catch
Shedding buys time, not capacity
Shedding keeps a service alive through a surge it can't serve. It does not make the surge servable, and it brings traps of its own:
Trap | What goes wrong | Do this instead |
|---|---|---|
Shedding at the autoscaling threshold | Shedding holds CPU just under the scale-up line, so new servers never arrive | Shed above the autoscaling threshold, and scale on the shed rate too |
Shedding health checks | Busy servers are marked dead, and the rest inherit their load | Answer health checks before anything else |
Refusals in the latency chart | A 503 takes a millisecond, so average latency looks healthy during an outage | Measure accepted requests' latency separately from refusals |
Instant retries | Every refusal comes back a moment later as a new request | Send |
Treating refusal as free | With tiny requests, the server can spend most of its CPU saying no | Push refusals out to the clients with client-side throttling |
Never testing it | The shedding code runs for the first time in a real outage | Load test well past the point where goodput stops rising |
Above all, shed requests are real customers getting an error. Count them, alert on them, and treat a steady shed rate as a capacity problem to fix, not as a feature quietly doing its job.
7 · In a real system
Shed at the gateway while the cache is down
URL Shortener System Design
The cache dies at peak; shed load at the gateway
Redis goes down at peak and retries swamp Cassandra. Try the fix that sheds load at the gateway: scraping goes first, new links next, opening links last. Cassandra holds at 85%, while about a third of redirects and every new link get a fast error instead of a slow one.
Check yourself
5 questions
Takeaways
Remember this
- Past capacity, an overloaded server stays busy but its goodput can fall to zero, and the overload can outlast the surge that caused it.
- Load shedding refuses the excess early and cheaply so accepted requests stay fast; limit requests in flight, not requests per second.
- Shed by priority: background work first, paying customers last, health checks never.
- Pass deadlines down as time left, and drop work whose caller has already given up.
- Backpressure slows producers to the consumer's pace, by blocking, credits or a clear "slow down"; shedding buys time, not capacity.