Concurrency, backpressure and load shedding
In one line
Every system has a maximum useful concurrency; backpressure tells callers to slow down when you are near it, and load shedding drops the least important work early, so the system stays fast for the requests it accepts instead of collapsing for everyone.
What it is
Concurrency is how many requests are being worked on at once. By Little's Law, concurrency = arrival rate x latency. At 1,000 requests per second and 200 ms latency you hold 200 requests in flight. If a downstream slows to 2 seconds, the same traffic now holds 2,000 in flight: threads, memory, sockets and database connections run out even though traffic did not change.
Backpressure is a signal flowing upstream that says 'slow down'. It can be explicit (HTTP 429 or 503 with Retry-After, a gRPC RESOURCE_EXHAUSTED status, a bounded queue that blocks the producer, TCP flow control, Reactive Streams demand) or implicit (a caller with a concurrency limit that stops sending when its slots are full).
Load shedding is the server deciding to reject some work so the rest succeeds. Rejecting a request costs microseconds; accepting it and timing out after 10 seconds wastes the full cost and still fails. Good shedding happens early (at the edge or on admission), cheaply, and by priority: drop analytics and recommendations before checkout and payments.
The failure these tools prevent is congestion collapse: queues grow without limit, every request waits longer than its caller's timeout, callers retry, load multiplies, and the system does lots of work while successfully serving almost nothing. Bounded queues, timeouts, retry budgets and shedding keep 'goodput' (successful, useful responses) high under overload.
Why interviewers ask
Most real outages are overload and cascading failure, not a single crashed server: a slow dependency, a retry storm, a traffic spike at a sale or a match. Senior and staff interviews probe this directly: 'what happens when the payment provider slows to 5 seconds?' Candidates who answer with timeouts, bounded pools, concurrency limits, priority shedding and retry budgets sound like people who have run production systems. In the AI era it matters more: LLM calls hold connections for 5 to 60 seconds, so concurrency, not CPU, is usually the first limit.
How it works
- 1Bound everything: every thread pool, connection pool, in-memory queue and channel has a maximum size. An unbounded queue just converts overload into memory exhaustion and huge latency.
- 2Set timeouts everywhere and propagate deadlines: if the client gives up after 3 seconds, downstream calls should be cancelled at 3 seconds too (context deadlines in Go, gRPC deadlines). Work for a caller who has left is pure waste.
- 3Admission control: cap in-flight requests per instance (a semaphore). When full, reject immediately with 503 and Retry-After rather than queueing. Size the cap from Little's Law and load tests.
- 4Make the cap adaptive when load varies: algorithms like Netflix's concurrency-limits (inspired by TCP congestion control) or Envoy's adaptive concurrency filter lower the limit when latency rises above the measured baseline and raise it when latency is healthy.
- 5Shed by priority: tag requests with a criticality (critical, default, sheddable). Under pressure, reject sheddable first. Shed at the edge or gateway when possible, before work is done.
- 6Prefer fresh work over stale work: drop requests that have already waited longer than their deadline; some systems serve the queue LIFO under overload or use CoDel-style queues that cap time in queue, so the requests served still have a chance to succeed.
- 7Control retries: exponential backoff with jitter, a retry budget (for example retries at most 10% of requests), and no retries on 429/503 from a shedding server except after Retry-After. Use circuit breakers to stop calling a dependency that is failing.
- 8For queues and streams, backpressure means consumers pull at their own pace; the producer side either blocks, gets rejected when a bounded buffer is full, or the backlog is absorbed by a durable log like Kafka while consumers scale on lag.
- 9Degrade gracefully: serve cached or simplified responses (no personalisation, fewer results) instead of errors when downstreams are slow.
Worked example · Payment provider slows down during a sale
- Checkout service: 2,000 requests per second, 20 instances, each call to the payment provider normally takes 300 ms. In flight: 2,000 x 0.3 = 600 requests, 30 per instance.
- The provider degrades to 6 seconds. Without limits, in flight becomes 2,000 x 6 = 12,000 requests, 600 per instance. Thread pools and connection pools fill, memory climbs, and even the health check endpoint stops responding. The load balancer marks instances unhealthy, pushing more traffic onto the rest: cascading failure.
- Clients time out at 10 seconds and retry twice, so the provider and the service now see up to 3x the traffic. Goodput drops close to zero.
- With protection: each instance caps provider calls with a bulkhead of 100 concurrent calls and an 8-second timeout (a payment call that times out is later checked by status, using its idempotency key, never blindly retried). When the bulkhead is full, checkout returns 503 with Retry-After: 5 immediately, or offers a second provider.
- The edge sheds sheddable traffic (recommendations, 'people also bought', review fetches) when checkout p99 passes 1 second, freeing about 30% of capacity.
- Retry budget: the client SDK retries at most 10% of calls, with jitter. A circuit breaker opens after 50% failures in 10 seconds and probes the provider every few seconds.
- Result: maybe 100 x 20 / 6 s = about 330 checkouts per second complete via the slow provider, the rest get a fast, honest 'try again shortly', and the service stays healthy to recover instantly when the provider does.
Picture it
A metro station during rush hour closes the entry gates for a few minutes when the platform is full. People outside wait a little, but nobody is crushed, and trains keep moving. Letting everyone in would make the platform dangerous and the trains slower for all.
Reference code
// Admission control: cap in-flight work and fail fast when full.
var slots = make(chan struct{}, 2000) // max concurrent requests per instance
func Admit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case slots <- struct{}{}:
defer func() { <-slots }()
// Bound the time this request may use downstream.
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
default:
// Full: reject now instead of queueing forever.
w.Header().Set("Retry-After", "1")
http.Error(w, "overloaded", http.StatusServiceUnavailable)
}
})
}Key patterns
Bulkheads
When: One dependency or feature can slow down while others must keep working
How: Separate pools (threads, connections, pods) per dependency or feature, each with its own limit, so one slow path cannot consume everything.
Admission control with fast rejection
When: Any service that can be overloaded
How: Semaphore or adaptive limiter at the entry point; reject with 429/503 plus Retry-After in microseconds when full.
Priority-based shedding
When: Mixed critical and optional traffic
How: Tag requests with criticality at the edge; shed sheddable traffic first as utilisation or latency rises; never shed health checks needed for routing.
Deadline propagation
When: Request fan-out across many services
How: Pass the remaining time budget downstream; drop work whose deadline has passed; never let downstream timeouts exceed upstream ones.
Retry budgets and circuit breakers
When: Callers of any remote dependency
How: Backoff with jitter, retries capped as a fraction of traffic, breaker opens on high failure rate and half-opens to probe.
Real-world use
Google SRE: handling overload
Google's SRE book describes per-request criticality (critical, sheddable), rejecting early on overload, client-side adaptive throttling where clients locally reject requests when the backend's accept ratio drops, and retry budgets to avoid retry storms.
Stripe's load shedders
Stripe has written about using a concurrent-requests limiter alongside rate limiters, and load shedders that reserve capacity for critical requests (such as creating charges) by dropping lower-priority traffic first under pressure.
Netflix concurrency-limits
Netflix open-sourced a library that discovers a service's concurrency limit automatically, using latency changes like TCP congestion control, and rejects excess requests instead of letting queues grow.
Facebook / Meta queue management
Meta's 'Fail at Scale' paper describes controlled-delay queues (inspired by CoDel) and adaptive LIFO under overload, so requests that have waited too long are dropped and fresh ones are served.
Envoy proxy
Envoy provides circuit breakers (max connections, pending requests, retries per cluster), an adaptive concurrency filter and admission control, letting a service mesh apply backpressure without app code.
Virtual waiting rooms
Ticketing and flash-sale sites put users in a queue page at the edge and admit them at a rate the backend can handle. It is load shedding with a good user experience.
In a project
Your company exposes an AI chat feature. Each streamed answer holds a connection for 10 to 40 seconds. During a marketing push, traffic doubles, pods run out of memory, and the whole product (including login and billing pages, which share the same gateway) goes down.
- Apply Little's Law: 300 new chats per second x 25 s average = 7,500 concurrent streams. Size per-pod limits (for example 400 streams per pod) and pods accordingly.
- Add admission control per pod for the streaming endpoint; when full, return 503 with Retry-After and a friendly 'busy, retry in a few seconds' message in the UI.
- Isolate the AI path: separate pods and a separate connection pool for streaming, so login and billing never compete with long-lived streams (bulkheads).
- Give each tenant a concurrency quota in the LLM gateway so one enterprise customer's batch job cannot take every slot.
- Bound per-connection send buffers; if a slow mobile client cannot keep up, drop it rather than buffering its tokens forever.
- Shed by priority in the gateway: paid users and in-progress conversations first, new free-tier chats shed first.
- Load-test at 2x and 5x expected peak and confirm goodput stays flat instead of collapsing.
Impact: Under the next spike, about 15% of new free-tier chats see a short 'busy' message, while paid chats, login and billing stay healthy. No pod runs out of memory, and recovery is instant when traffic falls.
Trade-offs
Use it when
- Any service with slow or unreliable dependencies (payment providers, LLM APIs, legacy systems)
- Traffic that can spike beyond capacity: sales, live events, launches
- Long-lived connections (streaming, WebSockets) where concurrency is the limit
- Multi-tenant systems where one tenant can starve others
Avoid it when
- Static limits that are far below real capacity; they reject good traffic. Measure, or use adaptive limits
- Shedding work that cannot be retried safely without idempotency; make writes idempotent first
- Hiding overload with unbounded queues 'so nothing is dropped'; latency and memory explode instead
- Retrying shed requests immediately; it turns shedding into a retry storm
Common mistakes
- Unbounded queues and pools: overload becomes out-of-memory crashes and minute-long latencies.
- No timeouts, or downstream timeouts longer than the caller's, so servers work for clients who already left.
- Retrying everything three times at every layer: 3 layers x 3 retries = up to 27x load on the bottom service.
- Shedding after doing expensive work (after the DB query) instead of at admission.
- Letting health checks fail under load, so the load balancer removes instances and overloads the rest.
- Scaling on CPU while the real limit is connections or concurrency, especially for I/O-bound and LLM workloads.
- Treating every request as equally important; payments and browsing should not compete for the same slots.
Interview questions
Quick revision
- Little's Law: in-flight = rate x latency. Slow downstream = more in flight.
- Bound every queue, pool and buffer.
- Timeouts everywhere; propagate deadlines; cancel abandoned work.
- Reject early and cheaply: 429 for quota, 503 + Retry-After for overload.
- Shed by priority: sheddable before critical.
- Adaptive concurrency limits follow latency (Netflix concurrency-limits, Envoy).
- Retries: backoff + jitter + budget (around 10%), one layer only.
- Bulkheads isolate dependencies; circuit breakers stop calling failing ones.
- Measure goodput, not just throughput.
- Under overload, prefer fresh requests over ones that already blew their deadline.
Check yourself
1. A service receives 500 requests per second with 40 ms latency. A dependency slows and latency becomes 2 seconds. Roughly how many requests are now in flight?
2. Why is rejecting a request immediately often better than queueing it?
3. Three service layers each retry failed calls 3 times. In the worst case, how much load can reach the bottom service per original request?
4. Which is the best first thing to shed during overload on an e-commerce site?
See it in a full design
These walkthroughs use this lesson.
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.