Backpressure
Learn how backpressure prevents fast producers from overwhelming slow consumers, why unbounded queues are not a solution, and how TCP, Kafka, and reactive streams each implement flow control.
The problem
A streaming pipeline: the event producer generates 100,000 events per second. The consumer can process 50,000 events per second. Without flow control, requests pile up in the buffer between them.
t=0s: producer emits 100K/s Β· consumer processes 50K/s Β· queue grows at +50K/s
t=10s: queue has 500,000 events Β· still growing
t=60s: queue has 3,000,000 events Β· memory pressure visible
t=120s: queue at ~6,000,000 events Β· consumer OOM Β· crashes
t=121s: queue at limit Β· producer still running Β· system down
The naive fix is "just make the queue bigger." This is not a fix. A larger queue buys time but does not change the fundamental math: if the consumer cannot keep up at peak rate, the queue will fill eventually. You have converted a fast crash into a slow one.
The correct fix is backpressure: let the consumer tell the producer to slow down.
What backpressure is
Backpressure is a flow control mechanism where the consumer signals its current capacity to the producer, causing the producer to moderate its emission rate rather than sending unboundedly.
The term comes from fluid dynamics: a pipe with a slow outlet creates literal back-pressure that slows the flow at the inlet. In software, the "back-pressure" is a signal propagated upstream through the call stack or message protocol.
Backpressure addresses three failure modes:
- Memory exhaustion: unbounded queues fill RAM, causing OOM kills.
- Latency collapse: a queue with 10 million items means 10 million items of queuing latency before any item reaches the consumer.
- Cascade failures: a slow downstream consumer causes the producer's outbound buffers to fill, propagating slowdown up the entire call chain.
How backpressure works
The core mechanism is always a feedback loop with three steps:
- The consumer signals capacity (a count, a rate, a boolean "ready" flag, or a window size).
- The producer observes the signal and buffers, slows, or drops in response.
- When the consumer recovers, it signals "ready" again and the producer resumes.
// Consumer-side: high/low watermark backpressure
MAX_QUEUE_SIZE = 10_000
HIGH_WATERMARK = 8_000 // 80%: start throttling
LOW_WATERMARK = 2_000 // 20%: resume after draining
class BackpressuredQueue:
queue = []
paused = false
function push(item):
if len(queue) >= MAX_QUEUE_SIZE:
raise QueueFull("drop or block producer")
queue.append(item)
if len(queue) >= HIGH_WATERMARK and not paused:
paused = true
signal_producer("pause")
function pop():
item = queue.pop_left()
if len(queue) <= LOW_WATERMARK and paused:
paused = false
signal_producer("resume")
return item
The high and low watermarks prevent oscillation: without hysteresis, the queue would toggle between "pause" and "resume" thousands of times per second as it crosses a single threshold. The gap between HIGH and LOW gives the queue a stable operating range.
The watermark algorithm internalized
spawnSync d2 ENOENT
Hysteresis prevents oscillation
Without a gap between pause and resume thresholds, the producer would toggle pause/resume thousands of times per second as queue depth bounces around a single threshold. Setting HIGH_WATERMARK at 80% and LOW_WATERMARK at 20% gives the queue a 60% "breathing room" where neither signal fires.
TCP flow control
TCP implements backpressure natively via the receive window (rwnd) field in the TCP header. The receiver advertises how much buffer space it has available. The sender cannot have more unacknowledged bytes in flight than the advertised window.
Sender β Receiver: [SEQ=1000 Β· DATA 1000 bytes]
Receiver β Sender: [ACK=2001 Β· rwnd=4096]
-- "I have 4096 bytes of buffer space available"
Sender β Receiver: [SEQ=2001 Β· DATA 1000 bytes]
Sender β Receiver: [SEQ=3001 Β· DATA 1000 bytes]
Sender β Receiver: [SEQ=4001 Β· DATA 1096 bytes]
-- Sender hits window limit: pauses
Receiver (buffer drained) β Sender: [ACK=5097 Β· rwnd=8192]
-- "I processed data, 8192 bytes now free" -- Sender resumes
Receiver (buffer full) β Sender: [ACK=5097 Β· rwnd=0]
-- Zero Window: sender must stop completely
The "zero window" advertisement is TCP's hard backpressure signal. When the receiver sends rwnd=0, the sender stops transmitting and waits for a window update, sending small "window probe" packets periodically to detect when the receiver has freed space.
This is why a slow database consumer on the other end of a TCP connection automatically throttles the producer: the database's socket receive buffer fills, it advertises a smaller window, and the sender slows without any application code needed.
Kafka consumer backpressure
Kafka does not push messages to consumers. Consumers poll for messages: the consumer controls the pace by calling poll() only when ready for more work.
// Kafka consumer loop
consumer = KafkaConsumer(
topics=["events"],
max_poll_records=500, // max messages returned per poll call
max_poll_interval_ms=30_000 // consumer must poll within 30s or is kicked
)
while true:
records = consumer.poll(timeout_ms=100) // blocks up to 100ms
for record in records:
process(record) // do the actual work
consumer.commit_offsets()
// if process() is slow, the next poll() is delayed
// Kafka sees the consumer as healthy as long as poll() recurs within max_poll_interval_ms
Kafka's backpressure mechanism:
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.