Why Async Generators Were the Missing Piece in My Node.js API Scaling Puzzle
Streaming Responses, Backpressure Handling, and Async Iteration Made Easy
I scaled my Node.js API to handle 1GB+ payloads without crashing — by streaming data with async generators. Here’s how they fixed my speed and memory issues.
I didn’t know async generators could solve both memory and speed problems — until I hit a 1GB payload limit.
The Turning Point: When JSON.stringify() Betrayed Me
It was 2 AM. I was testing an endpoint that streamed a giant dataset from our database — a list of over a million user records for a data export feature. Everything seemed fine during development. But once I hit production with real data, the server crashed.
Memory spikes. Timeouts. An unresponsive API. That’s when I realized my Node.js API wasn’t just slow — it was broken under load.
I needed a way to stream massive datasets efficiently, handle backpressure gracefully, and avoid memory blowouts. The solution wasn’t a new database or fancy caching strategy.
It was async generators — a little-known but insanely powerful JavaScript feature that turned my performance bottleneck into a scalable, stream-friendly architecture.
What Are Async Generators, Really?
Most JavaScript developers know about async/await. But async generators combine async with the power of iterators, allowing you to yield promises one by one—ideal for streaming.
Here’s the TL;DR:
async function* streamUsers() {
for (let userId of userIds) {
yield await fetchUserFromDB(userId); // each yield is async
}
}Instead of waiting for the entire dataset to load, async generators yield data incrementally, making them perfect for long-running, data-intensive operations.
Traditional Async Functions vs Async Generators: The Key Difference
Most of us write endpoints like this:
app.get('/users', async (req, res) => {
const users = await db.getAllUsers(); // Loads everything in memory
res.json(users);
});It’s fine for small datasets, but when getAllUsers() returns millions of rows, you’re stuck.
Async generators change the game:
app.get('/users', async (req, res) => {
res.setHeader('Content-Type', 'application/json');
const encoder = new TextEncoder();
res.write('[');
let first = true;
for await (const user of streamUsersFromDB()) {
if (!first) res.write(',');
res.write(JSON.stringify(user));
first = false;
}
res.write(']');
res.end();
});No massive memory footprint. No buffering. Just incremental, controlled streaming, tailored for large-scale APIs.
Streaming Responses at Scale: My Real-World Use Case
In our reporting API, users often export data going back 3–5 years. Even with pagination and filtering, large enterprises could easily cross 1GB of raw payload.
Rather than pushing everything into a single giant array, I used:
- PostgreSQL cursors to stream rows one at a time
- A custom async generator to wrap that stream
- An Express endpoint that piped results to the response using JSON streaming
The result? 🚀 A 70% reduction in memory usage, and endpoints that never crash — even under sustained load.
Integrating Async Generators with Express and Fastify
While Express and Fastify don’t support async iterators natively in the response, it’s easy to implement streaming manually.
Get Bhagya Rana’s stories in your inbox
Join Medium for free to get updates from this writer.
For Express, the key is controlling when and how you write to the response object:
app.get('/export', async (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.write('[');
let first = true;
for await (const row of getLargeDataset()) {
if (!first) res.write(',');
res.write(JSON.stringify(row));
first = false;
// Handle backpressure
if (!res.write('')) {
await new Promise(resolve => res.once('drain', resolve));
}
}
res.write(']');
res.end();
});Fastify supports onSend hooks and streams similarly, with more speed optimizations under the hood.
🔗 Optional internal link: If you want a full Fastify + async generator implementation, check out the Fastify Docs on Streams.
Handling Backpressure Gracefully
This was the part I underestimated.
When you push too much data too quickly to an HTTP response, Node.js applies backpressure. If your code doesn’t pause until the client catches up, you risk memory leaks or dropped connections.
Async generators + Node.js streams make it easy:
if (!res.write(chunk)) {
await new Promise(resolve => res.once('drain', resolve));
}This tiny block of code made my API resilient to slow clients, poor connections, and spikes in network latency. You don’t need a queue system or Redis. You just need to pause when necessary and resume naturally.
The Unexpected Benefit: Better DevX
Async generators aren’t just performant — they’re elegant.
Instead of juggling buffer arrays, retry logic, or Promise.all spaghetti, you write clean, linear code:
for await (const row of streamData()) {
// Process each item
}It’s debuggable. Testable. And aligns beautifully with the way Node.js handles I/O under the hood.
Plus, this pattern is future-proof. It works with:
- ReadableStreams in the Web API
- Database client libraries that expose async iterables (like MongoDB, Prisma, pg-query-stream)
- File system streams when reading large CSV/JSON/NDJSON files
When Should You Reach for Async Generators?
Here’s a quick checklist:
- Are you dealing with large datasets from a DB or file?
- Do you need to stream data to clients over HTTP?
- Is memory usage spiking due to full-buffer reads?
- Do you want to handle backpressure cleanly in your API?
If yes, async generators should be in your toolkit.
Final Thoughts: Scaling Isn’t Just About More Servers
We often think of scalability as “adding more instances” or “optimizing queries.” But sometimes, it’s about rethinking how data flows through your app.
Async generators in Node.js are that rethink.
They helped me stream 1GB+ payloads without breaking a sweat. They brought down latency, memory usage, and even reduced infra costs by keeping APIs lean.
If you’re building a Node.js API that handles large responses — especially data exports, logs, or reporting endpoints — async generators might be your missing piece too.
👋 Found this helpful?
Clap 👏, leave a comment if you’ve used async generators in your stack, and share with a fellow backend dev struggling with scaling APIs!
Want me to do a deep dive on PostgreSQL cursors + async generators or building a reusable JSON stream utility? Let me know below.









