Stackbook Logo
resilience

Backpressure Implementation

Intent

Propagate capacity limits from slow consumers back to producers so systems slow down gracefully instead of exhausting memory.

Problem

When a producer is faster than its consumer, work piles up in unbounded buffers and queues until memory is exhausted or latency becomes meaningless. The failure appears far from its cause, often as an out-of-memory crash. Dropping data blindly or buffering endlessly both lose.

Forces

    Solution

    Backpressure Implementation

    Backpressure is the mechanism by which a slow consumer tells a fast producer to ease off. Without it, the system has an implicit unbounded buffer somewhere, and that buffer eventually fills memory. With it, overload becomes a controlled slowdown or an explicit rejection.

    How it works

    Every hop in a pipeline needs a bounded amount of in-flight work and a way to signal it.

    • Bounded queues: a fixed-capacity queue between stages. When full, a producer's put either blocks, fails, or drops.
    • Credit-based flow control: the consumer grants the producer a number of credits; the producer sends only while it holds credits. HTTP/2, gRPC, and reactive streams work this way.
    • Pull-based consumption: the consumer requests the next batch when ready, as with Kafka consumers polling at their own pace.
    • Rejection at the edge: if the entire pipeline is saturated, the ingress returns 429 or 503 so clients slow down or retry later.

    Choose the full-queue policy deliberately: block when data must not be lost and producers can wait; drop newest or oldest when freshness matters more than completeness; reject when a caller can handle and retry.

    Implementation steps

    1. Draw the pipeline and find every buffer, including thread pool queues, socket buffers, and library defaults.
    2. Replace unbounded buffers with bounded ones sized from rate times acceptable delay.
    3. Pick the full-queue policy for each stage and document it.
    4. Use pull or credit protocols where available instead of push without feedback.
    5. Connect the final stage's limit to the edge, so saturation produces a client-visible signal.
    6. Add timeouts to blocking puts to avoid deadlock, and ensure no cycle of stages can wait on each other.
    7. Test by throttling the slowest consumer and observing memory, queue depth, and producer behavior.

    Common pitfalls

    • Unbounded executor queues in default thread pool configurations.
    • Blocking an event loop thread while waiting for capacity.
    • Applying backpressure in one service but buffering without limit in the next.
    • Dropping silently with no counters, hiding data loss.
    • Buffers sized for the worst burst, which only hides the problem and adds latency.

    Metrics to watch

    • Queue depth and age of the oldest item at each stage.
    • Producer blocked time and rejected or dropped counts.
    • Consumer lag in offsets or seconds.
    • Memory use and garbage collection pressure.
    • End-to-end latency percentiles during bursts.

    Example code

    import queue, threading
    
    buf = queue.Queue(maxsize=1000)   # bounded buffer
    
    def produce(item):
        try:
            buf.put(item, timeout=0.05)   # block briefly, then give up
            return True
        except queue.Full:
            metrics.incr("dropped_full")  # explicit, counted policy
            return False                  # caller returns HTTP 429
    
    def consume():
        while True:
            item = buf.get()
            process(item)                 # slow stage sets the pace
            buf.task_done()
    

    Key takeaways

    • Every buffer must be bounded and its full-policy chosen explicitly.
    • Prefer pull or credit-based protocols over unconstrained push.
    • Carry the signal all the way to the edge.
    • Count every drop and rejection.
    • Test by slowing the consumer and watching where memory goes.

    ✓ When to Use

    • •Streaming and event-processing pipelines
    • •Producer-consumer systems with uneven rates
    • •Services with in-memory work queues
    • •Ingestion APIs feeding slower storage

    ✗ When Not to Use

    • •Fire-and-forget telemetry where dropping is acceptable and simpler
    • •Systems with durable queues that already absorb bursts by design
    • •Low-volume synchronous systems with no buffering

    Pros

    • +Bounds memory and latency
    • +Surfaces overload instead of hiding it
    • +Stabilizes pipelines under bursts
    • +Enables explicit, deliberate drop policies

    Cons

    • −Can block upstream threads and propagate slowness
    • −Needs end-to-end design to be effective
    • −Drop policies lose data
    • −Deadlocks possible with circular dependencies

    Cost Profile

    Infrastructure

    Operational

    Cognitive

    Failure Modes

      Alternatives

        Related Patterns

          Competency Domains

          distribution communicationdata statereliability ops