Description
What is backpressure?
Backpressure is the mechanism by which a slower stage in a pipeline pushes back on the faster stages feeding it. When a consumer cannot keep up, it stops accepting new work — a bounded Message Queue refuses inserts, a stream stops reading, an API answers 429 or 503 — and the producer responds by slowing down, pausing or holding work at the source. Without it, the gap between arrival rate and processing rate accumulates as an ever-growing queue, rising memory and latency, and eventually a crash that loses everything in flight.
In queueing terms, if jobs arrive at λ per second, each takes S seconds and there are c workers, utilisation is ρ = λ × S ÷ c. Below 1 the queue fluctuates but drains; at or above 1 it grows without limit, however large the buffer. Backpressure keeps ρ below 1 by lowering λ at the source. In a WhatsApp send pipeline the hardest downstream limit is the phone number's Throughput — 80 messages per second by default, 1,000 after an automatic upgrade — and pushing past it returns Error 130429 (Rate Limit Hit) instead of going faster.
Backpressure is flow control that slows work down; it discards nothing. Load Shedding drops or defers low-priority work when the system is saturated, and Rate Limiting is an externally imposed cap, such as Meta's throughput or an API quota. Good pipelines combine all three: a rate limiter paces sends to the number's limit, backpressure carries that pace upstream to campaign schedulers and CSV importers, and load shedding protects OTPs if the backlog grows anyway.
Key takeaways
- Backpressure is a signal from a slow stage telling producers to slow down, so work waits at the source instead of piling up without bound.
- If producers outrun consumers for long, a queue grows forever: utilisation ρ = λ × S ÷ c must stay below 1.
- Backpressure slows work, load shedding drops work, and rate limiting is a cap imposed from outside.
- A WhatsApp number's throughput — 80 messages per second by default — is a hard downstream limit your send pipeline must respect.
- Bound every queue and buffer, and decide what happens when it is full: block, reject with 429 or 503, or shed low-priority work.
Why it matters
Why does backpressure matter?
Many messaging outages that start with "the queue filled up" are missing backpressure. A campaign importer that reads a two-million-row CSV as fast as the disk allows, or a webhook handler that accepts events faster than the CRM sync can write them, turns a temporary mismatch into an out-of-memory crash or a backlog that delays OTPs behind marketing. Propagating "slow down" back to the source keeps memory flat, latency predictable and failures local.
Mechanics
How does backpressure work?
step 1 — Bound every buffer
Give each queue, channel and in-memory array a maximum size or byte budget. An unbounded buffer is not a buffer; it is a deferred crash.
step 2 — Let consumers signal capacity
Consumers pull work when they are ready — prefetch counts,
highWaterMarkon streams, credit-based flow — instead of having work pushed at them regardless of load.step 3 — Propagate the signal upstream
When the send queue is full, the campaign scheduler pauses enqueueing; when your own API is saturated, it answers 429 or 503 with
Retry-Afterso callers back off.step 4 — Pace to the hardest limit
Run sends below the number's throughput — say 70 of 80 messages per second — with the token bucket algorithm, so the Cloud API is never the component applying backpressure through 130429 errors.
step 5 — Watch lag and saturation
Track Consumer Lag — queue depth and the age of the oldest message — and worker utilisation. A sustained rise means producers outrun capacity: add workers if the bottleneck can scale, slow the source if it cannot.
Usage
Backpressure in code
// Stream a contacts CSV into the send queue without loading it all into memory.import { createReadStream } from "node:fs";import { Writable } from "node:stream";import { pipeline } from "node:stream/promises";import { parse } from "csv-parse";const HIGH_WATER = 20_000; // pause the import above this many waiting jobsconst LOW_WATER = 10_000; // resume once the workers drain below thisconst sleep = (ms) => new Promise((r) => setTimeout(r, ms));const toQueue = new Writable({objectMode: true,highWaterMark: 500, // rows buffered before the parser is pausedasync write(row, _enc, done) {try {if ((await sendQueue.count()) > HIGH_WATER) {while ((await sendQueue.count()) > LOW_WATER) await sleep(1_000);}await sendQueue.add("template-send", { to: row.phone, params: [row.name, row.result] });done(); // calling done() late is the backpressure signal} catch (err) {done(err);}},});await pipeline(createReadStream("results.csv"), parse({ columns: true }), toQueue);
The parser stops reading the file whenever the writer has not called done(), so a full queue pauses the whole pipeline back to the disk.
Properties
Backpressure: key facts and limits
- Stability condition
- ρ = λ × S ÷ c < 1
- Default Cloud API throughput
- 80 messages per second per number
- Upgraded throughput
- 1,000 messages per second (automatic, if eligible)
- Over-limit error
- 130429 (rate limit hit)
- Common signals
- Bounded queue full, stream pause, HTTP 429/503 with Retry-After
- Node.js streams
- write() returns false above highWaterMark; wait for 'drain'
Example
Example: Result day at a Lucknow coaching institute
A coaching institute in Lucknow sends exam results to 300,000 students with a utility template. Its old importer read the whole CSV into memory and enqueued every row in seconds, while the send workers, capped by the number's 80 messages per second, needed over an hour — so the queue ballooned until the Redis node ran out of memory mid-run. The rebuilt importer streams the file and pauses whenever more than 20,000 jobs are waiting, resuming below 10,000. Memory stays flat, the run finishes in about 70 minutes at a steady 72 sends per second, and OTPs on their own queue are never delayed.
Caveats
Common mistakes with backpressure
Unbounded in-memory buffers
Arrays, promise lists and queues with no limit hide the problem until memory runs out and every in-flight job is lost. Bound them and decide in advance what happens when they are full.
Scaling workers past the real bottleneck
If the limit is the number's throughput or a CRM's write quota, more workers only produce more 130429s and 429s. Find the true constraint and pace to it.
Ignoring the signal in your own code
In Node.js, ignoring
stream.write()returning false, or looping overqueue.add()without awaiting, are the classic leaks. Respectdrain, await each insert, or batch with a limit.Pushing back on the wrong traffic
With one shared queue, a marketing import's backpressure also delays OTPs. Give each priority its own queue, following the bulkhead pattern, so a broadcast never slows authentication messages.
Notes
Best practices for backpressure
- Alert on the age of the oldest queued job, not only on depth — age is what customers feel.
- Use high and low watermarks (pause at 20,000, resume at 10,000) so producers do not flap on and off.
- Send
Retry-Afterwith every 429 or 503 so well-behaved clients back off by the right amount. - Load-test with the producer deliberately faster than the consumer, and confirm memory stays flat.
Questions
Backpressure FAQs
- What is the difference between backpressure and rate limiting?
- Backpressure is a dynamic signal from a slow consumer; rate limiting is a fixed cap. A rate limiter enforces, say, 70 sends a second whatever happens, while backpressure slows producers whenever any downstream stage falls behind, by as much as needed.
- Is backpressure the same as load shedding?
- No. Backpressure slows producers so all work is eventually done; load shedding deliberately drops or defers some work to protect the rest. Apply backpressure first, and shed only low-priority traffic when the backlog still grows.
- How does backpressure apply to the WhatsApp Cloud API?
- The phone number's throughput is the downstream limit. Pace sends below 80 messages per second (or 1,000 after an upgrade) and let a full send queue pause your campaign scheduler, instead of discovering the limit through 130429 errors.
- Can I apply backpressure to Meta's webhooks?
- Only indirectly, because Meta decides when webhooks are sent. Acknowledge each one quickly into a durable queue; if you return errors or time out, Meta retries for up to seven days — backpressure in its crudest, slowest form.