Architecture

Backpressure: What Happens When Producers Are Faster Than Consumers?

Learn how backpressure stops growing queues with bounded buffers, rate limits, concurrency limits and retries, so producers never overload consumers.

Aarav Sharma

Aarav Sharma

October 31, 20268 min read
Share
A fast producer and a slower consumer kept in balance by a bounded queue that waits or rejects work when it is full

Your API accepts every request, drops each job into a queue, and a worker processes them in the background. It works until one day the queue holds tens of thousands of jobs, response times climb, memory usage keeps rising, and the database starts refusing connections.

The cause is simple: one part of your system creates work faster than the next part can finish it. You see it in image-processing APIs, payment event handlers, and workers that write records to a database. A queue can hide the mismatch for a while, but it does not make the consumer any faster.

Backpressure gives your system a way to control incoming work before a temporary slowdown becomes an outage. In this post you build a bounded queue in Node.js, decide what happens when it fills up, and add retries that do not make an overload worse.

Quick Answer

Backpressure is a flow-control mechanism that keeps a system stable when work arrives faster than it can be processed. It stops queues and buffers from growing without limit by capping accepted work, slowing producers, restricting concurrency, or rejecting requests when capacity runs out. Common techniques include bounded queues, rate limiting, concurrency limits, retries with backoff, and load shedding. The right choice depends on whether the work can wait, must be preserved, or can safely be rejected.

What Is Backpressure?

A producer creates jobs or requests, a queue holds work that is waiting, and a consumer processes it. Backpressure applies when the consumer cannot keep up with the producer, and the system needs to signal or enforce a limit.

Backpressure does not mean stopping all traffic. It means keeping the flow within the capacity of the queue, the worker, the database, and every other dependency on the path. The system might slow the producer, accept only a limited number of jobs, ask a client to retry later, or reject new work until capacity returns.

The term is common in streaming libraries too. Node.js streams, for example, pause a fast readable stream until a slow writable stream catches up, as the Node.js guide to backpressuring in streams explains. The same idea applies to any producer and consumer, whether they share a process or sit on opposite sides of a message broker.

The Problem

Without flow control, an unlimited queue turns a short traffic spike into a lasting resource problem. Take an illustrative example: your API receives 500 requests per second and its worker completes 50 jobs per second. The queue grows by 450 jobs every second, which is 27,000 extra jobs after one minute if those rates hold.

A producer sends 500 jobs per second into an unbounded queue while a consumer completes 50 jobs per second, so the queue grows by 450 jobs every second and holds 27,000 extra jobs after one minute

That backlog has several effects:

  • Queue growth: incoming work piles up faster than workers can complete it.
  • Memory exhaustion: an in-memory queue consumes more memory as it grows.
  • High latency: each new job waits longer before processing begins.
  • Consumer overload: too many simultaneous jobs compete for CPU, connections, and other resources.
  • Database pressure: workers generate more queries or connections than the database can safely handle.
  • Cascading failures: slow dependencies delay work, and the delay spreads pressure to other services.
  • Data loss: jobs held only in memory disappear if the process crashes.

Adding a queue alone does not fix the mismatch. A queue absorbs a temporary burst, but if the producer keeps running faster than the consumer, the backlog keeps growing. You need a limit, and a clear decision about what happens when the system reaches it.

The Solution

A backpressure-aware design controls how much work enters the processing pipeline. Several techniques work together:

TechniquePurpose
Bounded queuePrevent unlimited buffering
Rate limitingRestrict how quickly requests enter
Concurrency limitControl simultaneous work
Producer throttlingSlow down the source of work
Load sheddingReject or drop selected work during overload
Retry with backoffAvoid immediate retry storms
Durable queuePreserve important jobs through process failures
MonitoringReveal when incoming work exceeds processing capacity

When the queue has room, the system accepts work. When the queue is full, the system follows a policy you defined in advance: wait, reject, retry later, redirect the work, or drop selected low-priority jobs.

Build a bounded queue in Node.js

Start with a queue that has a maximum size. This example runs directly with Node.js and shows the core rule: once the queue is full, enqueue() refuses more work.

class BoundedQueue {
  constructor(maxSize) {
    this.maxSize = maxSize;
    this.items = [];
  }

  enqueue(item) {
    if (this.items.length >= this.maxSize) {
      return false;
    }

    this.items.push(item);
    return true;
  }

  dequeue() {
    return this.items.shift();
  }

  size() {
    return this.items.length;
  }
}

const queue = new BoundedQueue(3);

console.log(queue.enqueue("Job 1")); // true
console.log(queue.enqueue("Job 2")); // true
console.log(queue.enqueue("Job 3")); // true
console.log(queue.enqueue("Job 4")); // false
console.log(queue.size());           // 3

Save the code as bounded-queue.js and run it:

node bounded-queue.js

The fourth job is rejected because the queue has reached its capacity of 3. On the other side, dequeue() hands the consumer the oldest job first, so workers process jobs in the order they arrived. In an API, the handler uses that false to decide whether to accept the request or tell the client to try again later.

Producer calling enqueue() on a bounded queue with a capacity of 3 that holds Job 1, Job 2 and Job 3, so enqueue() returns false for Job 4 while the consumer takes jobs out with dequeue()

This example is deliberately small. A production queue also needs a policy for rejected work, safe error handling, and durable storage when jobs must survive a restart. A queue held only in your application's memory gives you no persistence.

Limit consumer concurrency

Queue size controls how much work waits. Concurrency controls how many jobs a worker processes at the same time. Without a concurrency limit, a worker can start so many operations that it overloads a database, an external API, the CPU, or a connection pool.

Choose the concurrency limit from the capacity of the whole path, not just the worker. More workers can raise throughput, but they also push more simultaneous queries at a database that may already be struggling. Watch the downstream dependency before you raise the limit.

Apply rate limiting and define full-queue behavior

Rate limiting controls how quickly requests enter the system. It helps when a traffic spike could overwhelm a downstream component before the queue has time to react. Backpressure is broader: it manages work according to downstream capacity, and can combine rate limits, bounded buffers, and feedback to the producer.

A full queue needs an explicit policy, and the right one depends on the kind of work. A user-facing endpoint returns 429 Too Many Requests, and the client retries with backoff rather than immediately. A durable background queue holds critical jobs until workers have capacity. Low-priority analytics events can be dropped, if your business requirements allow it.

Flow from a new job to a capacity check: a queue with room accepts and enqueues the job, while a full queue returns 429 for a user-facing request, persists a critical job to a durable queue, or drops a low-priority event

Retry with backoff

Immediate retries add traffic to a system that is already overloaded. Exponential backoff increases the wait between attempts, which gives the service time to recover. This helper takes an asynchronous operation that throws when an attempt fails; the demo at the bottom fails twice and then succeeds.

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function retryWithBackoff(operation, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      if (attempt === maxRetries) {
        throw error;
      }

      const delay = 500 * 2 ** attempt; // 500 ms, 1000 ms, 2000 ms
      console.log(`${error.message}, retrying in ${delay} ms`);
      await sleep(delay);
    }
  }
}

// Demo: an operation that fails twice, then succeeds
let calls = 0;

async function flakyOperation() {
  calls++;
  if (calls < 3) {
    throw new Error(`Attempt ${calls} failed`);
  }
  return `Succeeded on attempt ${calls}`;
}

retryWithBackoff(flakyOperation)
  .then(result => console.log(result))
  .catch(error => console.error(error.message));

Run it with node retry-with-backoff.js. It waits 500 ms after the first failure and 1000 ms after the second, then prints Succeeded on attempt 3.

In a distributed system, add jitter, a small random variation to each delay, so many clients do not retry at exactly the same moment. The AWS Architecture Blog post on exponential backoff and jitter compares several jitter strategies. Retry only errors that are safe to retry, and make operations idempotent wherever a repeated request could create a duplicate effect.

Monitor queue pressure

Backpressure works best when you can see it happening. Track producer rate, consumer rate, queue depth, processing latency, rejected requests, active workers, retry count, error rate, and memory usage. A queue depth that keeps rising is the clearest sign that consumers are not keeping up.

Use those metrics to decide whether to reduce incoming traffic, adjust worker concurrency, speed up processing, or add capacity. Do not raise concurrency automatically without checking database connections, CPU, memory, and external-service limits. The goal is stable throughput, not the largest possible number of active jobs.

Trade-offs

Backpressure protects your resources, but it does not make product decisions for you. A bounded queue rejects legitimate work during a burst. That is acceptable for a request the client can retry, and wrong for a payment or another critical operation unless you persist the request first.

Durable queues protect important jobs from process failure, but they add infrastructure and operational work. Retries recover from temporary failures, yet excessive retries amplify an outage, and they duplicate work when operations are not idempotent. More workers can increase throughput while overloading the database or external service they depend on.

A fixed rate limit is the wrong tool when your bottleneck changes over time. It rejects traffic even when downstream capacity is available, while backpressure responds to the actual queue depth and consumer load. Choose the simplest combination that matches your workload, define what happens at capacity, and test the failure cases instead of assuming one technique covers every overload.

Conclusion

On Monday, compare how fast your application accepts work with how fast it completes it. Put a bound on every queue, set worker concurrency from the capacity of the database and services behind it, and write down what a full queue does: wait, reject, retry, redirect, or persist. Then add alerts on queue depth, latency, rejected requests, and retries so you see a growing backlog before it becomes an outage. For critical jobs, use durable storage and safe retries, and never let an in-memory queue hold the only copy of work you cannot lose.

Need help designing scalable backend systems, queue-based architectures, or reliable event-processing workflows? Our development team can help you design, build, and deploy systems with proper flow control, scalability, and monitoring. Contact us

Aarav Sharma

Aarav Sharma

Lead Software Engineer

Aarav leads product engineering at Matlab Infotech, where he has shipped mobile and web platforms across healthcare, fintech, and SaaS. He writes about pragmatic engineering and shipping fast without cutting corners.

Related articles

Let's Collaborate

Tell us about your project and we'll come back with a plan, a timeline, and a quote.

Project Type

Budget

Task Message

Your Contacts