Skip to content
Unverified — AI-generated content. Help verify this page

Node.js Interview Questions

Node.js interviews go beyond JavaScript. They test whether you understand how the runtime works — the event loop, the thread pool, memory management, and the streaming model that makes Node capable of handling thousands of concurrent connections on a single thread. This page covers the questions that matter.

Event Loop Deep Dive

Q: Explain the Node.js event loop phases in detail.

The event loop is a semi-infinite loop that processes callbacks in a specific order. Each iteration is called a "tick." There are six phases, and each phase has a FIFO queue of callbacks to execute.

PhaseCallbacks ProcessedKey Detail
TimerssetTimeout, setIntervalExecutes callbacks whose timer has elapsed. Not guaranteed to fire at exact time — just "no sooner than."
Pending CallbacksDeferred I/O callbacksSystem-level callbacks (e.g., TCP errors like ECONNREFUSED).
Idle / PrepareInternal onlyUsed by Node internals. Not accessible to user code.
PollI/O callbacksThe phase where Node spends most time. Retrieves new I/O events from the kernel and runs their callbacks. If no timers are scheduled, it will block here waiting for I/O.
ChecksetImmediateRuns immediately after the poll phase completes.
CloseClose handlerssocket.on('close', ...), process.on('exit', ...).

Between every phase, Node drains the microtask queue:

  1. process.nextTick callbacks (highest priority)
  2. Promise .then/.catch/.finally callbacks

Q: process.nextTick vs setImmediate

javascript
// process.nextTick — fires BEFORE the event loop continues
// setImmediate — fires in the CHECK phase (after poll)

setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
// Output: nextTick, setImmediate

// Recursive nextTick can STARVE the event loop
function danger() {
  process.nextTick(danger); // Never lets the loop advance to I/O
}
// This blocks all I/O, timers, and setImmediate callbacks

process.nextTick Can Starve I/O

Recursive process.nextTick prevents the event loop from reaching the poll phase. Use setImmediate for recursive/deferred work that should not block I/O.

Q: What is the output?

javascript
const fs = require('fs');

// Main module context
setTimeout(() => console.log('T1'), 0);
setImmediate(() => console.log('I1'));
// Order is NON-DETERMINISTIC — depends on whether 1ms timer
// has elapsed by the time event loop reaches the timers phase

// Inside an I/O callback — order IS deterministic
fs.readFile(__filename, () => {
  setTimeout(() => console.log('T2'), 0);
  setImmediate(() => console.log('I2'));
});
// Always prints: I2, T2
// Because: after I/O callback runs (poll phase),
// check phase (setImmediate) comes BEFORE timers phase

Q: What is libuv and why does Node need it?

libuv is the C library that provides Node.js with its event loop and thread pool. The operating system can handle some async operations natively (network I/O on Linux via epoll, macOS via kqueue), but filesystem operations, DNS lookups, and some crypto operations are blocking at the OS level. libuv runs these on a thread pool (default: 4 threads).

bash
# Increase thread pool size (default is 4)
UV_THREADPOOL_SIZE=16 node server.js

Streams

Q: Explain the four types of streams.

Stream TypeDescriptionExample
ReadableSource of data you consumefs.createReadStream, http.IncomingMessage
WritableDestination you write data tofs.createWriteStream, http.ServerResponse
DuplexBoth readable and writablenet.Socket, zlib.createGzip
TransformDuplex that modifies data passing throughzlib.createGzip, crypto.createCipheriv
javascript
const fs = require('fs');
const zlib = require('zlib');
const { pipeline } = require('stream/promises');

// Stream a file through gzip compression to another file
await pipeline(
  fs.createReadStream('input.log'),
  zlib.createGzip(),
  fs.createWriteStream('input.log.gz')
);
// Memory efficient: processes chunks, never loads entire file

// Transform stream — uppercase text
const { Transform } = require('stream');

const upperCase = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  }
});

process.stdin.pipe(upperCase).pipe(process.stdout);

Q: What is backpressure and how do you handle it?

Backpressure occurs when a writable stream cannot process data as fast as the readable stream produces it. Without handling backpressure, data accumulates in memory and causes OOM crashes.

javascript
// BAD — ignoring backpressure
const readable = fs.createReadStream('huge-file.csv');
const writable = fs.createWriteStream('output.csv');

readable.on('data', (chunk) => {
  writable.write(chunk); // Ignores return value!
  // If writable buffer is full, data accumulates in memory
});

// GOOD — respecting backpressure manually
readable.on('data', (chunk) => {
  const canContinue = writable.write(chunk);
  if (!canContinue) {
    readable.pause(); // Stop reading until writable drains
  }
});

writable.on('drain', () => {
  readable.resume(); // Buffer drained, resume reading
});

// BEST — use pipeline (handles backpressure automatically)
const { pipeline } = require('stream/promises');

await pipeline(readable, transformStream, writable);
// Also handles error propagation and cleanup

Q: Implement a custom readable stream

javascript
const { Readable } = require('stream');

class DatabaseCursor extends Readable {
  constructor(query, options = {}) {
    super({ ...options, objectMode: true });
    this.query = query;
    this.offset = 0;
    this.batchSize = 100;
  }

  async _read() {
    try {
      const rows = await db.query(this.query, {
        offset: this.offset,
        limit: this.batchSize,
      });

      if (rows.length === 0) {
        this.push(null); // Signal end of stream
        return;
      }

      for (const row of rows) {
        this.push(row);
      }

      this.offset += rows.length;
    } catch (err) {
      this.destroy(err);
    }
  }
}

// Usage — process millions of rows without loading all into memory
const cursor = new DatabaseCursor('SELECT * FROM events');

for await (const row of cursor) {
  await processEvent(row);
}

Cluster Module and Worker Threads

Q: When do you use cluster vs worker_threads?

Featureclusterworker_threads
Use caseScale HTTP servers across CPU coresCPU-intensive computation
Process modelSeparate processes (fork)Threads within one process
MemorySeparate memory per processShared memory possible (SharedArrayBuffer)
CommunicationIPC (serialized messages)MessagePort (faster, structured clone or SharedArrayBuffer)
Port sharingBuilt-in (multiple processes share one port)Not built-in
Crash isolationWorker crash does not affect masterThread crash can crash entire process
javascript
// CLUSTER — scale HTTP server to all CPU cores
const cluster = require('cluster');
const http = require('http');
const os = require('os');

if (cluster.isPrimary) {
  const numCPUs = os.cpus().length;
  console.log(`Primary ${process.pid} forking ${numCPUs} workers`);

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code) => {
    console.log(`Worker ${worker.process.pid} died (${code}). Restarting...`);
    cluster.fork(); // Auto-restart dead workers
  });
} else {
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`Handled by worker ${process.pid}\n`);
  }).listen(3000);
}
javascript
// WORKER THREADS — offload CPU-intensive work
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  // Main thread — offload heavy computation
  async function computeHash(data) {
    return new Promise((resolve, reject) => {
      const worker = new Worker(__filename, { workerData: data });
      worker.on('message', resolve);
      worker.on('error', reject);
    });
  }

  const result = await computeHash({ input: 'large-dataset' });
  console.log('Result:', result);
} else {
  // Worker thread — CPU-intensive work
  const crypto = require('crypto');
  const hash = crypto
    .createHash('sha256')
    .update(JSON.stringify(workerData))
    .digest('hex');

  parentPort.postMessage({ hash });
}

Production Recommendation

For HTTP servers, use a process manager like PM2 or the built-in cluster module. For CPU-intensive tasks in a server context, use a worker thread pool (e.g., piscina or workerpool) to avoid the overhead of spawning a new thread per request.


Memory Management and V8

Q: How does V8 manage memory?

V8 uses a generational garbage collector. Memory is divided into regions:

GC TypeTargetsAlgorithmSpeedPause
Scavenge (Minor GC)New generationSemi-space copy (Cheney's algorithm)Very fast~1ms
Mark-Sweep (Major GC)Old generationTri-color marking + sweepSlower5-50ms
Mark-CompactOld generationMark + compact (defragment)SlowestLonger
Incremental MarkingOld generationInterleaved with JS executionReduced pausesSpread out
bash
# Check heap usage
node -e "console.log(process.memoryUsage())"
# {
#   rss: 30000000,      # Resident Set Size (total OS memory)
#   heapTotal: 7000000, # V8 heap allocated
#   heapUsed: 5000000,  # V8 heap in use
#   external: 1000000,  # C++ objects bound to JS
#   arrayBuffers: 500000 # ArrayBuffer / SharedArrayBuffer
# }

# Increase old-generation heap limit (default ~1.5 GB on 64-bit)
node --max-old-space-size=4096 server.js  # 4 GB

Q: Common memory leak patterns and detection

Pattern 1 — Global accumulation:

javascript
// LEAK: array grows forever
const cache = [];

app.get('/api/data', (req, res) => {
  const result = processRequest(req);
  cache.push(result); // Never cleared
  res.json(result);
});

// FIX: use an LRU cache with a max size
const LRU = require('lru-cache');
const cache = new LRU({ max: 1000 });

Pattern 2 — Closures holding references:

javascript
// LEAK: closure retains reference to large object
function processData() {
  const hugeData = loadGigabyteFile(); // 1 GB

  return function getMetadata() {
    return { size: hugeData.length }; // Only needs .length
    // But the closure retains the entire hugeData object
  };
}

// FIX: extract what you need before closing over it
function processData() {
  const hugeData = loadGigabyteFile();
  const size = hugeData.length; // Extract needed value

  return function getMetadata() {
    return { size }; // Only closes over the number
  };
}

Pattern 3 — Event listener accumulation:

javascript
// LEAK: adding listeners without removing them
class Connector {
  connect(eventBus) {
    // New listener on each connect() call
    eventBus.on('data', this.handleData.bind(this));
    // bind() creates a NEW function each time — cannot remove
  }
}

// FIX: store reference, remove on disconnect
class Connector {
  #handler = null;

  connect(eventBus) {
    this.#handler = (data) => this.handleData(data);
    eventBus.on('data', this.#handler);
  }

  disconnect(eventBus) {
    eventBus.off('data', this.#handler);
  }
}

Detection tools:

bash
# Heap snapshot
node --inspect server.js
# Then open Chrome DevTools → Memory → Take Heap Snapshot

# Track heap growth over time
node --inspect server.js
# Memory → Allocation Timeline → Look for non-GC'd allocations

# CLI — heap dump on demand
kill -USR2 <pid>  # If using heapdump module

# Programmatic monitoring
setInterval(() => {
  const { heapUsed, heapTotal } = process.memoryUsage();
  const usedMB = (heapUsed / 1024 / 1024).toFixed(1);
  const totalMB = (heapTotal / 1024 / 1024).toFixed(1);
  console.log(`Heap: ${usedMB}MB / ${totalMB}MB`);
}, 10000);

Signs of a Memory Leak

  • heapUsed grows steadily over time without returning to a baseline
  • Response times gradually increase
  • Process OOM-killed periodically
  • Node.js emits MaxListenersExceededWarning

Buffer

Q: What is a Buffer and when do you use it?

A Buffer is a fixed-size chunk of memory allocated outside the V8 heap. It is used for binary data — file I/O, network protocols, cryptography — where JavaScript strings (UTF-16) are too slow or inappropriate.

javascript
// Creating buffers
const buf1 = Buffer.alloc(16);           // 16 bytes, zero-filled
const buf2 = Buffer.from('hello');        // From string (UTF-8)
const buf3 = Buffer.from([0x48, 0x65]);   // From byte array
const buf4 = Buffer.allocUnsafe(16);      // Uninitialized (faster, may contain old data)

// Reading and writing
buf1.writeUInt32BE(0xDEADBEEF, 0); // Write 4 bytes at offset 0
buf1.readUInt32BE(0);               // 0xDEADBEEF

// Conversion
buf2.toString('utf8');     // 'hello'
buf2.toString('hex');      // '68656c6c6f'
buf2.toString('base64');   // 'aGVsbG8='

// Comparison
Buffer.compare(buf2, Buffer.from('hello')); // 0 (equal)

// Concatenation
const combined = Buffer.concat([buf2, Buffer.from(' world')]);
combined.toString(); // 'hello world'

Buffer.allocUnsafe Is Faster But Risky

Buffer.allocUnsafe() does not zero-fill the memory, so it may contain sensitive data from previous allocations. Use it only when you will immediately overwrite all bytes.


Error Handling Patterns

Q: How should you handle errors in Node.js?

javascript
// 1. Synchronous — try/catch
try {
  JSON.parse(invalidJSON);
} catch (err) {
  console.error('Parse failed:', err.message);
}

// 2. Callbacks — error-first pattern
fs.readFile('/path', (err, data) => {
  if (err) {
    console.error('Read failed:', err.message);
    return;
  }
  processData(data);
});

// 3. Promises — .catch or try/catch with await
try {
  const data = await fs.promises.readFile('/path');
  processData(data);
} catch (err) {
  console.error('Read failed:', err.message);
}

// 4. Event emitters — 'error' event
const server = http.createServer();
server.on('error', (err) => {
  if (err.code === 'EADDRINUSE') {
    console.error('Port already in use');
    process.exit(1);
  }
});

// 5. Global handlers — last resort
process.on('uncaughtException', (err) => {
  console.error('Uncaught exception:', err);
  process.exit(1); // Must exit — state may be corrupted
});

process.on('unhandledRejection', (reason) => {
  console.error('Unhandled rejection:', reason);
  process.exit(1); // Node 15+ does this by default
});

Never Swallow uncaughtException

After an uncaught exception, the process is in an undefined state. Log the error, flush logs, and exit. A process manager (PM2, systemd) should restart it.


Security

Q: Common Node.js security issues

VulnerabilityExamplePrevention
Prototype pollutionJSON.parse of untrusted input setting __proto__Validate input, use Object.create(null), freeze prototypes
Path traversalfs.readFile(req.query.file)Validate and resolve paths, use path.resolve + check prefix
ReDoSRegex with catastrophic backtrackingUse re2 library, test regex with rxxr2, set timeouts
Command injectionexec('ls ' + userInput)Use execFile with argument arrays, never string interpolation
Denial of serviceNo request size limitSet maxBodySize, use rate limiting, timeouts
javascript
// PATH TRAVERSAL — BAD
app.get('/files/:name', (req, res) => {
  res.sendFile(req.params.name); // ../../etc/passwd
});

// PATH TRAVERSAL — GOOD
const path = require('path');
const SAFE_DIR = '/var/app/uploads';

app.get('/files/:name', (req, res) => {
  const filePath = path.resolve(SAFE_DIR, req.params.name);
  if (!filePath.startsWith(SAFE_DIR)) {
    return res.status(403).send('Forbidden');
  }
  res.sendFile(filePath);
});

// COMMAND INJECTION — BAD
const { exec } = require('child_process');
exec(`convert ${userInput} output.png`); // Shell injection!

// COMMAND INJECTION — GOOD
const { execFile } = require('child_process');
execFile('convert', [userInput, 'output.png']); // Arguments array

Quick-Fire Questions

QuestionAnswer
require vs import?require is CommonJS (synchronous, dynamic). import is ESM (asynchronous, static analysis, tree-shakeable).
What is package.json "type": "module"?Tells Node to treat .js files as ESM instead of CommonJS.
What does --experimental-permission do?Node 20+ permission model — restricts file system, child process, and worker access.
How to read env vars?process.env.MY_VAR. Use dotenv or Node 20.6+ --env-file flag.
What is AbortController?Cancellation mechanism for async operations. Pass signal to fetch, setTimeout, fs.readFile, etc.
Express vs Fastify?Fastify is ~3x faster (schema-based serialization, no regex routing). Express has larger ecosystem.
What is node: prefix?require('node:fs') — explicitly import built-in modules. Prevents name collision with npm packages.

"What I cannot create, I do not understand." — Richard Feynman