Home JavaScript Node.js Event Loop — The Sync Crypto Gotcha
Intermediate 8 min · March 06, 2026
Introduction to Node.js

Node.js Event Loop — The Sync Crypto Gotcha

When a single sync crypto call pushed event loop lag beyond 2000ms, the API died.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 25 min
  • Solid grasp of fundamentals
  • Comfortable reading code examples
  • Basic production concepts
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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
✦ Definition~90s read
What is Introduction to Node.js?

Node.js is a runtime environment that executes JavaScript outside the browser, built on Chrome's V8 engine and the libuv library. Its core innovation is the event loop — a single-threaded, non-blocking I/O model that lets you handle thousands of concurrent connections without the thread-per-request overhead of traditional servers like Apache or Java's servlet containers.

Imagine a single cashier at a store.

The event loop processes callbacks in phases (timers, I/O, idle, poll, check, close), yielding control to the OS for I/O operations via libuv's thread pool. This architecture makes Node.js ideal for I/O-bound workloads (APIs, proxies, real-time services) but dangerous for CPU-bound tasks or synchronous crypto operations, which block the event loop and destroy throughput.

You should avoid Node.js for heavy computation, image processing, or any workload requiring parallel CPU execution — that's where Go, Rust, or worker threads come in. Production servers typically use Express or Fastify, with CommonJS still dominant in legacy codebases while ES modules gain traction. npm manages dependencies with a flat-ish node_modules structure and lockfiles (package-lock.json) for deterministic installs, though Yarn and pnpm offer alternatives with better caching or disk efficiency.

Plain-English First

Imagine a single cashier at a store. Normally, they handle customers quickly by handing off tasks like bagging to a helper (libuv's thread pool). But if a customer asks the cashier to personally count 10,000 coins (a sync crypto call), the cashier stops serving everyone else until the counting is done. That's what happens when you call crypto.pbkdf2Sync — the entire checkout line freezes.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

A single synchronous crypto call in a Node.js request handler can spike event loop lag past 2000ms, turning a 10,000 req/s API into a 100 req/s disaster. This isn't a framework bug — it's the event loop design. You need to understand how libuv's thread pool and V8's single-threaded execution interact, then instrument monitoring to catch blocking before users do.

What the Node.js Event Loop Actually Is

The Node.js event loop is a single-threaded, non-blocking I/O orchestration engine. It processes JavaScript callbacks in phases: timers, pending callbacks, idle/prepare, poll, check (setImmediate), and close callbacks. Each phase has a FIFO queue of callbacks to execute. The loop iterates until no more work remains.

Crucially, the event loop does not run your JavaScript in parallel — it runs one callback at a time. Any synchronous CPU-bound operation, like a crypto hash with a large input, blocks the entire loop. A single crypto.pbkdf2Sync call can stall the loop for 100+ ms, starving all other requests. This is not a bug; it's the design. The loop yields only when the call stack empties.

Use the event loop for I/O-bound work: file reads, network requests, database queries. For CPU-heavy tasks (hashing, JSON parsing of large payloads, image processing), offload to worker threads or child processes. In production, a single synchronous crypto call in a request handler can drop throughput from 10,000 req/s to under 100 req/s.

⚠ Sync Crypto Is a Silent Killer
Using crypto.pbkdf2Sync or crypto.randomBytes (sync) in a request handler blocks the event loop for the entire duration — no other request gets processed until it finishes.
📊 Production Insight
A team added password hashing with pbkdf2Sync in an Express login route. Under load, response times jumped from 5ms to 800ms, and the process stopped accepting new connections because the event loop was blocked for hundreds of milliseconds per request. Rule: never use synchronous crypto in a hot path; always use the async version or offload to a worker thread.
🎯 Key Takeaway
The event loop is single-threaded — one blocking call stalls everything.
Synchronous crypto (pbkdf2Sync, randomBytes) is a common production footgun.
Offload CPU work to worker threads or use async APIs to keep the loop responsive.
introduction-nodejs Node.js Runtime Architecture Layers How V8, Libuv, and OS interact for concurrency Application Layer User Code | npm Modules Node.js Core APIs Event Emitter | Streams | Crypto V8 JavaScript Engine Memory Heap | Call Stack | Garbage Collector Libuv Library Event Loop | Thread Pool | Async I/O Operating System Kernel | File System | Network Stack THECODEFORGE.IO
thecodeforge.io
Introduction Nodejs

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.

ExampleJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 
 * Package: io.thecodeforge.node.basics
 */
const fs = require('fs');

console.log('1. Initiating non-blocking file read...');

// fs.readFile is asynchronous and non-blocking
fs.readFile('large-report.pdf', (err, data) => {
    if (err) {
        console.error('Error reading file:', err.message);
        return;
    }
    // This runs only when the OS finishes the heavy lifting
    console.log(`3. Success! Processed ${data.length} bytes.`);
});

// This executes while the file is still being read by the OS
console.log('2. Main thread is free! Handling other user requests...');
Output
1. Initiating non-blocking file read...
2. Main thread is free! Handling other user requests...
3. Success! Processed 45210 characters.
Try it live
📊 Production Insight
A single synchronous file read of 500MB blocks the event loop for ~200ms.
During that time, all other requests queue up — latency spikes follow.
Rule: never use fs.readFileSync in a request handler; use the async variant.
🎯 Key Takeaway
Non-blocking I/O is the superpower.
Block the event loop and you lose all concurrency.
Use async APIs for I/O, worker_threads for CPU work.
Choose the I/O pattern
IfOperation involves disk, network, or database
UseUse callbacks, promises, or async/await with non-blocking APIs
IfOperation is CPU-bound (image resizing, crypto, JSON parse of huge data)
UseOffload to worker_threads or a separate microservice
IfNeed to wait on multiple independent I/O operations
UseUse Promise.all() to parallelise — don't serialise

Node.js Architecture — How V8, Libuv, and the OS Work Together

Understanding the layered architecture of Node.js explains why it excels at I/O-bound tasks and why CPU work is problematic. At the top sits your JavaScript code, executed by Google's V8 engine. Below V8, Node.js provides bindings to C++ functionality — these are the bridge between JavaScript and the operating system. The most important component is Libuv, a C library that provides the event loop and the thread pool for operations the OS cannot do asynchronously (like file I/O on Linux). Libuv uses the OS kernel's native async capabilities (epoll on Linux, kqueue on macOS, IOCP on Windows) for network and DNS operations. When a JavaScript function like fs.readFile is called, V8 passes the request through Node.js bindings to Libuv, which either uses the OS kernel directly (if available) or enqueues work on its thread pool. Once the operation completes, Libuv places the callback in the event loop's appropriate phase, and V8 executes the next available microtask or callback.

📊 Production Insight
Knowing the architecture helps you identify where bottlenecks occur: a high pending callbacks phase means many I/O completions queued; high poll time indicates heavy disk or network activity. Use process._getActiveHandles() and process._getActiveRequests() on a running process to see what's keeping the loop busy.
🎯 Key Takeaway
V8 executes JavaScript, Node.js bindings bridge to C++, and Libuv orchestrates async I/O via the OS kernel or a thread pool. Every delay in this chain shows up as event loop lag.
introduction-nodejs THECODEFORGE.IO Node.js Runtime Architecture Layers How V8, libuv, and OS interact for concurrency Application Layer JavaScript Code | Node.js APIs V8 Engine JavaScript Execution | Memory Management Node.js Bindings C++ Wrappers | Async Hooks libuv Library Event Loop | Thread Pool | I/O Operations Operating System Kernel | File System | Network Stack THECODEFORGE.IO
thecodeforge.io
Introduction Nodejs

Building 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.

ExampleJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 
 * Package: io.thecodeforge.node.web
 */
const http = require('http');

const PORT = process.env.PORT || 3000;

const server = http.createServer((req, res) => {
    const { method, url } = req;

    // Standard REST routing logic
    if (method === 'GET' && url === '/') {
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ 
            status: 'success', 
            message: 'Welcome to TheCodeForge API' 
        }));

    } else if (method === 'GET' && url === '/health') {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('UP');

    } else {
        res.writeHead(404, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ error: 'Endpoint Not Found' }));
    }
});

server.listen(PORT, () => {
    console.log(`[TheCodeForge] Server ignited on port ${PORT}`);
});
Output
[TheCodeForge] Server ignited on port 3000
Try it live
⚠ Streaming requests
req is a ReadableStream. If you don't read the body, backpressure builds and the client hangs. Always consume or pipe the request body even if you don't need it.
📊 Production Insight
In production, never write raw routing logic inside createServer.
Missing a content-type header leads to silent JSON parse failures on clients.
Rule: abstract routing into a framework (Express, Fastify) or at least separate handler functions.
🎯 Key Takeaway
Understand streams, but don't write raw servers in production.
Use frameworks for routing, middleware, and error handling.
Know the http module internals to debug connection issues.

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.

ExampleJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 
 * Package: io.thecodeforge.node.modules
 */

// --- CommonJS (Standard in older Node apps) ---
// utils.js -> module.exports = { log: (msg) => console.log(msg) };
// app.js   -> const { log } = require('./utils');

// --- ES Modules (Recommended for new projects) ---
// In package.json, set "type": "module"

import os from 'os';
import { networkInterfaces } from 'os';

const systemMetrics = {
    platform: os.platform(),
    freeMem: (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB',
    uptime: (os.uptime() / 3600).toFixed(1) + ' hours'
};

console.log('System Status:', systemMetrics);
Output
System Status: { platform: 'linux', freeMem: '4.21 GB', uptime: '12.4 hours' }
Try it live
📊 Production Insight
Mixing CommonJS and ESM in the same project causes ERR_REQUIRE_ESM.
Use .mjs for ESM files or set "type": "module" in package.json.
Rule: pick one system per project — never mix unless you understand the transpilation chain.
🎯 Key Takeaway
ES Modules are the standard.
CommonJS still works but is legacy.
Set "type": "module" and use import/export for new projects.
Choose your module system
IfNew project with modern Node (16+)
UseUse ES Modules — set 'type': 'module' in package.json
IfExisting project with many CommonJS dependencies
UseStay with CommonJS, or use dynamic import() for specific ESM packages
IfBuilding a library shared with browser/Node
UseWrite in ESM and let bundlers handle CommonJS fallback

npm 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.

ExampleBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Initialize a new project
npm init -y

# Install a package as a production dependency
npm install express

# Install as dev dependency
npm install --save-dev nodemon

# Run a script defined in package.json
npm run start

# List outdated packages
npm outdated

# Update all packages (respects semver)
npm update
🔥Lockfile is critical
Without package-lock.json, different environments may get different versions of transitive dependencies. This causes 'works on my machine' bugs. Always commit the lockfile.
📊 Production Insight
A node_modules folder can exceed 300MB for a simple app — don't commit it.
Use npm ci in CI/CD for deterministic installs from lockfile.
Rule: never run npm update blindly in production; review breaking changes first.
🎯 Key Takeaway
Lockfile is your contract for reproducible builds.
Use npm ci in CI, npm install locally.
Audit dependencies regularly with npm audit.

Essential Built-in Modules — Quick Reference

Node.js ships with a rich set of built-in modules that cover most common server-side tasks. The table below lists the most frequently used modules in production applications.

ModulePurposeKey MethodsTypical Use Case
fsFile system operationsreadFile, writeFile, createReadStream, accessReading configuration files, streaming large assets
pathFile path manipulationjoin, resolve, basename, extnameBuilding cross-platform file paths, extracting extensions
http / httpsHTTP server and clientcreateServer, request, getBuilding web servers, making outbound API calls
osOperating system informationcpus(), freemem(), platform(), networkInterfaces()Resource monitoring, clustering logic
cryptoCryptographic operationscreateHash, randomBytes, pbkdf2 (async), createCipherivPassword hashing, token generation, encryption
streamStreaming data abstractionReadable, Writable, Transform, pipelineProcessing large files line-by-line, compression
eventsEvent emitter patternEventEmitter, on, emitBuilding custom event-driven modules
child_processSpawning external processesexec, spawn, forkRunning shell commands, forking worker scripts
worker_threadsTrue parallelism within NodeWorker, parentPort, workerDataOffloading CPU-intensive work to separate threads
perf_hooksPerformance measurementperformance.now(), monitorEventLoopDelayMeasuring latency, event loop health

Production tip: always use the promise-based versions (require('fs').promises) for modern async/await code. The callback-based versions are more error-prone under load.

📊 Production Insight
Choosing the right built-in module reduces external dependencies and avoids the security risk of third-party packages. For example, crypto.randomBytes() is cryptographically secure and free — no need for uuid library. However, be cautious: crypto.pbkdf2Sync blocks the event loop; prefer the async version or use worker_threads.
🎯 Key Takeaway
Master these ten modules and you can build most production Node.js applications without reaching for npm packages.

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.

ExampleJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 
 * Package: io.thecodeforge.node.eventloop
 */
const fs = require('fs');
const crypto = require('crypto');

console.log('1. Main script start');

setTimeout(() => console.log('5. Timer phase'), 0);

setImmediate(() => console.log('6. Check phase (setImmediate)'));

process.nextTick(() => console.log('3. nextTick queue'));

Promise.resolve().then(() => console.log('4. Microtask queue (Promise)'));

fs.readFile('dummy.txt', () => {
    console.log('7. I/O callback (poll phase)');
    process.nextTick(() => console.log('8. nextTick inside I/O'));
    setImmediate(() => console.log('9. setImmediate inside I/O'));
});

console.log('2. Main script end');
Output
1. Main script start
2. Main script end
3. nextTick queue
4. Microtask queue (Promise)
5. Timer phase
6. Check phase (setImmediate)
7. I/O callback (poll phase)
8. nextTick inside I/O
9. setImmediate inside I/O
Try it live
📊 Production Insight
process.nextTick() can starve the event loop if called recursively.
Promise microtasks run after nextTick but before timers — ordering matters.
Rule: prefer setImmediate over nextTick for deferring work to the next iteration.
🎯 Key Takeaway
nextTick -> Promise -> Timer -> I/O -> setImmediate.
Don't starve the loop with synchronous microtask chains.
Use setImmediate when you want to yield control.

Monitoring Event Loop Health — Production Checklist

Event loop health is the single best indicator of whether your Node.js application will degrade gracefully under load. Without monitoring, you'll only notice the problem when users start reporting timeouts. Here is a practical checklist to implement in every production Node.js service.

  1. Measure event loop lag
  2. Use setInterval to record the time between scheduling and execution of a callback. If the lag exceeds 50ms, log a warning with stack traces of all active handles.
  3. Track event loop utilization
  4. Node.js 14+ exposes perf_hooks.monitorEventLoopDelay() which returns a histogram. Export the mean and p99 metrics to your monitoring system.
  5. Set alert thresholds
  6. - Warning: lag > 100ms for more than 5 seconds
  7. - Critical: lag > 1000ms for any duration — means the server is effectively dead
  8. - Investigate if poll phase time > 80% of total loop time
  9. Log blocking operations
  10. Enable --trace-event-categories node.perf.usertiming in production to see which functions are blocking. For a lightweight approach, wrap suspect functions with performance.mark/performance.measure.
  11. Profile with clinic (0-60s)
  12. Run clinic doctor -- node app.js and generate a flamegraph. The Event Loop view will show exactly where time is being spent.
  13. Monitor in CI/CD
  14. Add a step in your pipeline that runs a short load test and asserts that event loop lag stays below 200ms under moderate concurrency.
  15. Catch sync API calls
  16. Use an ESLint rule (no-sync) to prevent *Sync methods from entering request handlers. Pair it with a runtime guard in a --inspect session that prints a warning when a synchronous call takes longer than 50ms.
  17. Simulate failure in staging
  18. Use process.nextTick in a tight loop to temporarily block the event loop and verify your monitoring alerts fire correctly.
io/thecodeforge/node/monitoring/event-loop-health.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { monitorEventLoopDelay } = require('perf_hooks');
const histogram = monitorEventLoopDelay();
histogram.enable();

setInterval(() => {
  const mean = histogram.mean / 1e6; // convert nanoseconds to ms
  const p99 = histogram.percentile(99) / 1e6;
  console.log(`Event loop lag: mean=${mean.toFixed(2)}ms, p99=${p99.toFixed(2)}ms`);
  
  if (p99 > 100) {
    console.warn('CRITICAL: Event loop lag above 100ms!');
  }
  histogram.reset();
}, 5000);
Output
Event loop lag: mean=2.34ms, p99=12.10ms
Event loop lag: mean=3.01ms, p99=15.88ms
Try it live
⚠ Don't fix your monitoring after the outage
Event loop lag monitoring is a proactive measure. If you wait until customers complain, you've already lost revenue and trust. Set up these checks on day one of a new service.
📊 Production Insight
In high-traffic clusters, event loop lag can vary significantly between instances. Use a distributed tracing system to correlate lag with specific request characteristics. One pattern we've seen work: emit lag metrics every second and graph them against request latency. If you see a correlation, you've found the blocking code.
🎯 Key Takeaway
Event loop health monitoring is not optional in production. Measure lag, set alerts, profile regularly, and enforce sync API bans to keep your Node.js application responsive.

Why You Should Care About Node.js—Speed Without Threads

Most server frameworks block on I/O. A database query or file read stalls the entire thread. Node.js doesn't. It uses an event loop and non-blocking I/O to handle thousands of concurrent connections without spawning a thread per request. That means lower memory overhead and higher throughput for I/O-bound workloads—APIs, real-time dashboards, streaming services. The trade-off? CPU-heavy tasks (image processing, encryption) will block the loop. You offload those to worker threads or a separate service. Know your bottleneck before you pick the tool.

server.mjsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
// io.thecodeforge
import { createServer } from 'node:http';

const server = createServer((req, res) => {
  // Simulate a non-blocking database call
  setTimeout(() => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Order processed');
  }, 10); // Never blocks the loop
});

server.listen(4000, () => console.log('Listening on 4000'));
Output
Listening on 4000
Try it live
⚠ Production Trap:
Never use synchronous methods (readFileSync, writeFileSync) inside request handlers. They freeze the event loop for all users. Profile with process.hrtime.bigint() to catch accidental blocking.
🎯 Key Takeaway
Node.js is fast not because of raw compute, but because it never waits.

Getting Started—Your First Server in 10 Lines

Forget the hello-world wrapper. You need to understand the minimum viable server. Import node:http, create a listener, handle a request, send a response. That's it. The callback fires every time a connection hits your port. You control headers, status codes, and the body. No framework, no magic. First, verify Node.js is installed with node --version. Then run the file. The server stays alive because the event loop keeps it open. Hit Ctrl+C to kill it. This pattern scales to thousands of lines—but start small.

start.mjsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
// io.thecodeforge
import { createServer } from 'node:http';

const server = createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('API v1 ready');
});

server.listen(3000, '127.0.0.1', () => {
  console.log(`Server running at http://127.0.0.1:3000/`);
});
Output
Server running at http://127.0.0.1:3000/
Try it live
⚠ Production Trap:
Bind to 127.0.0.1 in development, not 0.0.0.0. Exposing port 3000 to the network before adding authentication is how attackers find your service.
🎯 Key Takeaway
The HTTP server is just a callback. Everything else is added on top.

Asynchronous Programming—The Only Way to Survive

Node.js is single-threaded. If you block the thread, you block all users. The solution: asynchronous patterns. Callbacks worked in 2009, but they nest into callback hell. Promises (ES2015) flattened the pyramid. async/await (ES2017) made it look synchronous. Use async/await today. Every I/O call in Node's standard library returns a promise. Wrap error handling in try/catch. Never ignore promise rejections—they crash the process in Node 15+. Use process.on('unhandledRejection') as a safety net.

fetchOrders.mjsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
// io.thecodeforge
import { readFile } from 'node:fs/promises';

try {
  const data = await readFile('./orders.json', 'utf8');
  const orders = JSON.parse(data);
  console.log(`Loaded ${orders.length} orders`);
} catch (err) {
  console.error('Order load failed:', err.message);
  process.exit(1);
}
Output
Loaded 42 orders
Try it live
⚠ Production Trap:
JSON.parse() is synchronous and expensive for large files. Stream the file into a parser instead. Use fs.createReadStream with a streaming JSON parser like JSONStream.
🎯 Key Takeaway
Async is not optional—it's the only way to keep the event loop moving.

Installing Node.js on Windows, macOS, and Linux

Before you can run any Node.js code, you need the runtime installed. The recommended approach is to use a version manager rather than the official installer, because you'll likely need to switch between Node versions for different projects.

Windows & macOS: Install nvm-windows (Windows) or nvm (macOS/Linux). On macOS, brew install nvm works. On Linux, use the install script from the nvm GitHub repo. After installation, run nvm install --lts to get the latest long-term support version. Verify with node --version and npm --version.

Linux (without nvm): Use your package manager. For Ubuntu/Debian: curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs. For Fedora: sudo dnf install nodejs.

Docker: For CI or isolated environments, use the official Node image: docker run -it node:lts-alpine sh.

Production note: Always pin your Node version in .nvmrc or engines in package.json to avoid surprises. Never use the system package manager's Node for production—it's often outdated.

install.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
# macOS (Homebrew)
brew install nvm
nvm install --lts
nvm use --lts

# Windows (nvm-windows)
# Download from https://github.com/coreybutler/nvm-windows/releases
nvm install lts
nvm use lts

# Verify
node --version
npm --version
Output
v20.11.0
10.2.4
⚠ Avoid sudo npm install -g
Global installs with sudo can cause permission issues. Use nvm to manage Node versions—it installs in your home directory, avoiding sudo entirely.
📊 Production Insight
Pin your Node version in .nvmrc and package.json engines field. CI/CD pipelines should use the same version to prevent 'works on my machine' bugs.
🎯 Key Takeaway
Use a version manager (nvm) to install Node.js. It lets you switch versions per project and avoids permission headaches.

The Node.js REPL: Your Interactive Playground

The REPL (Read-Eval-Print Loop) is a quick way to test JavaScript snippets without creating files. Just type node in your terminal and you're in.

Start with basic expressions: > 2 + 2 prints 4. Use _ to reference the last result: > _ + 2 gives 6. Define variables and functions: > const greet = name => Hello, ${name}!; then > greet('World').

Multi-line blocks: Type { and press Enter—the prompt changes to .... Type your code and close with }. For example, a for loop: `` ... for (let i = 0; i < 3; i++) { ... console.log(i); ... } `` Press Enter twice to execute.

Special commands: .help lists all commands. .clear resets the context. .exit quits. .save saves your session to a file: .save mysession.js. .load loads a file: .load mysession.js.

Tab completion: Type global. and press Tab twice to see all globals. Type process. and Tab to explore process methods.

Production note: The REPL is for development only. Never leave a REPL open in production—it's a security risk and a resource leak.

repl-session.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
// Example REPL session
$ node
> const add = (a, b) => a + b
undefined
> add(3, 4)
7
> const users = ['Alice', 'Bob']
undefined
> users.map(u => u.toUpperCase())
[ 'ALICE', 'BOB' ]
> .exit
Try it live
💡REPL for Debugging
When debugging, drop into REPL with node -e "console.log(require('fs').readdirSync('.'))" for one-liners, or use node -i to start REPL after executing a script.
📊 Production Insight
Never run a persistent REPL in production. Use it only for ad-hoc debugging in development, and always exit when done.
🎯 Key Takeaway
The REPL is great for quick experiments and debugging. Use .save to persist sessions and .load to rerun them.

Hello World: Your First Node.js Server

Enough theory—let's build a real HTTP server. Create a file server.js with the following code. This is the minimal production-ready server you'll use as a starting point for any web app.

```javascript const http = require('http');

const hostname = '127.0.0.1'; const port = 3000;

const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello, World! '); });

server.listen(port, hostname, () => { console.log(Server running at http://${hostname}:${port}/); }); ```

Run it with node server.js. Open http://127.0.0.1:3000 in your browser—you'll see "Hello, World!". Press Ctrl+C to stop.

What's happening? http.createServer registers a callback that runs on every request. The server is event-driven: it doesn't block while waiting for requests. The callback receives req (incoming request) and res (outgoing response). We set a 200 status, a plain text content type, and send the response.

Production note: This server is single-threaded and handles one request at a time per event loop tick. For real apps, use a process manager like PM2 to fork multiple workers. Also, never use 127.0.0.1 in production—bind to 0.0.0.0 to accept external connections.

server.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello, World!\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
Output
Server running at http://127.0.0.1:3000/
Try it live
🔥Always Handle Errors
Add server.on('error', (err) => { console.error('Server error:', err); }) to catch port-in-use or permission errors gracefully.
📊 Production Insight
Use 0.0.0.0 as hostname in production, and always add error handling. Use PM2 or Docker for process management and zero-downtime restarts.
🎯 Key Takeaway
A Node.js HTTP server is just a few lines. The event loop handles concurrency automatically—no thread management needed.

npm init and package.json: The Heart of Every Project

Every Node.js project starts with a package.json file. It's the manifest that holds metadata, dependencies, scripts, and configuration. Create one by running npm init in your project directory. Answer the prompts or use npm init -y to accept defaults.

``json { "name": "my-app", "version": "1.0.0", "description": "A sample Node.js app", "main": "index.js", "scripts": { "start": "node index.js", "dev": "node --watch index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } ``

Key fields
  • name: must be lowercase, no spaces. Used for publishing packages.
  • version: follow semver (major.minor.patch).
  • main: entry point when someone require()s your package.
  • scripts: define shortcuts. npm start runs the start script. npm run dev runs the dev script. npm test runs tests.
  • dependencies and devDependencies: added automatically when you npm install or npm install --save-dev .

Production note: Always commit package.json and package-lock.json to version control. The lockfile ensures reproducible installs across environments. Never edit package-lock.json manually—let npm handle it.

init.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
# Create a new project
mkdir my-app && cd my-app
npm init -y

# Install a dependency
npm install express

# Install a dev dependency
npm install --save-dev nodemon

# Check package.json
cat package.json
Output
{
"name": "my-app",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.2"
}
}
⚠ Don't Commit node_modules
Add node_modules/ to .gitignore. The package-lock.json ensures reproducible installs—no need to bloat your repo.
📊 Production Insight
Use npm ci in CI/CD instead of npm install. It's faster and strictly uses the lockfile, failing if it doesn't match package.json.
🎯 Key Takeaway
npm init creates the project manifest. Use npm install to add dependencies. Always commit package-lock.json for reproducible builds.
Sync vs Async Crypto in Event Loop Impact on concurrency and performance Synchronous Crypto Asynchronous Crypto Event Loop Blocking Blocks entire loop Non-blocking Concurrent Requests Handles one at a time Handles many simultaneously CPU Utilization Single-threaded bottleneck Uses thread pool Scalability Poor for high traffic Excellent for high traffic API Example crypto.createHash('sha256') crypto.createHash('sha256').update().dig THECODEFORGE.IO
thecodeforge.io
Introduction Nodejs

Running Scripts with the Node Command

The node command is your primary tool to execute JavaScript files. Beyond node filename.js, there are several flags and patterns you'll use daily.

Basic execution: node app.js runs the file. If you want to evaluate a string, use -e: node -e "console.log('hello')".

Watch mode (Node 18+): node --watch app.js restarts the process when files change. Great for development—no need for nodemon.

Environment variables: Prefix the command: NODE_ENV=production node app.js. Access via process.env.NODE_ENV.

Inspector/debugging: node --inspect app.js starts the Chrome DevTools protocol. Open chrome://inspect in Chrome to attach. For breakpoints, add debugger; in your code.

Memory and performance: Use --max-old-space-size=4096 to increase heap size (in MB). --trace-gc logs garbage collection events.

Production note: In production, never run Node directly. Use a process manager like PM2 (pm2 start app.js -i max) to fork workers, handle crashes, and manage logs. Also, set NODE_ENV=production to disable development warnings and enable optimizations.

run.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Basic run
node app.js

# Watch mode (Node 18+)
node --watch app.js

# With environment variable
NODE_ENV=production node app.js

# Inspect mode
node --inspect app.js

# Increase memory
node --max-old-space-size=4096 app.js

# Evaluate expression
node -e "console.log(process.version)"
Output
v20.11.0
💡Use --watch in Development
Node 18+'s built-in --watch flag eliminates the need for nodemon in most cases. It's faster and has no extra dependencies.
📊 Production Insight
Never run Node bare in production. Use PM2 with -i max to utilize all CPU cores. Set NODE_ENV=production and monitor memory with --max-old-space-size.
🎯 Key Takeaway
Master the node command flags: --watch for dev, --inspect for debugging, and environment variables for configuration.
● Production incidentPOST-MORTEMseverity: high

The CPU-Bound Route That Killed Our API

Symptom
Under load, every request to the API started timing out after a few minutes. CPU usage was moderate (60%), but the event loop lag exceeded 2000ms.
Assumption
The team assumed that because the decryption was done with crypto, it must be asynchronous. They didn't check the method signature.
Root cause
A developer used crypto.pbkdf2Sync() instead of the async crypto.pbkdf2(). The synchronous version blocks the event loop for the duration of the hash computation.
Fix
Replaced all ...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.
Key lesson
  • 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 Sync functions in request paths.
Production debug guideSymptom → Action guide for common production problems4 entries
Symptom · 01
All requests start timing out after a few minutes
Fix
Check event loop lag: use clinic doctor or manually log process.hrtime() delta in a setInterval. Look for Sync functions or CPU-heavy loops.
Symptom · 02
Memory usage grows linearly over time
Fix
Take heap snapshot with node --inspect and compare snapshots. Check for closures in Promises, unclosed connections, or large retained objects.
Symptom · 03
'Cannot find module' error after deployment
Fix
Verify 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.
Symptom · 04
HTTP connections stay open indefinitely
Fix
Check for missing res.end() in response handlers. Use --inspect to list open handles. Add request timeout middleware (e.g., connect-timeout).
★ Quick Debug Cheat Sheet: Node.js Async IssuesInstant commands to diagnose the three most common Node.js production failures.
Event loop blocked
Immediate action
Measure lag
Commands
node -e "setInterval(() => { const start = Date.now(); setImmediate(() => console.log('lag (ms):', Date.now() - start)); }, 1000)"
clinic doctor -- node app.js
Fix now
Replace Sync calls with async; move CPU work to worker_threads
Memory leak+
Immediate action
Take heap snapshot
Commands
node --inspect app.js
chrome://inspect -> Memory tab -> Take snapshot before and after load test
Fix now
Fix closures, limit global caches, use WeakMap for event listeners
'port already in use'+
Immediate action
Kill process on port
Commands
lsof -ti :3000 | xargs kill
fuser -k 3000/tcp
Fix now
Use environment variable for port, or use server.close() on SIGTERM
Node.js vs Traditional Threaded Servers
AspectNode.jsApache (thread-per-connection)NGINX (event-driven)
Concurrency modelSingle thread + event loopThread per connectionEvent-driven (similar to Node.js)
Memory per connection~10-20 KB~ 2-8 MB~ 10-20 KB
Best forI/O-bound workloads (APIs, real-time)CPU-bound / simple static filesStatic files, reverse proxy
Worst forCPU-heavy tasks (image processing)High concurrency with many connectionsDynamic application logic
⚙ Quick Reference
9 commands from this guide
FileCommand / CodePurpose
iothecodeforgenodemonitoringevent-loop-health.jsconst { monitorEventLoopDelay } = require('perf_hooks');Monitoring Event Loop Health
server.mjsconst server = createServer((req, res) => {Why You Should Care About Node.js
start.mjsconst server = createServer((req, res) => {Getting Started
fetchOrders.mjstry {Asynchronous Programming
install.shbrew install nvmInstalling Node.js on Windows, macOS, and Linux
repl-session.js$ nodeThe Node.js REPL
server.jsconst http = require('http');Hello World
init.shmkdir my-app && cd my-appnpm init and package.json
run.shnode app.jsRunning Scripts with the Node Command

Key takeaways

1
Node.js runs JavaScript outside the browser using Google's V8 engine.
2
Single-threaded event loop with non-blocking I/O
efficient for many concurrent I/O operations.
3
CPU-intensive tasks block the event loop
use worker_threads for computation, not for waiting on I/O.
4
CommonJS (require/module.exports) is the traditional module system. ES Modules (import/export) are the modern standard.
5
npm is Node's package manager
package.json describes your project's dependencies and scripts.
6
Always commit package-lock.json and use npm ci for deterministic builds.
7
Monitor event loop lag in production
it's the earliest sign of blocking code.
8
Install Node.js with nvm
Use a version manager to avoid permission issues and easily switch between Node versions per project. Pin your version in .nvmrc.
9
Master the Node command
Use --watch for auto-restart in development, --inspect for debugging, and environment variables for configuration. Never run Node bare in production—use PM2.
10
package.json is your project's backbone
Run npm init to create it, use npm install for dependencies, and always commit package-lock.json. Use npm ci in CI for reproducible builds.
11
Install Node.js correctly
Use official installers for simplicity, nvm for version management, and always verify with node --version. Pin versions in production.
12
Master the REPL and node command
The REPL is for quick experiments; node -e for one-liners; --check for syntax validation; --inspect for debugging.
13
package.json is your project's backbone
Use npm init -y to start, keep dependencies lean, and commit package-lock.json for reproducible installs. Use npm ci in CI.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the phases of the Node.js Event Loop. Where does process.nextTic...
Q02SENIOR
Why is Node.js considered 'unsuitable' for heavy data crunching, and how...
Q03SENIOR
LeetCode Scenario: Given an array of 1,000 file paths, write a script to...
Q04SENIOR
Compare and contrast the behavior of 'require()' vs 'import' regarding s...
Q05JUNIOR
What is 'Callback Hell' and how do Promises or Async/Await resolve the u...
Q01 of 05SENIOR

Explain the phases of the Node.js Event Loop. Where does process.nextTick() fit into these phases?

ANSWER
The Event Loop has six phases: timers, pending callbacks, idle/prepare, poll, check, close. Between each phase, Node processes the 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.
FAQ · 10 QUESTIONS

Frequently Asked Questions

01
Is Node.js good for CPU-intensive tasks?
02
What is the difference between Node.js and a browser JavaScript environment?
03
What is Libuv and why does Node.js need it?
04
Should I use `npm install` or `npm ci` in CI/CD?
05
What is the difference between `setImmediate` and `process.nextTick`?
06
What's the difference between npm install and npm ci?
07
How do I debug a Node.js application without stopping it?
08
Why should I use a version manager like nvm instead of the official installer?
09
How do I uninstall Node.js completely?
10
Can I use ES modules with Node.js without a transpiler?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's Node.js. Mark it forged?

8 min read · try the examples if you haven't

Previous
Next.js Monorepo with Turborepo: Enterprise Architecture
1 / 47 · Node.js
Next
Node.js Modules and CommonJS