SD System Design In Depth
0%
lesson system-designqueuesreliability

Backpressure

Module 3.1: Network Protocols & Service Communication Updated Sep 17, 2026
📄 Cheat Sheet
Zach Tellman - Everything Will Flow
ClojureTV Zach Tellman - Everything Will Flow Watch on YouTube ↗

A celebrated Strange Loop-era talk on queues: why unbounded queues fail, and the only three responses to overload (buffer, drop, or push back), which is precisely this unit.

0:00 / --:--

Under the Hood: Flow Control Across Asynchronous Boundaries

When an upstream producer generates work faster than a downstream consumer can process it, memory must buffer the delta. If the buffer is unbounded and the overload is sustained, the consumer will eventually crash with an Out-of-Memory (OOM) error, and latency grows unacceptably long before that. Backpressure is the communication feedback loop that signals the producer to slow down or pause.

Backpressure Feedback Loop: Pull-Based Credit Flow

flowchart LR Consumer["Downstream Consumer (3 Workers Free)"] -->|"1. Grant Credit: request(3)"| Producer["Upstream Producer"] Producer -->|"2. Transmit Exactly 3 Items"| Consumer Consumer -->|"3. Process Items & Free Memory"| Work["Complete Work"] Work -->|"4. Grant Next Credit: request(N)"| Producer

Under the Hood: The Three Backpressure Defenses

  • Push vs. Pull (Reactive Streams Specification): Push-based streams are inherently vulnerable to overwhelming consumers. Pull- or demand-based streams invert control: in Reactive Streams consumers signal request(n) for a specific number of items, and HTTP/2 and gRPC flow control grant byte-level window credits. Kafka consumers are simply pull-based: they fetch from the broker at their own pace, with no credit protocol.
  • TCP Window Size (Kernel Flow Control): When an application thread stops reading from a TCP socket, the OS kernel receive buffer fills. The TCP stack automatically advertises a Window Size = 0 back to the sender, forcing the sender's kernel to pause transmitting packets.
  • Dropping Strategies: When queues reach hard capacity, systems must execute an explicit drop policy:
    • Tail Drop: Drop newly arriving messages (protects existing in-flight transactions).
    • Head Drop: Drop the oldest messages in the queue (ideal for time-sensitive data like live GPS telemetry).

Worked example: how fast a queue fills

A consumer processes 1,000 messages per second. A traffic spike sends 1,500 messages per second for 10 minutes. The backlog grows by 500 per second, so after 10 minutes there are 300,000 queued messages. At 2 KB each that is about 600 MB of memory for an in-memory queue.

  • Once the spike ends and arrivals drop back to, say, 800 per second, the consumer drains only 200 per second of backlog, so clearing 300,000 messages takes 25 minutes. A message arriving at the end of the spike waits about 300,000 / 1,000 = 5 minutes before processing.
  • If the spike instead lasts all day, the queue grows by about 43 million messages; no buffer is big enough. Only more consumers, less work per message, dropping, or slowing the producer can fix it.

Little's law makes the link explicit: items in the system = arrival rate x time in system. With a bound of 10,000 queued items and a drain rate of 1,000 per second, the worst queueing delay is about 10 seconds; the bound on the queue is effectively a bound on latency.

Strategies at a limit

StrategyHow it worksGood forCost
Block the producerSend or put call waits when the buffer is fullIn-process pipelines, TCP streamsProducer threads stall; risk of deadlock across cycles
Credit or demand signallingConsumer grants N credits; producer sends at most NReactive Streams, HTTP/2 and gRPC flow control, AMQP prefetchProtocol support required
Pull-based consumptionConsumer fetches when readyKafka consumers, SQS pollingBacklog accumulates in the broker instead; monitor lag
Reject with 429 or 503Tell the client to retry laterService edges, public APIsClients must back off properly
Drop newest (tail drop)Refuse new items when fullWork where existing items matter moreNew work is lost
Drop oldest (head drop)Discard stale items to make roomTelemetry, live locations, metricsOld items lost; fine when only the latest matters
Sample or aggregateKeep 1 in N, or merge updatesMetrics, logs, UI updatesPrecision

Backpressure in real stacks

  • TCP: the receiver advertises its free buffer as the receive window. If the application stops reading, the window drops to zero and the sender's kernel stops transmitting.
  • HTTP/2 and gRPC: per-stream and per-connection flow-control windows are updated with WINDOW_UPDATE frames.
  • Node.js streams: write() returns false when the buffer exceeds its high-water mark; producers should wait for the drain event.
  • Kafka: consumers pull at their own pace; the log is the buffer, bounded by retention. Consumer lag is the metric to alert on.

Failure modes

  • Retry storms that defeat backpressure: rejected clients that retry immediately keep load high.
  • Head-of-line stalls: blocking on one slow consumer can halt a shared pipeline; isolate per destination.
Check yourself Question 1 of 3
TCP zero window recall
From: Under the Hood: The Three Backpressure Defenses

A consumer's application thread stops reading from its TCP socket, while the sender keeps writing. What happens at the transport layer?

Queue Backpressure & Lag
Live
Queue Depth 0 / 100
Lag: 0.0s
Status: OK

Key Takeaways

Unbounded in-memory queues lead to Out-of-Memory (OOM) container crashes under sustained overload, and to very long queueing delays well before that.
Reactive pull-based flow control requires consumers to issue explicit credits before producers can send data.
Enforce hard queue capacity limits with explicit drop strategies (Tail Drop vs Head Drop).
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue