A system can be fast and still be badly designed.
Your API may accept thousands of requests per second. Kafka may deliver messages almost instantly. Your application may create hundreds of asynchronous tasks in milliseconds.
But what happens when the next component in the pipeline cannot keep up?
This is where backpressure becomes important.
What Is Backpressure?
Backpressure is a system's ability to control how quickly new work is produced when downstream components are already operating at capacity.
Imagine this pipeline:
API → Message Queue → Worker → Database
Suppose the API receives 1,000 requests per second, but the database can only process 400 records per second.
Incoming work: 1,000 requests/second
Processing capacity: 400 requests/second
Backlog growth: 600 requests/second
After one minute, 36,000 requests are waiting. After ten minutes, the backlog reaches 360,000.
The API may appear fast, but the overall system is accepting work faster than it can safely complete it.
How Backpressure Protects a System
Backpressure allows the slower component to influence the faster producer.
The downstream service is effectively saying:
I am at capacity. Slow down until I have room.
Depending on the architecture, the system may respond by:
- Pausing message consumption
- Limiting concurrent operations
- Rejecting requests temporarily
- Buffering work in a bounded queue
- Dropping low-priority work
- Scaling the number of consumers
Why a Queue Is Not Enough
A queue can absorb temporary traffic spikes, but it cannot fix a permanent difference between production and processing rates.
If producers continuously create 1,000 jobs per second while consumers process only 400, the queue will keep growing regardless of its size.
Eventually, storage fills up, messages become outdated, processing latency increases, or infrastructure costs rise unexpectedly.
A queue handles temporary bursts. Backpressure handles sustained imbalance.
Limit Concurrent Work
Consider the following JavaScript code:
await Promise.all(
users.map((user) => sendEmail(user))
);
If the application contains 100,000 users, it may attempt to create 100,000 operations almost simultaneously.
A safer approach is to process a controlled number of tasks at a time:
async function processWithLimit(items, limit, handler) {
const queue = [...items];
async function worker() {
while (queue.length > 0) {
const item = queue.shift();
if (item !== undefined) {
await handler(item);
}
}
}
const workers = Array.from(
{ length: Math.min(limit, items.length) },
() => worker()
);
await Promise.all(workers);
}
await processWithLimit(users, 10, sendEmail);
Only ten email requests now run concurrently. The complete batch may take longer, but the application remains stable.
Backpressure and Retry Storms
Retries can make an overloaded system even less stable.
Suppose a database starts timing out and every failed request is retried three times:
Original requests: 1,000
Retry attempts: 3,000
Total attempts: 4,000
Instead of giving the database time to recover, the application sends it even more traffic.
Retries should use exponential backoff, random jitter, maximum attempt limits, and circuit breakers.
Signs of a Backpressure Problem
Common warning signs include:
- Continuously increasing queue depth
- Growing Kafka consumer lag
- High memory consumption
- Database connection pool exhaustion
- Increasing processing latency
- Large numbers of pending tasks
- Repeated timeouts and retry storms
The most important comparison is the rate at which work enters the system versus the rate at which it leaves.
The Mental Model
Safe throughput is limited by the slowest essential component.
A fast producer does not automatically create a fast system. It may only create a backlog more quickly.
When designing an asynchronous pipeline, ask where excess work will wait, whether that waiting area is bounded, and what the system will do when it becomes full.
Strong systems are not designed only around the amount of work they can process. They are also designed around the work they cannot process.