Dev Encyclopedia
ArticlesTools

Get notified when new content drops

No spam. Just new articles, tools, and updates straight to your inbox.

Dev Encyclopedia

A reference for builders

Content

  • Articles
  • Tools
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

© 2026 Dev Encyclopedia

Privacy PolicyTermsDisclaimer
  1. Home
  2. /Blog
  3. /30 Node.js Interview Questions and Answers (2026)
nodejs26 min read

30 Node.js Interview Questions and Answers (2026)

30 Node.js interview questions with full answers: event loop, streams, clustering, worker threads, memory leaks, and security. Updated for 2026.

By Dev EncyclopediaPublished June 8, 2026
On this page

On this page

  • Category 1: Core Concepts and Architecture (Q1-Q8)
  • Q1. What is Node.js and what makes it different from other server runtimes?
  • Q2. What is V8 and what is libuv? How do they work together?
  • Q3. What is the Node.js event loop? Explain its phases in order.
  • Q4. What is the difference between process.nextTick(), setImmediate(), and setTimeout(fn, 0)?
  • Q5. What is the difference between CommonJS (require) and ES Modules (import)?
  • Q6. How does module caching work in Node.js?
  • Q7. What is an EventEmitter and how does it work?
  • Q8. What is the difference between synchronous and asynchronous code, and what happens if you run blocking code?
  • Category 2: Async Patterns and Error Handling (Q9-Q14)
  • Q9. What is callback hell and how do you avoid it?
  • Q10. How do async/await and Promises work, and what are common mistakes?
  • Q11. What is the difference between Promise.all, Promise.allSettled, Promise.race, and Promise.any?
  • Q12. How do you handle uncaughtException and unhandledRejection?
  • Q13. What is the error-first callback pattern?
  • Q14. What is Promise chaining and when do you still use it over async/await?
  • Category 3: Streams and Buffers (Q15-Q18)
  • Q15. What are the four types of streams in Node.js?
  • Q16. How do you create and consume a Readable stream?
  • Q17. What is backpressure and how do you handle it?
  • Q18. What is a Buffer in Node.js and when do you use it instead of strings?
  • Category 4: Performance, Clustering, and Scaling (Q19-Q24)
  • Q19. What is the Cluster module and how does it work?
  • Q20. What is the difference between Cluster and Worker Threads?
  • Q21. How do you detect and fix a memory leak in Node.js?
  • Q22. What does "blocking the event loop" mean and how do you avoid it?
  • Q23. How does V8 garbage collection work in Node.js?
  • Q24. What is graceful shutdown and how do you implement it in Node.js?
  • Category 5: Security, HTTP, and Production Patterns (Q25-Q30)
  • Q25. What security headers should a Node.js HTTP API set, and how?
  • Q26. How do you implement rate limiting in Node.js?
  • Q27. How do you manage environment variables and secrets in Node.js?
  • Q28. How does CORS work in Node.js and how do you configure it?
  • Q29. How do you implement structured logging in a Node.js production app?
  • Q30. What is HTTP/2 in Node.js and what advantages does it provide?
  • Quick Reference: All 30 Questions at a Glance
  • Frequently Asked Questions

Node.js is on the required skills list for most backend and full-stack job postings in 2026. It powers APIs at Netflix, LinkedIn, Uber, and thousands of startups. But knowing how to write an Express route is the baseline. Interviewers probe deeper: how the event loop actually works, what happens when you block it, how to scale across CPU cores, how to handle memory leaks, and how to shut down a server without dropping live connections.

These 30 questions cover what interviewers ask, from junior fundamentals to senior architecture. The answers are written to be said in an interview: precise, grounded in real behavior, with working code where it helps. If you're heading into a role where Node.js sits underneath a framework like Nest, it's worth pairing this with our NestJS interview prep guide, since several of these runtime questions resurface there in framework-specific form.

If you're setting up a project to practice these concepts in, getting your Node.js project tooling right first means you're not fighting build configuration while you're trying to demo streams or graceful shutdown.

💡 How to use this guide

Skim the Quick Reference table near the bottom first to see all 30 questions and their core concept at a glance. Then jump into the categories where you feel weakest using the table of contents. Each answer ends with the kind of detail that separates a junior response from a senior one, the part worth rehearsing out loud.

Category 1: Core Concepts and Architecture (Q1-Q8)

These questions establish whether you understand what Node.js actually is under the hood, not just how to use it. They are almost always the opening questions in a Node.js interview, and a shaky answer here sets a weak tone for everything that follows.

Q1. What is Node.js and what makes it different from other server runtimes?

Node.js is a JavaScript runtime built on Chrome's V8 engine. It lets you run JavaScript on the server side. What makes it distinct is its execution model: Node.js is single-threaded and uses a non-blocking, event-driven I/O model instead of spawning a new thread per request like Apache or traditional Java servers do.

In a thread-per-request model, each incoming connection gets its own OS thread. Under heavy load you end up with hundreds or thousands of threads consuming memory and context-switching. Node.js handles all requests on one thread, delegating I/O operations (file reads, network calls, database queries) to the operating system via libuv, and continuing to process other requests while waiting for those results to come back.

This makes Node.js very efficient for I/O-bound workloads: REST APIs, real-time apps, proxies, WebSocket servers. It is not the right choice for CPU-intensive work like video encoding or scientific computation. Those block the single thread and degrade performance for everyone.

Q2. What is V8 and what is libuv? How do they work together?

V8 is Google's open-source JavaScript engine, originally built for Chrome. It compiles JavaScript to native machine code using Just-In-Time (JIT) compilation. Node.js uses V8 to execute JavaScript.

libuv is a C library that handles asynchronous I/O, the thread pool, the event loop, and platform-specific abstractions (file system, networking, DNS, timers) across Linux, macOS, and Windows.

They work together like this: V8 executes your JavaScript. When your code calls something asynchronous, fs.readFile(), https.get(), setTimeout(), Node.js delegates that work to libuv. libuv either uses the OS's async I/O primitives (epoll on Linux, kqueue on macOS) or offloads to its internal thread pool (4 threads by default, configurable with UV_THREADPOOL_SIZE). When the operation completes, libuv pushes the callback into the event loop queue and V8 executes it on the main thread.

ℹ The line interviewers listen for

V8 runs your code. libuv runs everything your code is waiting on. Saying that distinction out loud, and naming the thread pool size and its environment variable, is what separates a memorized definition from real understanding.

Q3. What is the Node.js event loop? Explain its phases in order.

The event loop is the mechanism that allows Node.js to perform non-blocking I/O on a single thread. It runs continuously, processing queued callbacks in a specific phase order.

  1. Timers: executes callbacks from setTimeout() and setInterval() whose delay has elapsed.
  2. Pending callbacks: executes I/O callbacks deferred from the previous iteration, such as TCP errors.
  3. Idle / Prepare: internal use only. You will not interact with this phase directly.
  4. Poll: retrieves new I/O events and executes their callbacks. If there are no timers scheduled, the event loop blocks here waiting for I/O. This is where most of your application work happens.
  5. Check: executes setImmediate() callbacks. Always runs after the poll phase completes.
  6. Close callbacks: executes close event handlers, for example socket.on('close').

Between every phase transition, Node.js drains two microtask queues: process.nextTick() callbacks first, then resolved Promise callbacks. This means nextTick and Promise .then() callbacks always run before the next event loop phase begins.

Q4. What is the difference between process.nextTick(), setImmediate(), and setTimeout(fn, 0)?

This is one of the most asked Node.js interview questions. The answer comes down to when in the event loop each callback executes.

APIWhen it firesNotes
process.nextTick()Before the next event loop phase startsMicrotask queue, drains completely after every phase. Recursive calls can starve the event loop.
Promise callbacks (.then, async/await)After the nextTick queue is emptyAlso a microtask, processed in a separate queue from nextTick.
setImmediate()Check phase, after the Poll phaseGuaranteed to run in the current event loop iteration's check phase.
setTimeout(fn, 0)Timers phase of the next (or current) loop iterationThe minimum delay is 1ms in Node.js, not truly 0.
javascript
console.log('start');

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

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

setImmediate(() => console.log('setImmediate'));

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

console.log('end');

// Output order:
// start
// end
// nextTick
// Promise.then
// setTimeout 0  (or setImmediate first, the order between these two
//                depends on when the event loop starts and OS timing)
// setImmediate

💡 The reliable rule

nextTick runs before Promise.then, which runs before setImmediate, which runs at roughly the same point as setTimeout(fn, 0). Memorize that ordering. It is the single most common whiteboard question in Node.js interviews.

Q5. What is the difference between CommonJS (require) and ES Modules (import)?

Node.js supports two module systems. CommonJS (CJS) is the original Node.js module system. You use require() to load modules and module.exports to export. Files use the .js or .cjs extension. Modules are loaded synchronously, require() blocks until the file is read, and module resolution happens at runtime.

javascript — commonjs-example.js
// CommonJS
const path = require('path');
const { readFile } = require('fs/promises');
module.exports = { myFunction };

ES Modules (ESM) is the JavaScript standard module system, supported natively in Node.js since v12 and stable since v16. You use import/export syntax. Files use .mjs, or .js with "type": "module" in package.json. Modules are loaded asynchronously and statically: imports are resolved before code executes, which enables tree shaking and top-level await.

javascript — esm-example.mjs
// ESM
import { readFile } from 'fs/promises';
import path from 'path';
export function myFunction() {}
export default class MyClass {}
  • ESM cannot use require() directly: use createRequire() if you need it
  • ESM supports top-level await; CJS does not
  • __dirname and __filename are not available in ESM: use import.meta.url instead
  • Most npm packages still ship CJS, so mixing the two requires care

Q6. How does module caching work in Node.js?

When you require() a module for the first time, Node.js loads the file, executes it, and caches the result in require.cache. Every subsequent require() call for the same absolute file path returns the cached export object immediately. The module code does not execute again.

javascript — counter.js
// counter.js
let count = 0;
module.exports = {
  increment: () => ++count,
  get: () => count,
};

// main.js
const a = require('./counter');
const b = require('./counter'); // same reference, not a new instance

a.increment();
console.log(b.get()); // 1, same object
console.log(a === b); // true

This matters in two ways. First, it improves performance: file I/O only happens once per module. Second, it means modules behave like singletons. If you export a database connection from a module, every file that requires it gets the same connection. This is intentional and useful. If you want fresh instances each time, export a factory function instead.

You can inspect or clear the cache via require.cache, though deleting cache entries manually is almost never the right solution outside of testing.

Q7. What is an EventEmitter and how does it work?

EventEmitter is a Node.js core class that implements the observer (pub/sub) pattern. Objects that emit events extend EventEmitter. You register listeners with .on() and trigger them with .emit().

javascript — download-manager.js
const { EventEmitter } = require('events');

class DownloadManager extends EventEmitter {
  download(url) {
    // simulate async download
    setTimeout(() => {
      this.emit('progress', 50);
      setTimeout(() => {
        this.emit('done', { url, size: 1024 });
      }, 500);
    }, 500);
  }
}

const manager = new DownloadManager();

manager.on('progress', (percent) => {
  console.log(`Download ${percent}% complete`);
});

manager.on('done', ({ url, size }) => {
  console.log(`Downloaded ${url} (${size} bytes)`);
});

manager.download('https://example.com/file.zip');
  • .on(event, listener): register a persistent listener
  • .once(event, listener): register a listener that fires once then removes itself
  • .off(event, listener) or .removeListener(): remove a listener
  • .emit(event, ...args): synchronously call all listeners for the event
  • .listenerCount(event): check how many listeners are registered

Many Node.js core objects extend EventEmitter: streams, http.Server, child processes, and more. Recognizing that pattern is what makes the rest of the Node.js API feel consistent rather than arbitrary.

Q8. What is the difference between synchronous and asynchronous code, and what happens if you run blocking code?

Synchronous code executes on the main thread and blocks everything else until it finishes. Asynchronous code starts an operation, registers a callback, and returns immediately. Node.js moves on to other work and calls the callback when the operation completes.

The problem with blocking synchronous code is that Node.js is single-threaded. If your route handler calls fs.readFileSync() to read a 50MB log file, the entire server is frozen for the duration of that read. No other requests can be processed. Response times for every user spike at once.

javascript
// BLOCKS the event loop -- every request waits for this
app.get('/config', (req, res) => {
  const data = fs.readFileSync('./config.json', 'utf8'); // bad
  res.json(JSON.parse(data));
});

// NON-BLOCKING -- other requests process while the file reads
app.get('/config', async (req, res) => {
  const data = await fs.promises.readFile('./config.json', 'utf8'); // good
  res.json(JSON.parse(data));
});

⚠ What blocks the event loop

Synchronous file, crypto, and zlib operations, JSON.parse on very large payloads, long-running loops, and complex regex on untrusted input. Keep route handlers lightweight and delegate heavy work to worker threads or external services. Q22 in this guide goes deeper on detection and fixes.

Category 2: Async Patterns and Error Handling (Q9-Q14)

Async correctness is where mid-level candidates separate from senior ones. Anyone can write an async function. Fewer people can explain why a particular pattern is wrong, or what happens when an awaited call rejects and nothing catches it.

Q9. What is callback hell and how do you avoid it?

Callback hell is the pattern that emerges when you nest multiple asynchronous callbacks to sequence operations. Each operation's result is available only inside its callback, so dependent operations nest deeper and deeper.

javascript
// Callback hell
fs.readFile('user.json', 'utf8', (err, userData) => {
  if (err) return handleError(err);
  db.findUser(JSON.parse(userData).id, (err, user) => {
    if (err) return handleError(err);
    sendEmail(user.email, 'Welcome', (err) => {
      if (err) return handleError(err);
      console.log('Done');
    });
  });
});

The problems: error handling is duplicated at every level, the code reads right-to-rightward rather than top-to-bottom, and refactoring is painful.

  1. async/await: flat, linear, try/catch for error handling. The default choice in modern code.
  2. Promises with .then() chains: better than nested callbacks, more verbose than await.
  3. Named functions: break callbacks into named top-level functions to reduce nesting visually, useful when you cannot refactor to async/await yet.

Q10. How do async/await and Promises work, and what are common mistakes?

A Promise is an object representing the eventual result of an async operation. It has three states: pending, fulfilled, or rejected. async/await is syntax that makes Promise-based code read like synchronous code.

javascript
// Promise-based
function fetchUser(id) {
  return fetch(`/api/users/${id}`)
    .then(res => {
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.json();
    });
}

// async/await equivalent
async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

Interviewers probe for the same handful of mistakes over and over. If you've spent time on the front end too, these will look familiar: our guide to common async/await mistakes in JavaScript covers the same traps from the browser side, and the underlying reasoning carries over directly to Node.js.

javascript — mistake-1-sequential-vs-parallel.js
// Slow: fetches one at a time
const user = await getUser(id);
const posts = await getPosts(id); // waits for user unnecessarily

// Fast: parallel
const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);
javascript — mistake-2-await-in-foreach.js
// forEach ignores returned Promises -- callbacks run but are not awaited
items.forEach(async (item) => await process(item)); // WRONG

// Use for...of for sequential processing
for (const item of items) await process(item);

// Or Promise.all with map for parallel processing
await Promise.all(items.map(item => process(item)));
javascript — mistake-3-missing-try-catch.js
async function handler(req, res) {
  const data = await fetchData(); // if this rejects, it becomes an
  res.json(data);                 // unhandled rejection
}

// Correct
async function handler(req, res) {
  try {
    const data = await fetchData();
    res.json(data);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
}

Q11. What is the difference between Promise.all, Promise.allSettled, Promise.race, and Promise.any?

All four accept an array of Promises and return a single Promise. They differ in how they handle rejections and when they resolve.

CombinatorResolves whenRejects whenBest for
Promise.allAll promises fulfillAny promise rejects (fail-fast)Parallel operations where every result is required
Promise.allSettledAll promises settle (fulfill or reject)Never rejectsParallel operations where you want every result regardless of failures
Promise.raceThe first promise settles, fulfilled or rejectedThe first promise settles as a rejectionImplementing timeouts
Promise.anyThe first promise fulfillsAll promises reject (AggregateError)Trying multiple sources and taking the first success
javascript — promise-allsettled.js
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
results.forEach(r => {
  if (r.status === 'fulfilled') use(r.value);
  else logError(r.reason);
});
javascript — promise-race-timeout.js
const result = await Promise.race([
  fetchData(),
  new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000))
]);

Q12. How do you handle uncaughtException and unhandledRejection?

unhandledRejection fires when a Promise rejects and no .catch() or try/catch handles it. uncaughtException fires when a synchronous throw propagates outside any try/catch.

javascript
process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled rejection', { reason, promise });
  // Do NOT keep running -- the app is in an unknown state
  process.exit(1);
});

process.on('uncaughtException', (err) => {
  logger.error('Uncaught exception', { err });
  // Log, then exit. Use a process manager (PM2, systemd) to restart.
  process.exit(1);
});

🚫 Do not try to recover and keep running

An uncaught exception means your application reached a state you did not anticipate. The safest response is to log the error, finish serving any in-flight requests if possible, and exit so a process manager (PM2, systemd, Kubernetes) restarts the process cleanly. Attempting to continue from an unknown state leads to corrupted data and unpredictable behavior. This is the single most important point to land in this answer.

Q13. What is the error-first callback pattern?

Error-first, also called errback or Node-style callback, is a convention for callback-based async functions. The first argument of every callback is an error object: null or undefined if the operation succeeded, an Error instance if it failed. The remaining arguments carry the result.

javascript
fs.readFile('./data.json', 'utf8', function(err, data) {
  if (err) {
    // handle error -- always check first
    console.error('Read failed:', err.message);
    return;
  }
  // no error -- process the data
  console.log(JSON.parse(data));
});

This pattern was established before Promises existed. It allowed consistent error handling across all async operations without exceptions. You will encounter it in many older Node.js APIs and npm packages. util.promisify() converts any error-first callback function into a Promise-based one.

javascript
const { promisify } = require('util');
const readFile = promisify(fs.readFile);

const data = await readFile('./data.json', 'utf8');

Q14. What is Promise chaining and when do you still use it over async/await?

Promise chaining calls .then() and .catch() in sequence. Each .then() handler receives the resolved value of the previous promise and returns a new promise.

javascript
fetchUser(id)
  .then(user => fetchPosts(user.id))
  .then(posts => formatPosts(posts))
  .then(formatted => res.json(formatted))
  .catch(err => res.status(500).json({ error: err.message }));

async/await is preferred in most cases because it reads like synchronous code and handles errors with familiar try/catch. But there are cases where chaining is still cleaner: stream-style transformations where each .then() maps the value, situations where you want a single .catch() to handle errors from any step without wrapping the entire thing in try/catch, and utility functions that return Promises rather than being async themselves.

In practice, modern Node.js codebases use async/await for most logic and Promises directly when working with combinators like Promise.all, or when building library functions that hand a Promise back to the caller.

Category 3: Streams and Buffers (Q15-Q18)

Streams come up whenever an interview turns to handling large files, proxying HTTP responses, or processing data that doesn't fit comfortably in memory. They're also a reliable way for an interviewer to check whether you've actually built something beyond CRUD endpoints.

Q15. What are the four types of streams in Node.js?

Streams let you work with data as it arrives rather than loading everything into memory first. There are four types.

Stream typeWhat it doesExamples
ReadableA source you can read data fromfs.createReadStream(), http.IncomingMessage (request body), process.stdin
WritableA destination you can write data tofs.createWriteStream(), http.ServerResponse, process.stdout
DuplexBoth readable and writable, with independent sidesnet.Socket (a TCP socket): you read incoming data and write outgoing data on the same socket
TransformA duplex stream where the output is derived from the input as it passes throughzlib.createGzip(), crypto.createCipheriv(), a JSON parsing stream
javascript — gzip-pipeline.js
// Real-world transform: gzip a large file
const { createReadStream, createWriteStream } = require('fs');
const { createGzip } = require('zlib');

createReadStream('large-file.csv')
  .pipe(createGzip())
  .pipe(createWriteStream('large-file.csv.gz'))
  .on('finish', () => console.log('Compressed'));

The pipe() method connects a readable to a writable and handles the data flow, including pausing the source when the destination is full. That pausing behavior is backpressure, covered in Q17.

Q16. How do you create and consume a Readable stream?

There are two approaches: using the built-in factory functions, or creating a custom Readable by extending the class.

javascript — readable-stream-factory.js
const { createReadStream } = require('fs');

const stream = createReadStream('./large-file.txt', {
  encoding: 'utf8',
  highWaterMark: 64 * 1024, // 64KB chunks
});

stream.on('data', (chunk) => {
  process.stdout.write(chunk);
});

stream.on('end', () => console.log('\nDone'));
stream.on('error', (err) => console.error('Stream error:', err));
javascript — custom-readable-stream.js
const { Readable } = require('stream');

class NumberStream extends Readable {
  constructor(max) {
    super({ objectMode: true });
    this.current = 1;
    this.max = max;
  }

  _read() {
    if (this.current <= this.max) {
      this.push(this.current++);
    } else {
      this.push(null); // signals end of stream
    }
  }
}

const numbers = new NumberStream(5);
numbers.on('data', n => console.log(n)); // 1 2 3 4 5

⚠ Always attach an error handler

An unhandled stream error is an uncaught exception and will crash the process. Every stream you create or consume needs an .on('error', ...) handler, even if all it does is log and clean up.

Q17. What is backpressure and how do you handle it?

Backpressure occurs when a readable stream produces data faster than the writable stream can consume it. Without handling it, data accumulates in memory buffers until the process runs out of RAM and crashes.

pipe() handles backpressure automatically. When the writable stream's buffer is full, pipe() pauses the readable and resumes it when the buffer drains.

javascript
// pipe handles backpressure for you
readable.pipe(transform).pipe(writable);

When you write manually without pipe(), you must check the return value of .write() and pause and resume accordingly.

javascript
readable.on('data', (chunk) => {
  const canContinue = writable.write(chunk);
  if (!canContinue) {
    // Buffer is full -- pause the source
    readable.pause();
    writable.once('drain', () => {
      // Buffer drained -- resume the source
      readable.resume();
    });
  }
});

The modern approach uses the pipeline() utility from stream/promises, which handles backpressure and cleans up all streams on error.

javascript
const { pipeline } = require('stream/promises');

await pipeline(
  fs.createReadStream('input.csv'),
  createTransformStream(),
  fs.createWriteStream('output.csv')
);

Q18. What is a Buffer in Node.js and when do you use it instead of strings?

A Buffer is a fixed-size block of raw memory outside the V8 heap. It represents binary data, bytes, directly. Node.js uses Buffers for all network I/O, file I/O, and cryptographic operations because those work with binary data, not Unicode strings.

javascript
// Create a buffer
const buf = Buffer.from('Hello, world!', 'utf8');
console.log(buf);            // <Buffer 48 65 6c 6c 6f 2c ...>
console.log(buf.length);     // 13 (bytes, not characters)
console.log(buf.toString()); // 'Hello, world!'

// Allocate a zeroed buffer
const empty = Buffer.alloc(1024);

// Read raw bytes
const first = buf[0]; // 72 (ASCII 'H')
  • Working with binary protocols: TCP, WebSocket frames, binary file formats
  • Performing crypto operations: hashing, encryption
  • Receiving data from streams before you know the encoding
  • Comparing data at the byte level for performance

Use strings when the data is text and the encoding is known. Converting between Buffer and string has a cost, so avoid unnecessary back-and-forth in hot paths. Buffer.alloc() is safe because it zeroes the memory. Buffer.allocUnsafe() is faster but may contain old memory contents, so only use it when you will immediately overwrite every byte.

Category 4: Performance, Clustering, and Scaling (Q19-Q24)

This category is where senior interviews really begin. Anyone can build a working API. These questions check whether you can keep one running under load, across cores, and over months of uptime without leaking memory or freezing under traffic.

Q19. What is the Cluster module and how does it work?

Node.js runs on a single thread. On a server with 8 CPU cores, a single Node.js process uses only one of them. The cluster module solves this by forking the main process into multiple worker processes, each running its own event loop on its own CPU core. All workers share the same server port.

javascript — cluster-server.js
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) => {
    console.log(`Worker ${worker.process.pid} died. Forking replacement.`);
    cluster.fork(); // replace dead workers automatically
  });

} else {
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello from worker ' + process.pid);
  }).listen(3000);

  console.log(`Worker ${process.pid} started`);
}

Each worker has its own memory space. They do not share variables. Shared state such as sessions or caches must live in an external store like Redis. The primary process distributes incoming connections to workers using round-robin scheduling by default.

Q20. What is the difference between Cluster and Worker Threads?

Both allow Node.js to use multiple CPU cores, but they solve different problems and have different architectures.

ClusterWorker Threads
ArchitectureMultiple independent Node.js processes, each with its own V8 instance and event loopMultiple JavaScript threads within the same process
MemoryFully isolated; nothing is sharedShared via SharedArrayBuffer; zero-copy transfer with Transferable objects
CommunicationIPC message passing between primary and workerspostMessage between main thread and worker threads
Best forScaling an HTTP server across cores to handle more concurrent connectionsCPU-intensive work within a single request: image processing, compression, crypto, ML inference
javascript — worker-threads-sum.js
// Worker Threads -- CPU-intensive task off the main thread
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  const worker = new Worker(__filename, {
    workerData: { numbers: Array.from({ length: 1e7 }, (_, i) => i) }
  });
  worker.on('message', (result) => console.log('Sum:', result));
  worker.on('error', console.error);
} else {
  // This runs in the worker thread
  const sum = workerData.numbers.reduce((a, b) => a + b, 0);
  parentPort.postMessage(sum);
}

💡 The senior-level answer

In practice, a production Node.js server often uses both: cluster for horizontal scaling across cores, and worker threads within each cluster worker for heavy per-request computations. Naming that combination unprompted is a strong signal you've operated something like this in production.

Q21. How do you detect and fix a memory leak in Node.js?

A memory leak is when objects are retained in memory after they are no longer needed, preventing garbage collection. In Node.js, the heap grows over time and the process eventually runs out of memory and crashes.

  1. 1

    Watch heap usage over time

    Track process.memoryUsage().heapUsed in production. If it grows steadily over hours without leveling off, even under steady traffic, you have a leak.

    javascript
    // Quick check -- log memory every 30 seconds
    setInterval(() => {
      const mem = process.memoryUsage();
      console.log({
        heapUsed: Math.round(mem.heapUsed / 1024 / 1024) + 'MB',
        heapTotal: Math.round(mem.heapTotal / 1024 / 1024) + 'MB',
      });
    }, 30_000);
  2. 2

    Take and compare heap snapshots

    Use the --inspect flag with Chrome DevTools' memory profiler to take heap snapshots and compare them over time. Objects that grow in count between snapshots without ever shrinking are your leak candidates.

  3. 3

    Reach for production-grade profiling tools

    The clinic.js suite, specifically clinic doctor and clinic heapprofiler, gives you flame graphs and allocation timelines that are far easier to read under real production load than raw DevTools snapshots.

  4. 4

    Match the symptom to a known cause

    • Global arrays or maps that grow forever: add eviction logic or use a bounded cache such as lru-cache
    • Event listener accumulation: calling .on() in a loop without .off(). Check with emitter.listenerCount() and prefer .once() for one-time listeners
    • Closures holding large objects: a closure captures its entire outer scope. A timer callback or long-lived function that closes over a large object will not be garbage collected until the closure itself is released
    • Unresolved Promises: a Promise that never settles holds its closure alive indefinitely. Always ensure every Promise resolves or rejects

Q22. What does "blocking the event loop" mean and how do you avoid it?

The event loop is single-threaded. If your JavaScript code runs for a long time without returning control, the event loop cannot process other callbacks: no new requests, no timers, no I/O completions. The entire server freezes.

javascript
// BLOCKS for 5 seconds -- all requests stall
app.get('/bad', (req, res) => {
  const start = Date.now();
  while (Date.now() - start < 5000) {} // busy loop
  res.send('done');
});

Common culprits: JSON.parse() or JSON.stringify() on very large payloads (over roughly 1MB), synchronous file, crypto, or zlib operations like fs.readFileSync, long-running loops or recursive computations, and complex regular expressions run against untrusted input, which opens the door to ReDoS attacks.

  • Use the async version of every I/O API, never the Sync variant, in request handlers
  • Break large synchronous computations into smaller chunks and yield between them with setImmediate()
  • Move CPU-intensive work to Worker Threads (see Q20)
  • Stream large payloads instead of loading them fully into memory before processing
  • Set a reasonable request body size limit so an attacker cannot force a huge JSON.parse

Q23. How does V8 garbage collection work in Node.js?

V8 uses a generational garbage collector. Memory is divided into two regions. New space, the young generation, is small and fast. New objects are allocated here, and most objects die young: created in a request handler, used, then discarded. V8 runs a minor GC called Scavenge frequently and quickly to collect dead objects in new space.

Old space, the old generation, is larger. Objects that survive two minor GCs are promoted there. The major GC, Mark-Sweep-Compact, runs less frequently and is more expensive: it marks all live objects, sweeps the dead ones, and optionally compacts memory to reduce fragmentation. V8 also uses incremental and concurrent marking, running GC work on background threads, to reduce pause times.

  • Short-lived objects, like request-scoped variables, are cheap to allocate. The fast minor GC handles them
  • Long-lived objects, like caches and connection pools, accumulate in old space and trigger expensive major GCs. Keep old-space objects small and bounded
  • You cannot force garbage collection in production code, but you can observe it in tests with the --expose-gc flag

Q24. What is graceful shutdown and how do you implement it in Node.js?

Graceful shutdown means stopping the server cleanly: stop accepting new connections, finish processing in-flight requests, close database connections and file handles, then exit. Without it, an abrupt exit drops active requests and can corrupt database state.

javascript — graceful-shutdown.js
const server = http.createServer(app);
let isShuttingDown = false;

server.listen(3000, () => console.log('Server started'));

async function shutdown(signal) {
  if (isShuttingDown) return;
  isShuttingDown = true;
  console.log(`Received ${signal}. Starting graceful shutdown.`);

  // Stop accepting new connections
  server.close(async () => {
    console.log('HTTP server closed');

    try {
      // Close database connections, flush logs, etc.
      await db.end();
      await redisClient.quit();
      console.log('All connections closed. Exiting.');
      process.exit(0);
    } catch (err) {
      console.error('Error during shutdown:', err);
      process.exit(1);
    }
  });

  // Force exit if cleanup takes too long
  setTimeout(() => {
    console.error('Graceful shutdown timed out. Forcing exit.');
    process.exit(1);
  }, 10_000);
}

process.on('SIGTERM', () => shutdown('SIGTERM')); // sent by Kubernetes, systemd
process.on('SIGINT', () => shutdown('SIGINT'));   // Ctrl+C during development

ℹ Why interviewers care about this one

SIGTERM is what Kubernetes and process managers send before killing a process. Handling it correctly, including the force-exit timeout so a stuck shutdown doesn't hang a deploy forever, is what separates a production-grade service from a demo. It's a question that quietly tests whether you've actually operated something in production.

Category 5: Security, HTTP, and Production Patterns (Q25-Q30)

The final stretch covers the operational layer: the headers, limits, and conventions that keep an API safe and observable once it's serving real traffic. These questions are common in interviews for roles that own a service end to end, not just its feature code.

Q25. What security headers should a Node.js HTTP API set, and how?

Security headers are the first line of defense against a class of HTTP-based attacks. The standard approach in Express is the helmet package, which sets sane defaults for the most important headers.

javascript
const helmet = require('helmet');
app.use(helmet());
HeaderWhat it prevents
Content-Security-PolicyRestricts which sources the browser can load scripts, styles, and images from. Blocks injected script execution (XSS).
X-Frame-Options: DENYPrevents the page from being embedded in an iframe. Blocks clickjacking.
X-Content-Type-Options: nosniffStops browsers from MIME-sniffing a response away from its declared Content-Type, closing off certain XSS vectors.
Strict-Transport-Security (HSTS)Tells browsers to only connect via HTTPS for a set duration. Prevents SSL-stripping attacks.
X-DNS-Prefetch-Control: offLimits information leakage via DNS prefetching.
Referrer-Policy: no-referrerControls how much referrer information is sent with requests, reducing information leakage.

For APIs that do not serve HTML, CSP is less critical, but the other headers still apply. Customize Content-Security-Policy if you serve a frontend from the same origin.

Q26. How do you implement rate limiting in Node.js?

Rate limiting prevents abuse by capping how many requests a client can make in a time window. For Express, express-rate-limit is the standard solution.

javascript
const rateLimit = require('express-rate-limit');

// General API limiter
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                   // 100 requests per window per IP
  standardHeaders: true,      // include RateLimit-* headers in responses
  legacyHeaders: false,
  message: { error: 'Too many requests. Try again in 15 minutes.' },
});

// Stricter limiter for auth endpoints
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 10,                   // 10 attempts per hour per IP
  skipSuccessfulRequests: true, // only count failed attempts
});

app.use('/api/', apiLimiter);
app.use('/auth/login', authLimiter);
app.use('/auth/forgot-password', authLimiter);

For distributed systems running multiple Node.js instances, the default in-memory store does not share state across processes. Use a Redis store instead so every instance enforces the same limit.

javascript
const RedisStore = require('rate-limit-redis');
const limiter = rateLimit({
  store: new RedisStore({ client: redisClient }),
  windowMs: 15 * 60 * 1000,
  max: 100,
});

Q27. How do you manage environment variables and secrets in Node.js?

Environment variables keep secrets out of your source code. The standard local development tool is dotenv.

javascript
// Load .env in development only -- never in production
if (process.env.NODE_ENV !== 'production') {
  require('dotenv').config();
}

const dbUrl = process.env.DATABASE_URL;
const jwtSecret = process.env.JWT_SECRET;

if (!dbUrl || !jwtSecret) {
  throw new Error('Missing required environment variables');
}
  • Validate required variables at startup, failing fast with a clear error rather than crashing later with a cryptic undefined
  • Never commit .env files. Add .env to .gitignore and commit a .env.example with placeholder values instead
  • Use a secrets manager in production: AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, or Doppler. These rotate secrets, audit access, and avoid keeping secrets in environment variables at all
  • Prefer fetching sensitive values from a secrets manager at startup over reading them from environment variables, which can be accidentally logged
  • Never log process.env. A middleware that logs all headers or the environment can expose secrets without anyone noticing

Q28. How does CORS work in Node.js and how do you configure it?

CORS, Cross-Origin Resource Sharing, is a browser security mechanism. When a browser makes a request to a different origin (protocol, domain, and port) than the page it's on, it adds an Origin header. If the server does not explicitly allow that origin, the browser blocks the response.

For APIs, the cors npm package handles this cleanly.

javascript
const cors = require('cors');

// Allow a specific list of origins
const allowedOrigins = process.env.ALLOWED_ORIGINS.split(',');

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (mobile apps, Postman, curl)
    if (!origin) return callback(null, true);
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error(`CORS: origin ${origin} not allowed`));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true, // required if the frontend sends cookies or auth headers
  maxAge: 86400,     // cache preflight response for 24 hours
}));

⚠ The mistake that fails interviews

Never use cors({ origin: '' }) together with credentials: true. The browser will reject it outright. Always specify allowed origins explicitly in production. For public APIs that involve no auth and no cookies, origin: '' is fine.

Q29. How do you implement structured logging in a Node.js production app?

console.log() is not sufficient for production. It has no log levels, no structured output, and no way to query or aggregate logs at scale. The standard libraries for production logging are Winston, and Pino when you need maximum throughput.

javascript — logger.js
const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }), // include stack traces
    process.env.NODE_ENV === 'production'
      ? winston.format.json()               // JSON for log aggregators
      : winston.format.prettyPrint()        // readable in development
  ),
  defaultMeta: {
    service: 'api-service',
    version: process.env.APP_VERSION,
  },
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
  ],
});

// Use structured fields, not string concatenation
logger.info('Request processed', {
  method: req.method,
  path: req.path,
  statusCode: res.statusCode,
  durationMs: Date.now() - startTime,
  userId: req.user?.id,
});

logger.error('Database query failed', {
  error: err.message,
  stack: err.stack,
  query: sanitizedQuery,
});

JSON log format makes logs queryable in aggregators like Datadog, CloudWatch, or the ELK stack. Never log sensitive data: passwords, tokens, or personally identifiable information. Use a correlation ID, a request ID, in every log line so you can trace a single request across services.

Q30. What is HTTP/2 in Node.js and what advantages does it provide?

HTTP/2 is the second major version of the HTTP protocol. Node.js supports it natively via the http2 module. The main advantages over HTTP/1.1 are multiplexing, where multiple requests and responses share a single TCP connection simultaneously, eliminating the head-of-line blocking that forces browsers to open six connections per domain in HTTP/1.1; header compression via HPACK, which reduces overhead for APIs that send many repeated headers like Authorization and Content-Type; server push, where the server can proactively send resources to the client before they're requested; and stream prioritization, which lets clients signal which responses matter most.

javascript
const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.crt'),
});

server.on('stream', (stream, headers) => {
  const path = headers[':path'];

  stream.respond({
    'content-type': 'application/json',
    ':status': 200,
  });

  stream.end(JSON.stringify({ path, timestamp: Date.now() }));
});

server.listen(8443);

HTTP/2 requires HTTPS (TLS). In production, HTTP/2 is typically terminated at the reverse proxy, nginx, Caddy, or AWS ALB, rather than in Node.js directly. That simplifies certificate management and lets you take advantage of the proxy's optimized HTTP/2 implementation.

Quick Reference: All 30 Questions at a Glance

Use this as a final scan the night before an interview. If any row makes you pause, jump back up to that question's full answer.

#QuestionCore concept
1What is Node.js?V8 + libuv, non-blocking I/O, single thread
2What is V8 and libuv?JS engine + async I/O library
3What is the event loop? Phases in order.6 phases, microtask queues drain between each
4nextTick vs setImmediate vs setTimeout(0)Execution order across the event loop
5CommonJS vs ES Modulesrequire vs import, sync vs static async loading
6How does module caching work?Singleton pattern via require.cache
7What is EventEmitter?Observer pattern: .on(), .emit(), .once()
8Sync vs async, blocking the event loopSingle-thread impact, always use async APIs
9Callback hell and how to avoid itNesting problem; async/await is the fix
10async/await and common mistakesSequential vs parallel, the forEach trap
11Promise.all vs allSettled vs race vs anyConcurrency combinators, when to use each
12unhandledRejection and uncaughtExceptionLog it, then exit; let the process manager restart
13Error-first callback pattern(err, result) convention, util.promisify
14Promise chaining vs async/awaitWhen chaining still earns its place
15Four types of streamsReadable, Writable, Duplex, Transform
16Creating and consuming a Readable streamEvents, pipe, the custom Readable class
17Backpressure in streamspipe() handles it; manual pause/resume/drain
18Buffer vs stringBinary data, alloc vs allocUnsafe
19Cluster moduleFork per CPU core, shared port, IPC
20Cluster vs Worker ThreadsMultiple processes vs multiple threads
21Detecting and fixing memory leaksheapUsed monitoring, heap snapshots, clinic.js
22Blocking the event loopCauses and how to avoid each one
23V8 garbage collectionGenerational GC: new space vs old space
24Graceful shutdownSIGTERM handler, server.close(), force-exit timeout
25Security headers with HelmetCSP, HSTS, X-Frame-Options, nosniff
26Rate limitingexpress-rate-limit, Redis store for clusters
27Environment variables and secretsdotenv, startup validation, secrets managers
28CORS configurationcors package, allowed origins, credentials
29Production logging with WinstonStructured JSON logs, log levels, never log PII
30HTTP/2 in Node.jsMultiplexing, HPACK, server push

💡 Five things to memorize before you walk in

The event loop phase order and where microtasks drain (Q3 and Q4), the rule that you log and exit on uncaughtException rather than trying to recover (Q12), that pipe() and pipeline() handle backpressure so you rarely write manual pause/resume logic (Q17), the difference between Cluster (processes, no shared memory) and Worker Threads (threads, shared memory) (Q20), and the SIGTERM handler shape for graceful shutdown (Q24).

Frequently Asked Questions

What level of Node.js knowledge do these interview questions target?

This guide spans junior fundamentals through senior architecture. Questions 1 through 14 cover the core runtime model, modules, and async patterns that any Node.js role expects you to know cold. Questions 15 through 30 move into streams, clustering, memory management, and production operations, the territory where mid-level and senior candidates are differentiated.

If you're early in your Node.js journey, start with Category 1 and Category 2 and come back to clustering and memory leaks once those feel automatic. If you're interviewing for a senior or staff role, the categories on scaling, security, and production patterns are where most of your prep time should go.

How do interviewers usually test event loop knowledge beyond asking for the phase list?

The most common format is a short code snippet mixing process.nextTick(), Promise.then(), setTimeout(), and setImmediate(), and asking you to predict the output order (Q4 walks through exactly this). A weaker follow-up asks you to name the six event loop phases in order (Q3) and explain what runs in each.

The strongest candidates go further unprompted: they explain why nextTick can starve the event loop if called recursively, and why setTimeout(fn, 0) doesn't actually mean zero milliseconds. That extra layer is what separates a memorized answer from one grounded in real experience.

How does Node.js compare to a thread-per-request runtime like a traditional Java or PHP server?

The core difference is the concurrency model. Thread-per-request runtimes give every connection its own OS thread, which is straightforward to reason about but expensive at scale: thousands of concurrent connections mean thousands of threads competing for memory and CPU time through context switching.

Thread-per-request (Java, PHP)Node.js
Concurrency modelNew OS thread per connectionSingle thread, event-driven, non-blocking I/O
Memory under loadGrows linearly with concurrent connectionsStays comparatively flat for I/O-bound work
Best fitCPU-bound and compute-heavy workloadsI/O-bound workloads: APIs, real-time apps, proxies
Failure mode under loadThread exhaustion, context-switch overheadA blocked event loop freezes the entire process

Neither model is universally better. Node.js wins decisively for I/O-bound services and loses just as decisively if you put CPU-heavy work directly on its single thread, which is exactly what Q1, Q8, and Q22 in this guide are testing whether you understand.

How do I explain backpressure in an interview without sounding like I'm reciting a definition?

Ground it in a concrete scenario rather than the abstract definition. For example: you're streaming a 2GB file from disk to an HTTP response, and the client's network connection is slow. Without backpressure, Node.js would read the entire file into memory faster than the socket can send it, and the process would run out of RAM on a large enough file or enough concurrent downloads.

javascript
// pipe() pauses the read stream automatically when the
// response socket's buffer fills up, and resumes it on 'drain'
fs.createReadStream('large-video.mp4').pipe(res);

Then name the mechanism: pipe() (or the modern pipeline() utility from stream/promises) checks the return value of .write() on the destination, pauses the source when it returns false, and resumes on the 'drain' event. That's the concrete chain of events Q17 in this guide walks through, and reciting it from a real scenario reads as far more credible than reciting the definition alone.

What if I've never actually debugged a memory leak in production? How should I answer that question?

Be honest that you haven't hit one in production, then demonstrate that you know how you'd find one. Walk through the detection ladder in order: watch process.memoryUsage().heapUsed for a steady upward trend, take heap snapshots with Chrome DevTools and compare object counts between them, and reach for clinic.js when you need production-grade flame graphs (Q21 covers all three in detail).

Then name the usual suspects: unbounded caches, accumulated event listeners, closures holding large objects, and Promises that never settle. An interviewer asking this question is testing whether you have a method, not whether you've personally lived through an incident. A clear method, stated calmly, often lands better than a war story that wanders.

Related Articles

javascript

5 async/await Mistakes That Slow Your JavaScript Code

Sequential awaits, await in forEach, missing Promise.all: these 5 async/await mistakes silently slow your JavaScript. Here's how to spot and fix each one.

May 30, 2026·8 min read
javascript

npm Scripts You're Probably Not Using (But Should Be)

pre/post hooks, cross-env, npm-run-all, argument passing, and built-in variables: the npm script patterns developers Google one at a time, in one place.

Jun 1, 2026·8 min read
nodejs

30 NestJS Interview Questions and Answers (2026)

30 NestJS interview questions with full answers: modules, DI, guards, pipes, interceptors, JWT auth, microservices, and testing. Updated for 2026.

Jun 8, 2026·24 min read

On this page

  • Category 1: Core Concepts and Architecture (Q1-Q8)
  • Q1. What is Node.js and what makes it different from other server runtimes?
  • Q2. What is V8 and what is libuv? How do they work together?
  • Q3. What is the Node.js event loop? Explain its phases in order.
  • Q4. What is the difference between process.nextTick(), setImmediate(), and setTimeout(fn, 0)?
  • Q5. What is the difference between CommonJS (require) and ES Modules (import)?
  • Q6. How does module caching work in Node.js?
  • Q7. What is an EventEmitter and how does it work?
  • Q8. What is the difference between synchronous and asynchronous code, and what happens if you run blocking code?
  • Category 2: Async Patterns and Error Handling (Q9-Q14)
  • Q9. What is callback hell and how do you avoid it?
  • Q10. How do async/await and Promises work, and what are common mistakes?
  • Q11. What is the difference between Promise.all, Promise.allSettled, Promise.race, and Promise.any?
  • Q12. How do you handle uncaughtException and unhandledRejection?
  • Q13. What is the error-first callback pattern?
  • Q14. What is Promise chaining and when do you still use it over async/await?
  • Category 3: Streams and Buffers (Q15-Q18)
  • Q15. What are the four types of streams in Node.js?
  • Q16. How do you create and consume a Readable stream?
  • Q17. What is backpressure and how do you handle it?
  • Q18. What is a Buffer in Node.js and when do you use it instead of strings?
  • Category 4: Performance, Clustering, and Scaling (Q19-Q24)
  • Q19. What is the Cluster module and how does it work?
  • Q20. What is the difference between Cluster and Worker Threads?
  • Q21. How do you detect and fix a memory leak in Node.js?
  • Q22. What does "blocking the event loop" mean and how do you avoid it?
  • Q23. How does V8 garbage collection work in Node.js?
  • Q24. What is graceful shutdown and how do you implement it in Node.js?
  • Category 5: Security, HTTP, and Production Patterns (Q25-Q30)
  • Q25. What security headers should a Node.js HTTP API set, and how?
  • Q26. How do you implement rate limiting in Node.js?
  • Q27. How do you manage environment variables and secrets in Node.js?
  • Q28. How does CORS work in Node.js and how do you configure it?
  • Q29. How do you implement structured logging in a Node.js production app?
  • Q30. What is HTTP/2 in Node.js and what advantages does it provide?
  • Quick Reference: All 30 Questions at a Glance
  • Frequently Asked Questions