






















Your worker is “healthy.” CPU is fine. No 500s. No restart loops.
But queue lag keeps climbing. Memory grows all day. Retries spike. By evening, one dependent API slows down and your whole pipeline falls over.
That is a backpressure failure.
Most teams treat this as a scaling problem (“add more pods”). Usually it is a flow-control problem: producers can create work faster than consumers can safely process it, and nothing in the system says “slow down.”
This is the practical fix in Node.js: bounded concurrency, explicit in-flight limits, and stream-aware processing that applies pressure before memory bloats.
Backpressure is a feedback signal from a slower stage to a faster stage.
Without it, your app buffers unbounded data in memory (or in queues) and dies slowly.
With it, throughput stays close to the real downstream capacity.
A common worker loop looks safe but is not:
for (const job of jobs) {
processJob(job); // fire-and-forget
}
Or slightly “better”:
await Promise.all(jobs.map(processJob));
Both can overload downstream services and your own process memory.
Symptoms you can observe in production:
Start with a fixed in-flight limit.
import pLimit from 'p-limit';
const CONCURRENCY = 20;
const limit = pLimit(CONCURRENCY);
export async function processBatch(messages) {
const tasks = messages.map((msg) =>
limit(async () => {
await handleMessage(msg);
})
);
const results = await Promise.allSettled(tasks);
// Ack only succeeded messages; Nack/requeue failed ones explicitly.
return results;
}
Why this works:
This single change removes a lot of “random” incidents.
Fixed concurrency is good. Adaptive concurrency is better under variable latency.
A simple rule:
handleMessageKeep guardrails:
Do not chase every second of noise; adjust every 15–30s window.
write() return valuesIf you process large payloads/files/events via streams, Node already gives you backpressure signals.
The key rule: when writable.write(chunk) returns false, stop writing until drain.
import { once } from 'node:events';
async function pump(readable, writable) {
for await (const chunk of readable) {
if (!writable.write(chunk)) {
await once(writable, 'drain');
}
}
writable.end();
}
Better: use pipeline() so error handling and teardown are correct.
import { pipeline } from 'node:stream/promises';
await pipeline(sourceStream, transformStream, sinkStream);
If you ignore this and keep writing, you create hidden in-memory queues and eventually OOM.
Backpressure is not only code. Broker settings must match.
For RabbitMQ-style workers:
prefetch to your real in-flight limit (or a small multiple)For Kafka-style consumers:
If broker fetch size is huge while app concurrency is tiny, you still buffer too much.
You need four graphs per worker:
Alert on trends, not single spikes:
This tells you whether to lower concurrency, scale horizontally, or fix a dependency.
If you only add pods first, you often amplify overload against the same dependency.
min(2 * vCPU, 32) as a starting point1x–2x concurrencyTune from there using real p95 and queue-age trends.
Backpressure is not an optimization. It is a reliability control.
If your service handles asynchronous work and you cannot answer “where do we apply pressure when downstream slows?”, you have an outage with a timestamp missing.
Add bounded concurrency, respect stream drain signals, and align broker fetch with real processing capacity. Most “mystery queue meltdowns” disappear after that.
A lot of delivery teams that build high-throughput Node.js systems treat this as a non-negotiable baseline. Yojji, for example, often emphasizes these reliability controls in backend and cloud-heavy projects where scaling safely matters more than peak benchmark numbers.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。