How to Apply Upload Backpressure
Apply backpressure to uploads by pausing the request stream when the sink is full, so a slow disk does not cause memory blowup on the server.
On this page
Introduction
The hardest part of handle slow sink is not writing the code — it is deciding what the code is allowed to reject. A backpressure setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in backpressure, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.
A backpressure guard for handle slow sink should be proportional to the risk. A ten-field form does not need a hundred-line schema; it needs the five checks that catch the five mistakes the users actually make. Ship the five, measure the rejections, and add the sixth only when the traffic proves it exists. Speculative generality is the bug you write on purpose, and backpressure makes it easy to write, which is why discipline matters.
The sections below walk through the concrete decisions: where to put the backpressure check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.
Pipe with pause
Pipe with pause is the first decision because it determines what the rest of the backpressure pipeline can assume. A wrong choice here turns into handle slow sink under load, and the invariant you thought backpressure guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Pipe with pause is the one most teams skip: drive backpressure to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
const ws = fs.createWriteStream(path, { highWaterMark: 1024 * 1024 });
req.on('data', chunk => {
if (!ws.write(chunk)) req.pause();
});
ws.on('drain', () => req.resume());
The javascript above is the shape to ship for handle slow sink. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.
Use highWaterMark
The mechanism behind Use highWaterMark is straightforward in backpressure, but the consequences ripple. A wrong choice here turns into handle slow sink under load, and the invariant you thought backpressure guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Use highWaterMark is the one most teams skip: drive backpressure to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Resume on drain
Resume on drain is where most backpressure implementations leak performance. A wrong choice here turns into handle slow sink under load, and the invariant you thought backpressure guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Resume on drain is the one most teams skip: drive backpressure to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
test('pauses on full buffer', () => {
ws.write = () => false;
req.emit('data', Buffer.alloc(1));
expect(req.isPaused()).toBe(true);
});
Pin the behavior with this test. A backpressure setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.
Measure buffer depth
The hard part of Measure buffer depth is not the happy path — it is what happens when backpressure fails halfway. A wrong choice here turns into handle slow sink under load, and the invariant you thought backpressure guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Measure buffer depth is the one most teams skip: drive backpressure to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Reject on sustained pressure
Reject on sustained pressure should be observable without a debugger. A wrong choice here turns into handle slow sink under load, and the invariant you thought backpressure guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Reject on sustained pressure is the one most teams skip: drive backpressure to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Test backpressure
Test backpressure is a trade, not a winner. A wrong choice here turns into handle slow sink under load, and the invariant you thought backpressure guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.
The test for Test backpressure is the one most teams skip: drive backpressure to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.
Common pitfalls
The common pitfalls in handle slow sink all reduce to one mistake: treating backpressure as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.
- Skipping the backpressure boundary so handle slow sink enters unvalidated.
- Assuming backpressure is fast without measuring the tail latency under load.
- Letting handle slow sink fail silently instead of failing loudly with a metric.
- Testing only the happy path so the backpressure failure surfaces in production.
- Sharing mutable state across requests so one bad input poisons the next.
Conclusion
Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: backpressure fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.
Self-check: write a test that drives backpressure to the failing condition and assert the system degrades gracefully. Then handle slow sink is a property you maintain, not a feature you hope for.
Sources: Node streams backpressure, Stream API
FAQ
- Why not just pipe?
- pipe handles backpressure automatically. Use the manual approach when you need to inspect or transform chunks.
- What highWaterMark?
- Match the chunk size. 1MB is a good default for file streams.
Report a problem with this articleOpens a pre-filled issue on GitHub.