Back-pressure for Node.js services
When a downstream service slows down, HTTP tends to turn that into a cascading failure. A queue absorbs the spike instead. Here's the difference, and the trade-offs to watch.
Every system has a breaking point under load. What matters is how it breaks. With synchronous HTTP calls between services, a slowdown in one place tends to propagate outward into a cascading failure. With a queue between services, the same spike gets absorbed. Understanding why is worth more than any single resilience library.
Why HTTP struggles under load
Say service A calls service B over HTTP, and B gets slow — a dependency is struggling, or traffic spiked. Here's the chain reaction:
- A's requests to B pile up as open connections, each holding memory and a socket while it waits.
- A's own callers are now waiting on A, so the pressure propagates upstream.
- Timeouts fire, clients retry, and retries add more load to the very service that's already overwhelmed.
- Eventually connection pools exhaust and healthy requests fail alongside the unhealthy ones.
Teams patch this with timeouts, circuit breakers, and bulkheads — all genuinely useful — but they're mechanisms to fail faster, layered on top of a transport that couples caller and callee tightly in time.
What a queue changes
When A talks to B through a message queue, A doesn't hold a connection open waiting for a free instance of B. It puts a message on B's queue. If B is momentarily slower than its inflow, the queue grows — it buffers the spike rather than converting it into failed connections. B works through the backlog at its own pace, and when consumers catch up, the queue drains.
That's back-pressure done structurally: the queue is a shock absorber between producer and consumer. Add more consumers (more instances of B) and the backlog clears faster, with no balancer to reconfigure.
@imqueue gives you a couple of levers that matter here:
- Delivery mode. In guaranteed (safe) mode the hand-off from queue to consumer is protected: a message taken by an instance that died before starting on it is rescheduled rather than lost. You trade some throughput for that. Work already inside a handler when the process dies is a separate problem, and the answer to it is draining on shutdown.
- A recovery deadline.
safeDeliveryTtlgoverns how quickly an abandoned hand-off returns to the queue — not how long a handler may run. Slow-but-healthy work is not re-queued for being slow, so tune it for how fast you want recovery, not against your worst-case processing time.
The trade-offs to respect
A queue changes the failure mode; it doesn't repeal physics:
- A backlog is latency. Buffering a spike means requests wait. That's usually far better than failing them, but for user-facing, latency-critical calls you still want enough consumers that the queue stays shallow.
- Unbounded growth is its own failure. If inflow permanently exceeds what your consumers can handle, the queue grows without bound. Monitor queue depth and scale consumers (or shed load deliberately) — a growing backlog is the signal.
- Not everything should wait. Truly time-sensitive interactive calls may still want a fast-fail path; use back-pressure where deferral is acceptable.
The headline: HTTP couples services in time, so overload spreads; a queue decouples them, so overload is absorbed and bounded by how fast you choose to consume. If cascading failures under load are a recurring pain, that structural difference is the lever. See Getting Started to try the model, and the delivery-mode options in the API reference.
Read this page as plain markdown — no HTML, no navigation. For pasting into an LLM, or for an agent to fetch.
Building on @imqueue? The open-source packages live on GitHub and the docs at imqueue.org. Shipping inside a closed-source product? See commercial licensing & support.