There’s a bug that silently destroys Node.js API performance under load — no error messages, no crashes, no obvious symptoms. Your response times just slowly climb from 20ms to 800ms as traffic increases, and every profiler you run shows nothing obviously wrong. The culprit is almost always the same thing: blocking the event loop. This guide focuses on Node.js event loop blocking,
⚡ TL;DR: Node.js is single-threaded. Any synchronous operation that takes more than ~10ms blocks every other request waiting to be processed. This isn’t a bug — it’s the architecture. Here’s how to find it, measure it, and fix it permanently.
How the Event Loop Actually Works (The Part Tutorials Skip)
The Node.js event loop processes one callback at a time. When a callback is running, nothing else can run. No other requests get processed, no timers fire, no I/O completes. Everything waits.
// This is what "blocking the event loop" means in practice
const express = require('express');
const app = express();
app.get('/fast', (req, res) => {
res.json({ status: 'ok' }); // Returns in < 1ms
});
app.get('/blocking', (req, res) => {
// This runs synchronously — blocks ALL other requests for 2 seconds
const start = Date.now();
while (Date.now() - start < 2000) {} // Busy loop
res.json({ status: 'done' });
});
// If one request hits /blocking:
// ALL concurrent requests to /fast also wait 2 seconds
// Even though /fast takes < 1ms
// This is the event loop blocking problemThe Real Culprits (Not the Obvious Ones)
Nobody writes deliberate busy-loops in production. The actual blocking operations are subtle:
1. JSON.parse() / JSON.stringify() on Large Payloads
// Parsing a 5MB JSON response — blocks for ~50ms
app.post('/process', (req, res) => {
const data = JSON.parse(largeJsonString); // BLOCKING — 5MB = ~50ms
res.json(transform(data));
});
// Fix: Use streaming JSON parser for large payloads
const { parser } = require('stream-json');
const { streamArray } = require('stream-json/streamers/StreamArray');
app.post('/process', (req, res) => {
const pipeline = req.pipe(parser()).pipe(streamArray());
pipeline.on('data', ({ value }) => processItem(value));
pipeline.on('end', () => res.json({ done: true }));
});2. Synchronous File System Operations
// ❌ Every Sync fs call blocks ALL requests
const config = fs.readFileSync('./config.json'); // BLOCKING
const exists = fs.existsSync('./file.txt'); // BLOCKING
fs.writeFileSync('./log.txt', data); // BLOCKING
// ✅ Use async versions — they yield control back to the event loop
const config = await fs.promises.readFile('./config.json');
const exists = await fs.promises.access('./file.txt').then(() => true).catch(() => false);
await fs.promises.writeFile('./log.txt', data);3. Synchronous Crypto Operations
// ❌ bcrypt with high rounds blocks for 200-500ms
app.post('/login', async (req, res) => {
const valid = bcrypt.compareSync(req.body.password, hash); // BLOCKING: ~300ms
res.json({ valid });
});
// ✅ Use async version + run in worker thread for CPU-heavy crypto
const { Worker } = require('worker_threads');
// Or use argon2 which has better async support
const argon2 = require('argon2');
app.post('/login', async (req, res) => {
const valid = await argon2.verify(hash, req.body.password); // Non-blocking
res.json({ valid });
});4. Heavy Computation in Request Handlers
// ❌ Image processing, data transformation, sorting large arrays
app.get('/report', (req, res) => {
const sorted = hugeArray.sort((a, b) => complexComparison(a, b)); // BLOCKING
const processed = sorted.map(expensiveTransform); // BLOCKING
res.json(processed);
});
// ✅ Offload CPU work to worker threads
const { Worker } = require('worker_threads');
function runInWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js', { workerData: data });
worker.on('message', resolve);
worker.on('error', reject);
});
}
app.get('/report', async (req, res) => {
const result = await runInWorker(hugeArray); // Main thread stays free
res.json(result);
});How to Detect Event Loop Blocking in Production
// Method 1: Measure event loop lag with a simple timer
let lastCheck = Date.now();
setInterval(() => {
const now = Date.now();
const lag = now - lastCheck - 1000; // Should fire every 1000ms exactly
if (lag > 50) {
console.warn(`Event loop lag: ${lag}ms — something is blocking`);
}
lastCheck = now;
}, 1000);
// Method 2: Use clinic.js (best free tool for Node.js profiling)
// npm install -g clinic
// clinic doctor -- node server.js
// Generates a visual flame chart of blocking operations
// Method 3: Built-in perf_hooks
const { monitorEventLoopDelay } = require('perf_hooks');
const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();
setInterval(() => {
console.log('Event loop p99 delay:', h.percentile(99) / 1e6, 'ms');
h.reset();
}, 5000);The Worker Thread Pattern for CPU-Bound Work
// worker-pool.js — Reusable worker thread pool
const { Worker } = require('worker_threads');
const os = require('os');
class WorkerPool {
constructor(workerScript, size = os.cpus().length) {
this.workers = Array.from({ length: size }, () => ({
worker: new Worker(workerScript),
busy: false
}));
this.queue = [];
}
run(data) {
return new Promise((resolve, reject) => {
const available = this.workers.find(w => !w.busy);
if (available) {
this._runOnWorker(available, data, resolve, reject);
} else {
this.queue.push({ data, resolve, reject });
}
});
}
_runOnWorker(slot, data, resolve, reject) {
slot.busy = true;
slot.worker.postMessage(data);
slot.worker.once('message', (result) => {
slot.busy = false;
resolve(result);
if (this.queue.length > 0) {
const next = this.queue.shift();
this._runOnWorker(slot, next.data, next.resolve, next.reject);
}
});
slot.worker.once('error', reject);
}
}
// Usage
const pool = new WorkerPool('./compute-worker.js');
app.get('/compute', async (req, res) => {
const result = await pool.run(req.body);
res.json(result);
});Quick Wins Checklist
- ✅ Replace all
fs.*Sync()calls withfs.promises.* - ✅ Stream large JSON instead of
JSON.parse(bigString) - ✅ Move bcrypt/argon2 to worker threads
- ✅ Run
clinic doctorto visualize blocking operations - ✅ Add event loop lag monitoring to your metrics
- ✅ Use
setImmediate()to yield between chunks of CPU work - ❌ Never use
*Syncfunctions in request handlers - ❌ Never sort/process arrays with 10,000+ items synchronously in a handler
For teams running Node.js at scale, DigitalOcean's App Platform automatically scales Node.js workers horizontally — so even if one worker blocks, others stay responsive.
Fixed your event loop? Next: the Node.js stream backpressure bug that causes memory to grow unbounded under load — silently, until your server OOMs at 3am.
Related reads on CheatCoders: The event loop concepts here connect directly to V8's JIT optimization — both are about keeping your code on the fast path. For Java developers, Java virtual threads solve the same blocking I/O problem with a different architectural approach. Go deeper with async/await under the hood and Node.js clustering and worker threads for CPU-bound work off the event loop. External resource: Official Node.js event loop documentation.
Disclosure: This post contains affiliate links. If you purchase through these links, CheatCoders earns a small commission at no extra cost to you. We only recommend tools and books we genuinely find valuable.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
You just read something most developers never learn. Get more secrets like this delivered every week — JavaScript internals, Python optimizations, AWS architectures, system design, and AI workflows.
Related: Node.js Event Loop Lag: Catch p99 Stalls Before Users Feel Them · Node.js Security Best Practices: Protect Your API From the Most Common Attacks · API Debugging in 30 Seconds: The Fastest Ways to Fix API Issues
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.

Pingback: Java Virtual Threads vs Traditional Threads: What Nobody Tells You - CheatCoders
Pingback: The V8 JIT Trick That Makes Your JS 3x Faster (Without Changing Logic) - CheatCoders
Pingback: Hidden AWS Lambda Cold Start Fix — Zero Cost, Zero Latency - CheatCoders
Pingback: Node.js Cluster Module: Scale Your API to All CPU Cores Without Kubernetes - CheatCoders