DEV Community

Alex Aslam
Alex Aslam

Posted on

Mastering the Node.js Event Loop: How We Scaled to Higher RPS

The Breaking Point

Our real-time analytics dashboard was buckling under pressure. At 10,000 requests per second (RPS), API response times spiked from 20ms to over 1 second. The server wasn’t CPU-bound—it was event-loop congestion.

Node.js was starving under its own single-threaded nature. Here’s how we fixed it.


Understanding the Bottleneck

The event loop is Node.js’s heartbeat. When blocked, everything slows down. Common culprits:

  1. Sync I/O (e.g., fs.readFileSync)
  2. CPU-heavy tasks (e.g., JSON parsing, large computations)
  3. Uncontrolled microtasks (e.g., runaway Promise.resolve() chains)

Optimization #1: Liberating the Event Loop

Problem:

A legacy synchronous config loader:

const config = JSON.parse(fs.readFileSync('config.json')); // Blocking!
Enter fullscreen mode Exit fullscreen mode

Fix:

  • Switched to async fs.promises:
  const config = await fs.promises.readFile('config.json', 'utf-8');
Enter fullscreen mode Exit fullscreen mode
  • Cached results to avoid repeated I/O.

Impact: Reduced event-loop lag by 35%.


Optimization #2: Taming Promises

Problem:

A flood of unbatched Promise.all calls:

await Promise.all(users.map(user => sendEmail(user))); // 10K emails at once!
Enter fullscreen mode Exit fullscreen mode

Fix:

  • Limited concurrency with p-limit:
  const limit = require('p-limit');
  const batch = limit(100); // Max 100 concurrent emails
  await Promise.all(users.map(user => batch(() => sendEmail(user))));
Enter fullscreen mode Exit fullscreen mode

Impact: Cut event-loop delay from 200ms to <10ms.


Optimization #3: Offloading Heavy Work

Problem:

A CPU-intensive report generator:

app.get('/report', () => generatePDFReport()); // 2-second block!
Enter fullscreen mode Exit fullscreen mode

Fix:

  • Moved to Worker Threads:
  const { Worker } = require('worker_threads');
  app.get('/report', async (req, res) => {
    const worker = new Worker('./pdf-worker.js');
    worker.postMessage(req.query);
    worker.on('message', (pdf) => res.send(pdf));
  });
Enter fullscreen mode Exit fullscreen mode

Impact: Zero event-loop disruption during PDF generation.


Key Takeaways

Avoid sync operations—they’re event-loop poison.
Batch async tasks—uncontrolled Promises are silent killers.
Offload CPU work—Worker Threads are your ally.

Our dashboard now handles 50K RPS with consistent <50ms latency.

What’s your event-loop war story? Let’s discuss below.

Top comments (6)

Collapse
 
nevodavid profile image
Nevo David

insane seeing node stretched to this level tbh - you ever hit a point where throwing more tweaks just stopped helping?

Collapse
 
alex_aslam profile image
Alex Aslam

Absolutely hit that wall. There comes a point where no amount of Node.js tweaks help—that’s when we had to rethink architecture. We ended up splitting into microservices and using edge caching.

What was your breaking point? Always curious how others handle the 'now what?' moment.

Collapse
 
ciphernutz profile image
Ciphernutz

Great read! Loved the practical insights on scaling Node.js and mastering the event loop. Thanks for sharing

Collapse
 
alex_aslam profile image
Alex Aslam

Thanks so much! 🙌 Thrilled you found it useful. The event loop is one of those things that seems simple... until it isn't.
What's been your biggest 'aha' moment with Node.js performance? (Mine was realizing how much damage one blocking fs.readFileSync could do!) ⚡️

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Pretty cool watching this come together step by step honestly, sometimes you just have to keep digging till stuff clicks

Collapse
 
alex_aslam profile image
Alex Aslam

100% this! 🔥 The 'aha' moments only come after staring at metrics until your eyes cross. Our biggest breakthroughs happened when we almost gave up—then tried one last tweak.
What was your most stubborn Node.js performance puzzle? (Mine was a Promise.all() that looked innocent… but wasn’t.)