Event Loop Best Practices
Rules for keeping the Node.js event loop responsive under load - offload CPU, batch I/O, and measure lag before users feel it.
How to Use This List
- Review before shipping endpoints that parse, transform, or encrypt payloads.
- Pair with load tests that sustain concurrent requests, not single-shot curls.
- Treat any unchecked CPU-on-main-thread item as a production incident waiting to happen.
- Revisit after dependency upgrades that add sync work in middleware.
A - Keep the Main Thread Free
- Never run CPU-heavy loops, crypto, or compression on the request path without workers. Move to
worker_threadsor a queue. - Avoid
*SyncAPIs (readFileSync,bcrypt.hashSync) in HTTP handlers. Use promise or callback async variants. - Cap
JSON.parseinput size at the edge and application. Large bodies block parsing synchronously. - Do not log huge objects synchronously. Serialize summaries or sample fields only.
- Profile new middleware with loop delay metrics before merge. One slow middleware hurts every route.
B - Async Patterns
- Use
Promise.allfor independent I/O, not sequentialawaitin loops. Add concurrency limits for large batches. - Prefer
queueMicrotaskorsetImmediateoversetTimeout(fn, 0)for deferral. Match deferral type to ordering needs. - Never recurse
process.nextTickto batch work. It starves I/O and timers. - Always
try/catchawaited code in request handlers. Unhandled rejections take down the process. - Use
for awaiton streams instead of loading entire payloads into memory. Streams cooperate with backpressure.
C - Timers and Scheduling
- Treat
setTimeout/setIntervalas approximate, not real-time clocks. Use external schedulers for wall-clock cron. - Prevent overlapping
setIntervalcallbacks. ChainsetTimeoutafter work completes. - Clear timers on shutdown (
SIGTERM). Or call.unref()only when intentionally fire-and-forget. - Add jitter to retry backoff timers. Avoid synchronized retries across instances.
- Limit concurrent timers in memory. Thousands of handles add timer phase overhead.
D - Observability
- Export p99 event loop delay from
monitorEventLoopDelay. Alert before user-facing p99 doubles. - Track
eventLoopUtilizationalongside CPU. High ELU with moderate CPU signals sync blocks. - Use
blocked-atin staging to attribute blocks to stack traces. Not always-on in production. - Load test with realistic concurrency, not single requests. Blocks hide until traffic overlaps.
- Document known sync sections in ADRs. Future contributors inherit the guardrails.
E - Architecture
- Push CPU-bound jobs to BullMQ/workers. HTTP layer stays I/O-bound and fast.
- Use connection pooling for DB and HTTP clients. Avoid per-request TCP + TLS setup blocking patterns.
- Prefer streaming responses for large downloads. Chunked transfer keeps memory and loop time stable.
- Scale replicas before optimizing microtasks. Horizontal scale masks I/O; CPU blocks need workers.
- Re-read libuv Phases when debugging ordering. Guessing queue behavior wastes hours.
FAQs
What is the single most important rule?
No long synchronous JavaScript on the main thread during request handling. Everything else follows from that.
Is async automatically non-blocking?
No. async only yields at await points. Sync code between awaits still blocks.
How much JSON is too large to parse sync?
Depends on hardware - profile. Rule of thumb: cap bodies at 1-10 MB for typical APIs; stream beyond that.
Should I use cluster or workers for CPU?
cluster duplicates HTTP listeners across cores. worker_threads shares one server with CPU pools. Often use both patterns in different layers.
Does NestJS hide loop issues?
No - guards, pipes, and interceptors still run on the main thread unless you offload explicitly.
Are native addons safe?
Only if they offload to libuv thread pool or return async. Sync native calls block like JavaScript loops.
What about zlib gzip sync?
zlib.gzipSync blocks - use promisify(gzip) or zlib/promises in request paths.
Can I trust Express 5 async errors?
Improved, but explicit error handling and loop hygiene remain your responsibility.
How do I review PRs for loop risk?
Search for Sync, tight loops, JSON.parse(req.body), and new middleware without benchmarks.
Does Fastify schema validation block?
JSON Schema compilation is usually at startup; per-request validation is sync but fast - still profile large payloads.
When is setImmediate appropriate?
Deferring work until after the current I/O poll phase - batching multiple socket reads before aggregation.
Where do I learn more after this list?
Detecting Event-Loop Blockage for metrics and worker_threads for fixes.
Related
- Detecting Event-Loop Blockage - metrics and tooling
- How Node.js Works - V8 + libuv model
- worker_threads - offload CPU work
- Streams Best Practices - backpressure and I/O
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.