Node.js Event Loop — The Sync Crypto Gotcha
When a single sync crypto call pushed event loop lag beyond 2000ms, the API died.
- Node.js runs JavaScript outside the browser using Chrome's V8 engine
- Single-threaded event loop with non-blocking I/O — handles thousands of concurrent connections without thread-per-request overhead
- Libuv manages async I/O (files, network, DNS) via an OS-level thread pool
- npm is the default package manager — over 2 million packages, but dependency bloat is a real production risk
- Biggest mistake: blocking the event loop with CPU-heavy work - use worker_threads or offload to a dedicated service
How Node.js Handles Concurrency
Traditional servers, like Apache, create one thread per connection. This consumes significant memory as the number of users grows. Node.js takes a different approach: it uses a single main thread and an Event Loop. When an I/O operation (like a database query or file read) is initiated, Node hands the task off to the system kernel or a background thread pool (Libuv). The main thread remains free to handle new incoming requests immediately.
This 'non-blocking' nature is why a single Node.js instance can out-perform traditional multi-threaded servers in I/O-bound scenarios.
Promise.all() to parallelise — don't serialiseBuilding a Production-Ready HTTP Server
While frameworks like Express are the industry standard, understanding the native http module is essential for grasping how Node.js communicates with the outside world. Every request is a stream, and every response is a stream.
Modules — CommonJS and ES Modules
Node.js originally popularized CommonJS (require), but the industry has moved toward the official JavaScript standard: ES Modules (import/export). Choosing the right one impacts how your code is bundled and optimized.
import() for specific ESM packagesnpm and Dependency Management
npm is Node's default package manager. It installs dependencies into node_modules and tracks them in package.json. The package-lock.json locks exact versions to ensure reproducible builds across environments.
package-lock.json, different environments may get different versions of transitive dependencies. This causes 'works on my machine' bugs. Always commit the lockfile.node_modules folder can exceed 300MB for a simple app — don't commit it.npm ci in CI/CD for deterministic installs from lockfile.npm update blindly in production; review breaking changes first.npm ci in CI, npm install locally.npm audit.The Event Loop Deep Dive — Phases and Timers
The Event Loop is the core of Node.js concurrency. It runs in phases: timers, pending callbacks, idle/prepare, poll, check, close. Understanding this order is essential for debugging async behaviour and unexpected delays.
The CPU-Bound Route That Killed Our API
crypto.pbkdf2Sync() instead of the async crypto.pbkdf2(). The synchronous version blocks the event loop for the duration of the hash computation....Sync calls with the async equivalents. Added a worker_threads pool for any remaining CPU-heavy operations. Implemented event loop lag monitoring with process.hrtime() and alerts if lag > 50ms.- Never use synchronous crypto or filesystem methods in a request handler.
- Monitor event loop lag in production — it's the canary for blocking code.
- Code reviews must flag
Syncfunctions in request paths.
clinic doctor or manually log process.hrtime() delta in a setInterval. Look for Sync functions or CPU-heavy loops.node --inspect and compare snapshots. Check for closures in Promises, unclosed connections, or large retained objects.node_modules contains the package. Run npm ls <package> to check dependency tree. If missing, ensure npm ci ran correctly and lockfile is up to date.res.end() in response handlers. Use --inspect to list open handles. Add request timeout middleware (e.g., connect-timeout).Key takeaways
npm ci for deterministic builds.Common mistakes to avoid
4 patternsBlocking the event loop with sync I/O
readFileSync, writeFileSync, pbkdf2Sync, etc., with their async counterparts.Not handling promise rejections
.catch() to every promise chain, or use a global process.on('unhandledRejection') handler.Using `process.nextTick()` for deferred work
setImmediate() to defer work to the next iteration instead of nextTick.Forgetting to commit `package-lock.json`
package-lock.json to version control. Use npm ci in CI/CD.Interview Questions on This Topic
Explain the phases of the Node.js Event Loop. Where does process.nextTick() fit into these phases?
nextTick queue and then the microtask queue (Promises). process.nextTick() runs after the current operation completes, before moving to the next phase. This makes it higher priority than setImmediate() which runs in the check phase.Frequently Asked Questions
That's Node.js. Mark it forged?
3 min read · try the examples if you haven't