Lập trình · 21/09/2026

Backpressure for Backends and Workers: Control Load Before the System Overloads

A system does not fail merely because traffic is high; it usually fails because it accepts work faster than it can complete that work for long enough. Requests, messages, or data chunks that have not been processed must wait somewhere: application memory, a connection pool, a broker queue, a socket buffer, or a dependency's queue. If those buffers have no bounds and produce no feedback, latency rises, memory expands, timeouts trigger retries, and the entire system enters a load-amplification loo

Backpressure cho Backend và Worker: Kiểm soát tải trước khi hệ thống quá tải

A system does not fail merely because traffic is high; it usually fails because it accepts work faster than it can complete that work for long enough. Requests, messages, or data chunks that have not been processed must wait somewhere: application memory, a connection pool, a broker queue, a socket buffer, or a dependency's queue. If those buffers have no bounds and produce no feedback, latency rises, memory expands, timeouts trigger retries, and the entire system enters a load-amplification loop.

Backpressure is the mechanism by which a slower consumer signals upstream producers to slow down, wait, reduce in-flight work, or accept a controlled rejection. It is not one library. It is an end-to-end design principle spanning the HTTP edge, application executors, databases, message brokers, and workers. This guide explains how to locate bottlenecks, set finite limits, choose overload responses, and test behavior beyond capacity.

1. What problem does backpressure solve?

Consider an API receiving 500 requests per second while its downstream dependency can sustainably complete only 300. The extra 200 requests per second do not disappear. After one minute, 12,000 items are waiting, before retries are counted. If the application retains each request, context, payload, and promise in RAM, a latency problem becomes heavy garbage collection, then an out-of-memory event or restart. Moving everything to a broker only relocates the backlog; completion time can still exceed the user's expectation.

Backpressure makes the system acknowledge finite capacity. At a threshold, it may pause reads, lower concurrency, stop fetching messages, return a retryable rejection, or enter a deliberate degraded mode. The goal is not to eliminate demand. The goal is to keep the service in a predictable operating region: bounded memory, bounded waiting, protected dependencies, and explicit outcomes for callers.

MechanismWhat it controlsRisk when absent
Bounded queueWork waiting inside a processMemory grows until the process becomes unstable
Concurrency limitTasks competing for CPU or a dependencyPools exhaust while switching and timeouts increase
Admission controlWhether new work may enterEvery request slows down and then fails together
Flow controlProducer rate relative to consumer rateBuffers accumulate between stages
Deadline and cancellationWhether obsolete work keeps resourcesThe server computes results nobody needs

Backpressure is not the same as autoscaling. Scaling out may add capacity when work is parallelizable and dependencies have headroom, but starting instances takes time and cannot fix a saturated database. Backpressure is the immediate safety boundary; autoscaling is a slower resource-supply response. Production systems commonly need both.

2. Map the load path and find hidden queues

Before changing a setting, map the full journey of one unit of work. For HTTP, the path may include a load balancer, socket accept queue, middleware, executor, connection pool, database, and downstream service. For a worker, it may include a broker partition, consumer prefetch, client buffer, internal executor, and result store. Every point can contain a queue even when its configuration does not use the word “queue.”

For each point, record four facts: maximum capacity, arrival rate, departure rate, and policy when full. A “temporary” buffer without a limit is where an incident hides. Total response time includes waiting at every layer, so measuring only handler execution creates the false impression that the application remains fast.

  • Socket and HTTP: how many connections are accepted, how many requests run concurrently, and how long does keep-alive retain resources?
  • Executor: is the task queue bounded, what happens on rejection, and do CPU and I/O work share one pool?
  • Database: how many connections exist, how long can acquisition wait, and do queries have deadlines?
  • Broker: how many unacknowledged messages can a consumer hold, how large are messages, and how quickly does backlog age?
  • Downstream: does the client limit in-flight calls and apply timeouts, a retry budget, and a circuit breaker?

A useful principle is to queue work close to the component that has enough information to make the right decision. A durable broker suits asynchronous work that may complete later. Application memory suits only small, short-lived waiting. A web server's RAM should not accidentally become a message broker.

3. Use Little's Law to reason about load and latency

In a stable system, the average amount of work in the system is approximately completion rate multiplied by average residence time: L = λ × W. This relationship does not replace benchmarking, but it is a useful consistency check. If a service completes 200 requests per second and each request spends 0.5 seconds in the system, about 100 requests will be processing or waiting at any moment.

When arrivals exceed completions for a sustained period, the system is not stable and the queue grows continuously. A larger queue only postpones failure; it does not create throughput. It can also let work expire before execution, wasting CPU on results whose callers have already timed out.

A useful limit is not derived from spare RAM. It follows from the latency SLO, dependency capacity, per-item size, and how long a result remains valuable.

If an endpoint has an 800 ms end-to-end deadline and useful processing normally requires 300 ms, several seconds of queueing cannot be reasonable. Admission control should reject work when predicted waiting has already consumed the budget. A fast rejection is often safer than allowing all requests to time out after a long wait.

4. Build bounded queues and concurrency limits

The two most important controls are queue length and concurrency. A queue absorbs a short burst; concurrency determines how many tasks compete for CPU, connections, and bandwidth. Increasing concurrency helps until a resource saturates. Beyond that point, throughput barely improves while latency, errors, and context-switching costs rise sharply.

Do not use one semaphore for every workload. A lightweight cache read, a database write, and a report-generation task have different resource profiles. Separate bulkheads prevent one slow class from consuming every slot. A global budget is still needed so the sum of independent pools does not overload the process or a shared dependency.

async function submit(task, deadline) {
  if (deadline.expired()) return reject("deadline_exceeded");
  if (!queue.tryPush({ task, deadline })) return reject("overloaded");
}

async function workerLoop() {
  for await (const item of queue) {
    if (item.deadline.expired()) continue;
    await concurrencyPermit.run(() => item.task(item.deadline));
  }
}

This is pseudocode that illustrates the contract, not a portable runtime API. tryPush must be bounded and return promptly; permits must be released in a finally path; cancellation must reach the database or HTTP client. If an outer timeout stops waiting while the query continues, the lower-level slot remains occupied and the apparent backpressure is incomplete.

Ordering also matters. FIFO is simple but lets a batch of heavy tasks block interactive requests. A priority queue can support service classes, but it needs starvation protection and per-tenant limits. Fair queueing by customer or workload prevents one noisy producer from consuming all capacity.

5. Admission control for HTTP APIs

At the HTTP edge, the service should decide early whether it has enough budget to accept a request. Signals can include in-flight requests, queue depth, predicted wait, pool utilization, and dependency state. CPU by itself is often a late signal; by the time CPU is visibly high, queues may already be too long.

A temporary overload rejection must be consistent and must not pretend success. Use a status appropriate to the API contract, plus a machine-readable error code, correlation ID, and retry guidance only when retry is actually safe. If the response includes Retry-After, derive it from a real policy rather than using a decorative constant. Write requests need idempotency keys or uniqueness constraints before clients are encouraged to retry.

  • Limit request body size and upload rate so slow clients cannot retain connections indefinitely.
  • Set an end-to-end deadline and subtract budget as each downstream call begins.
  • Do not enqueue a request that is cancelled or already past its deadline.
  • Reserve capacity for health checks and essential operational endpoints.
  • Apply tenant quotas or fairness so one customer's load does not spread to everyone.

Rate limiting and backpressure are related but distinct. Rate limiting governs request quantity over a window or identity, which supports policy and fairness. Backpressure reflects the consumer's current capacity. A client within quota may still be rejected while a dependency is degraded; conversely, an idle system must still enforce business quotas.

6. Flow control in streams and data pipelines

In a pipeline, the consumer must influence the producer's pace. In Node.js streams, for example, a producer should stop writing when write() returns false and wait for the drain event. Ignoring that signal allows the buffer to grow. A runtime's standard pipeline mechanism is usually safer than manually connecting callbacks because it coordinates flow control, errors, and resource cleanup.

Every stage needs backpressure: file reading, decompression, parsing, transformation, database writes, and network sends. Limiting only the final stage is insufficient if an intermediate stage gathers the full dataset into an array. Prefer chunked processing, avoid unnecessary payload copies, and bound buffers by bytes rather than only item count when item sizes vary significantly.

Even when a transport protocol has a flow-control window, do not assume it protects the entire application. TCP can slow a byte sender but does not know how much CPU or how many queries a business message consumes. Application-level in-flight limits, deadlines, and resource controls are still required.

7. Message consumers: prefetch, acknowledgement, and backlog

A worker should stop obtaining messages when its capacity is occupied. Prefetch, or a limit on unacknowledged deliveries, is a form of backpressure: the broker does not deliver beyond that threshold to the consumer. A value that is too high lets one worker hold many messages it cannot process, consuming memory, reducing distribution fairness, and making shutdown harder to drain. A value that is too low can leave a worker idle when network latency is significant.

Choose prefetch from actual concurrency, job duration, message size, and acknowledgement behavior. Each execution slot generally needs only a small intentional buffer. Acknowledge only after mandatory effects are durable; acknowledging first can lose work after a crash. A crash after commit but before acknowledgement can redeliver a message, so handlers must tolerate repetition.

SignalPossible meaningInvestigation
Backlog rises while worker CPU is lowDependency waiting or overly cautious fetchingMeasure every dependency and connection state
Unacknowledged count is high without throughput growthPrefetch exceeds capacity or jobs are stuckLower prefetch, add deadlines, detect stuck jobs
Oldest-message age risesWorkers do not meet real demandScale if dependencies allow, shed load, or slow producers
Redelivery risesCrashes, lease expiry, or incorrect acknowledgement orderInspect idempotency, job duration, and shutdown

A durable backlog is not inherently wrong when the product permits batch processing, but it needs an age SLO. Counting messages while ignoring the oldest message can hide a small queue that has been stuck for hours.

8. Retries must share the same load budget

When a dependency slows down, unconditional retries turn one request into many at precisely the moment the system is least able to serve them. Backpressure works only when retries honor the deadline, cap attempts, apply exponential backoff, and add jitter. Do not retry permanent errors, start an attempt without enough remaining time, or allow every layer to retry independently without one shared budget.

A retry queue must also be bounded or durable. Holding every failed task in memory for another attempt lets the recovery mechanism cause an out-of-memory failure. For asynchronous work, a dead-letter flow needs a maximum attempt count, final reason, and defined handling process. For synchronous requests, returning a structured error and letting the caller follow the contract is often safer.

A circuit breaker can reduce calls to a failing dependency, but it does not replace a concurrency limit. During half-open probing, only a very small number of requests should test recovery. Allowing thousands of callers to probe together creates another surge.

9. Observe saturation, not only errors

Errors are a late signal. A backpressure dashboard should show demand, throughput, and saturation together. Measure arrivals, completions, in-flight work, queue depth, oldest-item age, queue wait, service time, rejection, timeout, cancellation, and every pool's utilization. Use histograms for wait and execution time because averages hide the long tail.

  • Queue wait / total latency: a rising ratio shows that congestion precedes the handler.
  • Pool utilization: sustained near-full usage plus growing wait indicates saturation.
  • Rejected work: classify by endpoint, tenant, and reason.
  • Cancellation effectiveness: after the caller cancels, does the dependency really release resources?
  • Offered versus completed load: a sustained gap becomes backlog or shed load.

Alerts should reflect SLOs and trends. A depth of 1,000 may be normal for 10 ms jobs and severe for 30-second jobs. Work age and estimated drain time are usually more meaningful than one fixed item-count threshold.

10. Test overload and choose degradation policies

A load test should do more than discover the highest requests-per-second number. Increase demand gradually beyond saturation and observe how the service fails. A sound design keeps memory approximately bounded, avoids a sudden throughput collapse, rejects early according to policy, and recovers quickly when demand falls. A poor design accepts everything, lets latency grow without limit, and remains unstable long after the load source stops.

  1. Measure a baseline with steady traffic and representative payloads.
  2. Create a short burst to verify that queues absorb it without breaking the SLO.
  3. Sustain offered load above capacity and verify rejection plus the memory ceiling.
  4. Slow the database or a downstream service and confirm pressure propagates upstream.
  5. Cancel clients mid-flight and verify lower-level work actually stops.
  6. Restore dependencies, lower demand, and measure time to normal operation.

Product owners should define degradation policy before an incident. Some endpoints may return stale cached data, omit nonessential fields, or convert work to an asynchronous job. A financial endpoint may need to reject completely rather than return a partial result. Random production exceptions should not become the service's priority mechanism.

Production checklist

  • Every in-memory queue has an explicit bound and a depth metric.
  • Every pool has a concurrency limit tied to the actual constrained resource.
  • Requests and jobs carry deadlines, and cancellation reaches dependencies.
  • At capacity, the system rejects or delays work according to a defined contract.
  • HTTP admission control, rate limits, and tenant quotas have distinct roles.
  • Consumer prefetch matches concurrency, message size, and job duration.
  • Acknowledgement, idempotency, and retry budgets protect against loss and duplication.
  • Dashboards show offered load, throughput, queue wait, saturation, and rejection.
  • An overload test proves bounded memory and rapid recovery beyond saturation.

References

Discussion

Comments 0

Sign in to comment

You need an account to join the discussion and reply to other readers.

Sign inRegister

No comments yet. Be the first to share your thoughts.