What is backpressure — and why your script ate all the memory
Look at this experiment I ran. There's a receiver that writes data slowly: ten milliseconds per chunk. I sent it 200,000 one-kilobyte chunks. In a loop, back to back, without waiting.
The loop finished instantly. No errors. And the process memory was holding 205 megabytes — everything the receiver hadn't digested yet.
There's no bug in any single line. I just ignored one word that came back to me two hundred thousand times in a row. The word is false. And it means “slow down.”
What is backpressure, in plain words
Backpressure is a signal from whoever receives data to whoever sends it: “I can't keep up, wait.”
Picture a conveyor belt in a candy factory. Candies ride toward a packer. If the belt is faster than the packer, candies pile up. First on the table, then on the floor. There are three ways out:
- Store — get a bigger box. Works until the box runs out.
- Drop — throw the extra candies away. Sometimes that's an honest option.
- Slow the belt — the packer pulls a lever and the belt slows down.
Backpressure is the third way. The slow part sets the speed of the fast part. Not the other way round.
How it works in code
Data in a program often moves as a stream: a file is read in chunks, a server response arrives in parts, database rows come in batches. Every stream has a source and a receiver. And the receiver has a buffer — that same candy box.
In Node.js the box is 64 KB by default (since Node 22; it used to be 16 KB). When you write to a stream, write() answers:
true— “keep going, there's room”;false— “buffer's full, wait for thedrainevent.”
Here's the key part: false is not an error. No data is lost. Node obediently accepts the next chunk, and the next. It just keeps them in memory. Nobody stops you — except memory itself.
// this leaks: write() says "stop", the loop doesn't listen
for (const row of rows) {
out.write(row)
}
// this is right: on false, wait until the buffer drains
import { once } from "node:events"
for (const row of rows) {
if (!out.write(row)) await once(out, "drain")
}
Even simpler — don't write that loop at all. pipeline() from node:stream/promises connects a source to a receiver and pauses the source whenever the receiver falls behind:
import { pipeline } from "node:stream/promises"
import { createReadStream, createWriteStream } from "node:fs"
await pipeline(createReadStream("big.csv"), createWriteStream("copy.csv"))
A five-gigabyte file copies this way with almost flat memory. The source reads exactly as much as the receiver is ready to take.
Why it matters to you
Without backpressure a program behaves treacherously. On small data everything flies. You tested on a 10 MB file — great. Then production gets a 3 GB file and the process dies with JavaScript heap out of memory. Or the server just slows to a crawl because memory ran out and the system started swapping.
It looks like a memory leak, but the cause is different. A leak is memory nobody frees. Here the memory does get freed. Just later than you grab it.
And it's a common pattern in AI-written code. The model writes “read everything, process everything, write everything” — because on a short example that's the clearest code. Ask directly: “process the file as a stream, with pipeline, respecting backpressure.” One sentence — and the code changes.
Where you'll meet backpressure
- Big files. CSV imports, video, logs. Read as a stream, not all at once.
- Databases. A million rows through a cursor, while writing to another database is slower. No pausing — a mountain of rows in memory.
- WebSocket. A browser socket has
bufferedAmount— how many bytes haven't been sent yet. If the client is on bad internet and you push game frames 60 times a second, that number grows. Check it and skip frames. - API requests. Here backpressure comes from outside: the server answers 429 “too many.” Same “slow down,” just over the network. A semaphore helps — it won't let more than N requests run at once.
How is backpressure different from a queue?
A queue is the “bigger box”: it stores. Backpressure decides what to do when the box is full. A good queue has a limit too, and at some point tells the sender “no.”
What if I just make the buffer bigger?
You'll postpone the crash, not remove it. If the source is steadily faster than the receiver, any buffer fills up eventually. A buffer smooths short bursts, not a constant speed gap.
Does the browser's fetch have backpressure?
Yes. A fetch response body is a ReadableStream. While you read it chunk by chunk with getReader(), the browser doesn't pull data faster than you take it. That's why big responses are better read as a stream than with await res.text().
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.


