Node.js interviews test event-loop intuition, async-programming depth, built-in module knowledge, and backend architecture skills. This guide covers the 50 most common questions — with concise answers and runnable code examples.
Quick reference
| Topic | Most asked questions |
|---|---|
| Architecture | Event loop, non-blocking I/O, single thread |
| Async | Callbacks, Promises, async/await, EventEmitter |
| Modules | CommonJS vs ES modules, require caching |
| Core modules | fs, path, http, stream, crypto, child_process |
| Performance | Clustering, worker threads, memory leaks |
| Ecosystem | npm, package.json, Express, error handling |
Architecture
1. What is Node.js and how does it differ from browser JavaScript?
Node.js is a server-side JavaScript runtime built on V8 (Chrome's JS engine) and libuv (a C library that provides the event loop and async I/O).
| Feature | Node.js | Browser JS |
|---|---|---|
| Environment | Server | Browser sandbox |
| APIs | fs, http, process, os |
DOM, fetch, localStorage |
| Modules | CommonJS + ESM | ESM (+ legacy <script>) |
| Security | Full OS access | Sandboxed |
| Global object | global / globalThis |
window / globalThis |
// Node.js — read a file
const fs = require('fs');
const data = fs.readFileSync('hello.txt', 'utf8');
console.log(data);
2. Explain the Node.js event loop.
The event loop lets Node.js perform non-blocking I/O with a single thread by offloading operations to the OS or libuv thread pool, then executing callbacks when they complete.
Phases (each iteration / "tick"):
- timers — executes
setTimeout/setIntervalcallbacks whose delay has elapsed - pending callbacks — I/O callbacks deferred from the previous loop
- idle/prepare — internal use
- poll — retrieves new I/O events; blocks here if queue is empty
- check — executes
setImmediatecallbacks - close callbacks — e.g.
socket.on('close', ...)
Between each phase, Node drains the microtask queue (Promise callbacks, queueMicrotask) and process.nextTick queue.
console.log('1 — sync');
setTimeout(() => console.log('4 — timer'), 0);
setImmediate(() => console.log('5 — check'));
Promise.resolve().then(() => console.log('3 — microtask'));
process.nextTick(() => console.log('2 — nextTick'));
// Output: 1 → 2 → 3 → 4 → 5
3. What does "non-blocking I/O" mean?
When Node.js starts an I/O operation (reading a file, querying a DB, making an HTTP request), it does not wait for the result. It registers a callback and continues executing other code. The callback runs when the OS signals completion.
const fs = require('fs');
// Non-blocking — callback fires when OS delivers data
fs.readFile('big.log', 'utf8', (err, data) => {
console.log('file ready', data.length);
});
console.log('this runs FIRST');
The thread pool (default 4 workers, set UV_THREADPOOL_SIZE) handles tasks that can't be done asynchronously by the OS, such as DNS lookups, crypto, and fs operations on some platforms.
4. What is process.nextTick vs setImmediate vs setTimeout(fn, 0)?
| When it runs | Microtask? | |
|---|---|---|
process.nextTick(cb) |
Before any I/O, before Promise microtasks |
Yes (own queue, highest priority) |
Promise.then(cb) |
After nextTick queue is empty |
Yes |
setImmediate(cb) |
After poll phase (check phase) | No |
setTimeout(cb, 0) |
timers phase, ≥ 1 ms delay | No |
Rule of thumb: nextTick is fastest but can starve I/O if called recursively. setImmediate is preferred for deferring work after I/O.
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('Promise'));
setImmediate(() => console.log('setImmediate'));
setTimeout(() => console.log('setTimeout'), 0);
// nextTick → Promise → setTimeout → setImmediate
// (timer vs setImmediate order can vary outside I/O callbacks)
5. Is Node.js truly single-threaded?
The JS execution is single-threaded. But Node.js uses multiple threads internally:
- libuv thread pool (default 4 threads) — handles
fs,dns.lookup,crypto,zlib - V8 garbage collector — runs on a background thread
- Worker threads (
worker_threadsmodule) — user-created threads for CPU-bound work
So "single-threaded" refers to the JS event loop, not the entire process.
Modules
6. What is the difference between CommonJS and ES modules?
| Feature | CommonJS (CJS) | ES Modules (ESM) |
|---|---|---|
| Syntax | require() / module.exports |
import / export |
| Loading | Synchronous, dynamic | Asynchronous, static |
Top-level await |
No | Yes |
| Tree shaking | No | Yes |
| File extension | .js (default) |
.mjs or "type":"module" in package.json |
__dirname |
Available | Use import.meta.url |
// CJS
const path = require('path');
module.exports = { greet };
// ESM
import path from 'path';
export { greet };
7. How does require() caching work?
The first time you require() a module, Node executes it and caches the result in require.cache. Subsequent calls return the cached export — the module code does not run again.
// counter.js
let count = 0;
module.exports = { increment: () => ++count, get: () => count };
// main.js
const a = require('./counter');
const b = require('./counter'); // same cached object
a.increment();
console.log(b.get()); // 1 — same reference
To bypass caching (rare): delete require.cache[require.resolve('./counter')]
8. What is the module.exports vs exports gotcha?
exports is a reference to module.exports. Reassigning exports breaks the link.
// WRONG — exports is now a new object; module.exports is still {}
exports = { greet: () => 'hi' };
// CORRECT
module.exports = { greet: () => 'hi' };
// OR
exports.greet = () => 'hi'; // mutating the shared object is fine
9. How do you get __dirname in an ES module?
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Or with the newer Node.js 21+ API:
import { dirname } from 'path';
const __dirname = dirname(import.meta.filename); // Node ≥ 21.2
Async patterns
10. What is callback hell and how do you avoid it?
Callback hell (pyramid of doom) is deeply nested callbacks that make code hard to read and reason about.
// callback hell
fs.readFile('a.txt', (err, a) => {
fs.readFile('b.txt', (err, b) => {
fs.readFile('c.txt', (err, c) => {
// ...
});
});
});
Fixes:
- Named functions — extract callbacks into named functions
- Promises — chain
.then()calls - async/await — read like synchronous code
// async/await
async function readAll() {
const a = await fs.promises.readFile('a.txt', 'utf8');
const b = await fs.promises.readFile('b.txt', 'utf8');
const c = await fs.promises.readFile('c.txt', 'utf8');
return a + b + c;
}
11. How do you run async operations in parallel in Node.js?
Use Promise.all for parallel execution. await inside a loop runs sequentially.
// SEQUENTIAL — slow
for (const url of urls) {
const res = await fetch(url); // waits for each
}
// PARALLEL — fast
const results = await Promise.all(urls.map(url => fetch(url)));
For bounded concurrency (avoid overwhelming a server):
import pLimit from 'p-limit';
const limit = pLimit(5); // max 5 concurrent
const results = await Promise.all(
urls.map(url => limit(() => fetch(url)))
);
12. What is the EventEmitter class?
EventEmitter is the foundation for Node.js's event-driven architecture. Many core objects (http.Server, fs.ReadStream, net.Socket) extend it.
const { EventEmitter } = require('events');
class OrderService extends EventEmitter {
place(order) {
// process order...
this.emit('order:placed', order);
}
}
const svc = new OrderService();
svc.on('order:placed', (order) => console.log('Email sent for', order.id));
svc.once('order:placed', () => console.log('First order!')); // fires only once
svc.place({ id: 42 });
Memory leak warning: Node emits a warning if you add more than 10 listeners to one event. Increase with emitter.setMaxListeners(n).
13. What is util.promisify?
Converts a Node-style callback function (err, result) => void into a Promise-returning function.
const { promisify } = require('util');
const fs = require('fs');
const readFile = promisify(fs.readFile);
const data = await readFile('hello.txt', 'utf8');
Modern Node.js ships promise-based APIs natively: fs.promises, dns.promises, etc.
Core modules
14. How do you create an HTTP server without Express?
const http = require('http');
const server = http.createServer((req, res) => {
if (req.method === 'GET' && req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
return;
}
res.writeHead(404);
res.end('Not found');
});
server.listen(3000, () => console.log('Listening on :3000'));
15. What are Node.js streams and what types exist?
Streams process data piece by piece rather than loading everything into memory — essential for large files, HTTP requests, and real-time data.
| Type | Description | Example |
|---|---|---|
| Readable | Source of data | fs.createReadStream, http.IncomingMessage |
| Writable | Destination | fs.createWriteStream, http.ServerResponse |
| Duplex | Readable + Writable | net.Socket, crypto.Cipher |
| Transform | Duplex that modifies data | zlib.createGzip, crypto.createCipheriv |
const fs = require('fs');
const zlib = require('zlib');
// Pipe a large file through gzip compression to output — minimal memory usage
fs.createReadStream('bigfile.csv')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('bigfile.csv.gz'));
16. What is backpressure in streams?
Backpressure occurs when a Writable can't keep up with a Readable. If ignored, data buffers in memory and can crash the process.
pipe() handles backpressure automatically by pausing the readable when the writable's internal buffer is full.
When using streams manually:
readable.on('data', (chunk) => {
const ok = writable.write(chunk);
if (!ok) {
readable.pause(); // stop producing
writable.once('drain', () => {
readable.resume(); // resume when writable is ready
});
}
});
17. How do you read and write files in Node.js?
const fs = require('fs/promises'); // promise-based (Node ≥ 10)
// Read
const content = await fs.readFile('data.txt', 'utf8');
// Write (overwrites)
await fs.writeFile('out.txt', 'hello');
// Append
await fs.appendFile('log.txt', 'new line\n');
// Stream (large files)
const readable = require('fs').createReadStream('big.csv');
for await (const chunk of readable) {
process(chunk);
}
18. What does the path module provide?
Platform-safe path manipulation — essential for cross-platform code (Windows uses \, POSIX uses /).
const path = require('path');
path.join('/users', 'alice', 'docs'); // /users/alice/docs
path.resolve('src', 'index.js'); // absolute from CWD
path.dirname('/users/alice/file.txt'); // /users/alice
path.basename('/users/alice/file.txt'); // file.txt
path.extname('/users/alice/file.txt'); // .txt
path.parse('/users/alice/file.txt');
// { root: '/', dir: '/users/alice', base: 'file.txt', ext: '.txt', name: 'file' }
19. How do you hash a password in Node.js?
Use crypto.scrypt (built-in) or the bcrypt / argon2 packages. Never use MD5 or SHA for passwords.
const crypto = require('crypto');
const { promisify } = require('util');
const scrypt = promisify(crypto.scrypt);
async function hashPassword(password) {
const salt = crypto.randomBytes(16).toString('hex');
const key = await scrypt(password, salt, 64);
return `${salt}:${key.toString('hex')}`;
}
async function verify(password, stored) {
const [salt, hash] = stored.split(':');
const key = await scrypt(password, salt, 64);
return crypto.timingSafeEqual(Buffer.from(hash, 'hex'), key);
}
20. How do you spawn a child process?
const { exec, execFile, spawn, fork } = require('child_process');
// exec — runs in a shell, buffers output (small output only)
exec('ls -la', (err, stdout, stderr) => console.log(stdout));
// spawn — streaming, no shell, safe for user input
const ls = spawn('ls', ['-la']);
ls.stdout.pipe(process.stdout);
// fork — spawn a Node.js child with IPC channel
const child = fork('./worker.js');
child.send({ task: 'compute' });
child.on('message', (result) => console.log(result));
Use spawn (not exec) when input comes from users to avoid shell injection.
Express and HTTP
21. What is Express middleware and how does it work?
Middleware is a function with signature (req, res, next). Express chains middleware left to right; calling next() passes control to the next one.
const express = require('express');
const app = express();
// Application-level middleware
app.use(express.json());
// Custom logging middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next(); // must call next or send a response
});
// Route handler
app.get('/users', (req, res) => res.json([]));
// Error-handling middleware — 4 parameters
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: err.message });
});
app.listen(3000);
22. What is app.use vs app.get?
app.use(path, fn) |
app.get(path, fn) |
|
|---|---|---|
| HTTP method | Any method | GET only |
| Path matching | Prefix match (/api matches /api/users) |
Exact match |
| Use case | Middleware, routers | Route handlers |
23. How do you handle errors in Express async route handlers?
Express doesn't catch Promise rejections automatically (v4). You must either:
Option A — wrapper:
const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) throw Object.assign(new Error('Not found'), { status: 404 });
res.json(user);
}));
Option B — Express 5 (handles async natively):
// npm install express@5
app.get('/users/:id', async (req, res) => {
const user = await db.findUser(req.params.id); // rejection auto-forwarded to error handler
res.json(user);
});
24. What is CORS and how do you enable it in Express?
Cross-Origin Resource Sharing — browsers block requests from one origin to another by default. The server opts in via response headers.
const cors = require('cors');
// Allow all origins (dev only)
app.use(cors());
// Production: specific origin + credentials
app.use(cors({
origin: 'https://app.example.com',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
For preflight (OPTIONS) requests, Express's cors() middleware handles them automatically.
25. How do you parse the request body in Express?
app.use(express.json()); // parses application/json
app.use(express.urlencoded({ extended: true })); // parses form data
app.post('/data', (req, res) => {
console.log(req.body); // already parsed
res.json({ received: req.body });
});
For file uploads, use multer:
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('file'), (req, res) => {
res.json({ filename: req.file.filename });
});
Error handling
26. What is the difference between operational errors and programmer errors?
| Operational errors | Programmer errors | |
|---|---|---|
| Cause | Expected runtime conditions: network failure, invalid user input, file not found | Bugs: TypeError, wrong API usage, logic errors |
| Handling | Catch and handle gracefully (send 400/500, retry, log) | Let it crash and restart (PM2/nodemon), fix the code |
| Examples | ECONNREFUSED, ENOENT, 404 |
Cannot read properties of undefined |
27. How do you handle uncaught exceptions and unhandled rejections?
// Last resort — avoid relying on these
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err);
process.exit(1); // MUST exit — process state is undefined
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason);
// In Node.js 15+, this crashes by default (good behaviour)
process.exit(1);
});
Better: always await or .catch() promises; use a global error handler in Express.
28. How do you implement graceful shutdown?
const server = app.listen(3000);
async function shutdown(signal) {
console.log(`${signal} received — shutting down`);
server.close(async () => {
await db.end(); // close DB pool
process.exit(0);
});
// Force-kill after 10 s if connections don't drain
setTimeout(() => process.exit(1), 10_000).unref();
}
process.on('SIGTERM', () => shutdown('SIGTERM')); // Kubernetes sends this
process.on('SIGINT', () => shutdown('SIGINT')); // Ctrl+C
Performance and scaling
29. What is the cluster module?
Node.js runs on a single CPU core. The cluster module forks multiple worker processes, each running the same server code on all available cores.
const cluster = require('cluster');
const os = require('os');
const http = require('http');
if (cluster.isPrimary) {
const cpus = os.cpus().length;
console.log(`Primary ${process.pid} — forking ${cpus} workers`);
for (let i = 0; i < cpus; i++) cluster.fork();
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.pid} died — restarting`);
cluster.fork();
});
} else {
http.createServer((req, res) => {
res.end(`Worker ${process.pid} responded`);
}).listen(3000);
}
The OS load-balances incoming connections across workers.
30. What are Worker Threads and when do you use them?
worker_threads lets you run JavaScript in parallel threads — useful for CPU-intensive work that would block the event loop (image processing, JSON parsing of huge files, ML inference).
// main.js
const { Worker } = require('worker_threads');
const worker = new Worker('./cpu-task.js', { workerData: { n: 40 } });
worker.on('message', (result) => console.log('Fibonacci:', result));
worker.on('error', console.error);
// cpu-task.js
const { workerData, parentPort } = require('worker_threads');
function fib(n) { return n < 2 ? n : fib(n-1) + fib(n-2); }
parentPort.postMessage(fib(workerData.n));
Cluster vs Worker Threads:
- Cluster — multiple processes, each with own event loop, for I/O-bound horizontal scaling
- Worker Threads — multiple threads in one process sharing memory, for CPU-bound tasks
31. What causes memory leaks in Node.js and how do you detect them?
Common causes:
- Global variables holding growing data
- Event listeners not removed (
emitter.off()) - Closures capturing large objects
- Uncleared timers (
setIntervalwithoutclearInterval) - Cache with no eviction policy
Detection:
// Heap snapshot via --inspect flag, then Chrome DevTools > Memory
node --inspect app.js
// Log memory usage
setInterval(() => {
const { rss, heapUsed, heapTotal } = process.memoryUsage();
console.log({ rss: rss >> 20, heapUsed: heapUsed >> 20 });
}, 5000);
Tools: clinic.js, heapdump, 0x flame graphs, --inspect + Chrome DevTools allocation timeline.
32. What is the Node.js thread pool and how do you configure it?
libuv maintains a thread pool (default 4 threads) for operations that can't be async at the OS level: fs on some platforms, dns.lookup, crypto, zlib.
UV_THREADPOOL_SIZE=16 node server.js
If your app does heavy crypto or fs work, increasing this reduces queuing. Max is 128 (1024 in Node 22+).
npm and packages
33. What is the difference between dependencies and devDependencies?
dependencies |
devDependencies |
|
|---|---|---|
| When installed | Always (npm install) |
Only in dev (npm install, not npm install --omit=dev) |
| Examples | express, axios, prisma |
jest, eslint, typescript, nodemon |
| In production Docker | Include | Exclude with npm ci --omit=dev |
34. What is npm ci and when should you use it?
npm ci installs exactly what's in package-lock.json, failing if lock and package.json diverge. It deletes node_modules first.
Use it in CI/CD and Docker for reproducible, faster installs.
COPY package.json package-lock.json ./
RUN npm ci --omit=dev # faster, deterministic, no devDeps
35. What is semantic versioning (semver)?
MAJOR.MINOR.PATCH — e.g. 4.18.2
| Symbol | Meaning | Example |
|---|---|---|
^4.18.2 |
≥ 4.18.2 and < 5.0.0 (compatible minor/patch) | default npm install |
~4.18.2 |
≥ 4.18.2 and < 4.19.0 (patch only) | conservative |
4.18.2 |
Exact version | locked |
* |
Any version | dangerous |
Security
36. How do you prevent command injection in Node.js?
Never pass user input to exec() (which runs in a shell). Use execFile() or spawn() with an args array.
const { execFile } = require('child_process');
// SAFE — no shell, args are separate
execFile('convert', [userProvidedFilename, 'out.png'], (err, stdout) => {
// ...
});
// DANGEROUS — shell interprets special chars
exec(`convert ${userProvidedFilename} out.png`); // rm -rf if filename is "x; rm -rf /"
37. What is path traversal and how do you prevent it?
Attackers supply ../../etc/passwd as a filename parameter to read arbitrary files.
const path = require('path');
const BASE = path.resolve('./uploads');
function safeRead(userFilename) {
const resolved = path.resolve(BASE, userFilename);
if (!resolved.startsWith(BASE + path.sep)) {
throw new Error('Access denied');
}
return fs.promises.readFile(resolved);
}
38. How do you store secrets in Node.js?
- Use environment variables — never hardcode in source
- Use
.envfiles locally (withdotenv) — add.envto.gitignore - In production — use a secret manager (AWS Secrets Manager, HashiCorp Vault, Doppler)
require('dotenv').config();
const DB_URL = process.env.DATABASE_URL;
if (!DB_URL) throw new Error('DATABASE_URL is required');
Testing
39. How do you test Node.js applications?
Common stack: Jest or Vitest + Supertest for HTTP.
// users.test.js
const request = require('supertest');
const app = require('./app');
describe('GET /users/:id', () => {
it('returns 200 for existing user', async () => {
const res = await request(app).get('/users/1');
expect(res.status).toBe(200);
expect(res.body).toHaveProperty('name');
});
it('returns 404 for unknown user', async () => {
const res = await request(app).get('/users/9999');
expect(res.status).toBe(404);
});
});
40. How do you mock modules in Jest?
jest.mock('./db', () => ({
findUser: jest.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
}));
const { findUser } = require('./db');
test('returns user', async () => {
const user = await findUser(1);
expect(user.name).toBe('Alice');
expect(findUser).toHaveBeenCalledWith(1);
});
Advanced topics
41. What is AsyncLocalStorage and what is it used for?
AsyncLocalStorage (Node.js 16+) stores context that flows through the entire async call chain without threading it through every function argument. Ideal for request-scoped data like user IDs, trace IDs, and loggers.
const { AsyncLocalStorage } = require('async_hooks');
const storage = new AsyncLocalStorage();
// Express middleware — set context at request entry
app.use((req, res, next) => {
const ctx = { requestId: req.headers['x-request-id'] ?? crypto.randomUUID() };
storage.run(ctx, next); // all async work inside next() sees this store
});
// Deep utility function — access without parameter drilling
function log(msg) {
const ctx = storage.getStore();
console.log(`[${ctx?.requestId}] ${msg}`);
}
42. What is the difference between Buffer and Uint8Array?
Buffer is a Node.js subclass of Uint8Array with extra convenience methods for encoding/decoding.
// Create
const buf = Buffer.from('hello', 'utf8');
const hex = buf.toString('hex'); // 68656c6c6f
const b64 = buf.toString('base64'); // aGVsbG8=
// Allocation
Buffer.alloc(10); // zero-filled (safe)
Buffer.allocUnsafe(10); // faster but uninitialized (may contain old data)
// Concatenate
const result = Buffer.concat([buf1, buf2]);
Use Buffer for binary protocols, file content, crypto, and network data.
43. How does Node.js handle the require of a JSON file?
require('./config.json') parses the JSON and returns a plain JS object. It's cached like any other module.
const config = require('./config.json'); // { port: 3000, debug: true }
console.log(config.port); // 3000
In ESM, use import with assert { type: 'json' } (or with { type: 'json' } in newer Node):
import config from './config.json' with { type: 'json' };
44. What is libuv and what role does it play?
libuv is the C library that powers Node.js's event loop, async I/O, timers, and the thread pool. It provides a platform-agnostic interface to OS async APIs (epoll on Linux, kqueue on macOS, IOCP on Windows).
Key responsibilities:
- Event loop phases and scheduling
- Thread pool for blocking operations
- Timers (
setTimeout,setInterval) - TCP/UDP sockets, DNS, file system, child processes, signal handling
45. What is __proto__ vs prototype in the Node.js context?
Applies to all JS, including Node.js:
function Animal(name) { this.name = name; }
Animal.prototype.speak = function() { return `${this.name} speaks`; };
const dog = new Animal('Rex');
// dog.__proto__ === Animal.prototype (true)
// Animal.prototype.constructor === Animal (true)
console.log(dog.speak()); // Rex speaks
// Modern way
Object.getPrototypeOf(dog) === Animal.prototype; // true
46. What is the net module used for?
Low-level TCP server and socket creation — useful for custom binary protocols, proxies, and testing.
const net = require('net');
const server = net.createServer((socket) => {
socket.write('Welcome\n');
socket.on('data', (data) => {
socket.write(`Echo: ${data}`);
});
socket.on('end', () => console.log('Client disconnected'));
});
server.listen(8080, () => console.log('TCP server on :8080'));
47. How do you implement rate limiting in an Express app?
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requests per window per IP
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, slow down.' },
});
app.use('/api/', limiter);
For distributed systems, use a Redis store (rate-limit-redis) so limits are shared across cluster workers.
48. What is PM2 and why is it used?
PM2 is a production process manager for Node.js:
- Auto-restart on crash
- Cluster mode — starts one worker per CPU core automatically
- Zero-downtime reload (
pm2 reload app) - Logging and log rotation
- Startup scripts (systemd/init.d)
pm2 start app.js -i max # cluster mode, all cores
pm2 logs
pm2 monit
pm2 save && pm2 startup # persist across reboots
49. Explain the "event-driven architecture" in Node.js.
Instead of polling or blocking, Node.js components emit events and react to them. The pattern decouples producers from consumers.
const { EventEmitter } = require('events');
class PaymentService extends EventEmitter {
async charge(order) {
const result = await stripe.charge(order);
this.emit('payment:success', { order, result });
}
}
const payments = new PaymentService();
payments.on('payment:success', ({ order }) => emailService.sendReceipt(order));
payments.on('payment:success', ({ order }) => analytics.track('purchase', order));
payments.on('payment:success', ({ order }) => inventory.decrement(order.items));
Benefits: open/closed principle (add listeners without modifying PaymentService), testability, and natural async flow.
50. What are the most common Node.js performance anti-patterns?
| Anti-pattern | Problem | Fix |
|---|---|---|
Sync fs methods in server |
Blocks event loop | Use async fs.promises.* |
JSON.parse of massive files |
Freezes event loop for seconds | Use streaming JSON parser (stream-json) |
| No connection pooling | New DB connection per request | Use pg.Pool, mysql2/promise pool |
Unbound setInterval |
Memory leak, prevents exit | Store ref, call clearInterval on shutdown |
Promise.all on 1000 items |
Overwhelms external APIs | Use p-limit for bounded concurrency |
| Storing state in global vars (clustered) | Workers don't share memory | Use Redis or a DB for shared state |
| Catching errors silently | Hidden bugs, no logs | Always log + re-throw or propagate |
| Blocking in a hot path (crypto/bcrypt sync) | Degrades all concurrent requests | Use async API or offload to worker thread |
Common mistakes
| Mistake | Correct approach |
|---|---|
require inside a loop |
require is cached — still wasteful for clarity; require at top |
Using == instead of === |
Always === in JS |
| Not handling rejected Promises | Add .catch() or try/catch with await |
forEach with async callback |
Use for...of + await or Promise.all(arr.map(...)) |
res.send() after res.end() |
Check res.headersSent or return early |
Setting NODE_ENV=production wrong |
Use environment variables, not hardcoded strings |
Mutating req.params / req.query |
They're read-only; copy into a variable |
Blocking the event loop with crypto.pbkdf2Sync |
Use async variant or worker thread |
Node.js vs Deno vs Bun
| Feature | Node.js | Deno | Bun |
|---|---|---|---|
| Runtime | V8 + libuv | V8 + Tokio (Rust) | JavaScriptCore + Zig |
| Package manager | npm | built-in (URL imports + jsr) | bun (compatible with npm) |
| TypeScript | Requires tsx / ts-node |
Built-in | Built-in |
| ESM | Supported (v12+) | Default | Default |
| Permissions | No sandboxing | Explicit --allow-net etc. |
No sandboxing |
| Maturity | Highest | Stable | Growing fast |
| Best for | Production backends | Secure scripting, edge | Speed, monorepo tooling |
FAQ
Q: Is Node.js good for CPU-intensive tasks?
A: Not directly — heavy computation blocks the event loop. Use Worker Threads, cluster, or offload to a separate microservice (Go/Rust/Python).
Q: When should I use async/await vs streams?
A: async/await for small payloads where buffering is fine. Streams for large payloads (files, audio, video) where you can't afford to buffer the whole thing in memory.
Q: What version of Node.js should I use in production?
A: Always an LTS (Long-Term Support) release. LTS versions receive security patches for 30 months. Check the current LTS at nodejs.org/en/about/previous-releases.
Q: How does Node.js compare to Go or Python for web APIs?
A: Node.js excels at I/O-bound workloads (APIs, real-time, proxies) with low CPU. Go is faster for CPU-bound work and concurrency. Python has a richer data science ecosystem. For a JSON REST API with DB calls, all three are similar in practice.
Q: How do I debug a Node.js app?
A: node --inspect app.js, then open chrome://inspect in Chrome. Or use VS Code's built-in debugger (.vscode/launch.json with "type": "node").
Q: What is global in Node.js?
A: The global object (equivalent to window in browsers). Variables declared at the top level with var in CJS modules are not on global — they're scoped to the module. Use global.myVar = x to actually set a global (generally avoid this).