Node.js · Backend Interview Q&A

Node.js Interview Questions & Answers: 50 Questions for Backend Roles

The event loop phase by phase, output-ordering puzzles with dry runs, streams and backpressure, scaling with cluster and worker threads, Express middleware, and production error handling.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 24 min read
Illustration of a circular event loop track with work packets queued at phases and data streams flowing through pipes
TL;DR
  • Node interviews at Indian product companies (Swiggy, Razorpay, Zeta, Postman, Freshworks and many startups) are dominated by four areas: the event loop, async control flow, streams, and Express plus production error handling.
  • Know the six libuv phases, and that nextTick and promise microtasks drain after every callback. Expect at least one "predict the output" puzzle.
  • "Node is single-threaded" is half true: your JS runs on one thread, but the libuv thread pool (4 threads by default) handles fs, dns.lookup, crypto and zlib.
  • Scale HTTP across cores with cluster or more containers; offload CPU work with worker_threads; never block the loop with sync crypto or giant JSON parses.
  • Production answers win offers: graceful shutdown on SIGTERM, central error middleware, crash-and-restart on uncaught exceptions, event loop lag metrics.

This is a working set of 50 Node.js interview questions with the answers an interviewer actually wants to hear, grouped by topic. Each answer is short enough to say out loud in a minute, and where the honest answer needs code or a dry run, it is included. The questions are aimed at SDE-1 to SDE-2 backend roles, the level where interviewers stop accepting "Node is non-blocking" and start asking why and what breaks.

If you are also preparing JavaScript fundamentals (closures, this, prototypes), pair this with our JavaScript and React interview questions. If your stack is TypeScript, see the TypeScript guide after this one.

What Node.js Backend Interviews Actually Test

A typical Node.js backend loop has a DSA round, a "backend fundamentals" round, and for 2+ years of experience, an LLD or system design round. The Node-specific questions cluster in the fundamentals round and usually escalate along a predictable path:

  1. Concept check. "Explain the event loop." Anyone can recite "non-blocking I/O"; the interviewer is listening for phases, the microtask queue and the thread pool.
  2. Output puzzle. A snippet mixing setTimeout, setImmediate, promises and nextTick. You must reason step by step.
  3. Failure scenario. "Your API's p99 jumped from 80 ms to 4 s after a deploy. CPU is at 100% on one core. What happened?" This tests whether you understand blocking.
  4. Design-in-the-small. Upload a 2 GB CSV, rate-limit an endpoint, write auth middleware, shut down without dropping requests.

The questions below follow that path. Treat the code as something to run yourself: typing out the event loop puzzles once in a terminal teaches more than reading five blog posts about them.

Node.js Event Loop Interview Questions (Q1-12)

Q1. What is the event loop, in one paragraph?

The event loop is the mechanism (implemented by libuv) that lets a single JavaScript thread handle thousands of concurrent operations. Your code starts I/O (a socket read, a DB query, a file read) and registers a callback. The operating system or the libuv thread pool does the waiting. When the result is ready, libuv queues the callback, and the loop runs it on the main thread once the call stack is empty. Concurrency comes from never waiting on the main thread, not from running JS in parallel.

Q2. Name the phases of the event loop in order.

PhaseWhat runsExample
1. timersCallbacks whose setTimeout/setInterval threshold has elapsedsetTimeout(fn, 100)
2. pending callbacksSome system-level callbacks deferred from the previous iterationCertain TCP errors such as ECONNREFUSED on some systems
3. idle, prepareInternal to libuvNone you write
4. pollFetch new I/O events and run their callbacks; may block here waiting for I/O if nothing else is duefs.readFile callback, incoming socket data
5. checksetImmediate callbackssetImmediate(fn)
6. close callbacksClose eventssocket.on('close')

The detail that separates strong answers: between every individual callback, Node drains the process.nextTick queue and then the promise microtask queue (this has been the behaviour since Node 11; before that, microtasks drained only between phases). The official reference is the Node.js guide The Node.js Event Loop.

Q3. Predict the output (the classic puzzle).

JavaScript (CommonJS file)
const fs = require('fs');

console.log('A');
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));

fs.readFile(__filename, () => {
  setTimeout(() => console.log('io-timeout'), 0);
  setImmediate(() => console.log('io-immediate'));
  process.nextTick(() => console.log('io-tick'));
});

console.log('B');

Output on a typical run:

A
B
nextTick
promise
timeout      (these two may swap)
immediate
io-tick
io-immediate
io-timeout

Dry run. Synchronous code first: A, B. The main script is "one callback", so after it finishes Node drains the nextTick queue (nextTick), then promise microtasks (promise). Now the loop starts. Whether timeout or immediate prints first from the main module is not deterministic: setTimeout(fn, 0) is really 1 ms, and if the loop enters the timers phase before 1 ms has passed, the timer is not ready yet and the check phase runs immediate first. Inside the readFile callback we are in the poll phase, so the order is fixed: nextTick drains first (io-tick), then the loop moves to check (io-immediate), and only on the next iteration reaches timers (io-timeout).

⚠️
ESM caveatIn an ES module (.mjs or "type": "module"), top-level code is itself evaluated asynchronously, so promise can print before nextTick. Mentioning this unprompted is a strong signal.

Q4. process.nextTick vs setImmediate vs setTimeout(fn, 0)?

nextTick runs right after the current operation, before anything else. setImmediate runs in the check phase of the current iteration. setTimeout(fn, 0) runs in the timers phase after at least 1 ms. The names are famously backwards: nextTick is more immediate than setImmediate. Prefer setImmediate for "yield then continue" because it lets I/O run in between.

Q5. How can process.nextTick starve the event loop?

Because the nextTick queue is fully drained before the loop continues, a callback that recursively schedules another nextTick never lets the loop reach the poll phase. Incoming requests sit unprocessed forever. The same is true for an endless chain of resolved promises. Recursive setImmediate does not starve I/O, because new immediates queued during the check phase run in the next iteration.

Q6. Is Node.js single-threaded?

JavaScript execution is single-threaded per isolate: one call stack, one heap. Node the process is multi-threaded. The libuv thread pool (default 4 threads, set with UV_THREADPOOL_SIZE before startup) runs file system calls, dns.lookup, async crypto functions such as pbkdf2 and scrypt, and zlib. V8 also runs garbage collection and JIT compilation on helper threads.

Q7. Does network I/O use the thread pool?

No, and this is a common trap. Sockets use the OS's non-blocking readiness APIs (epoll on Linux, kqueue on macOS, IOCP on Windows), so 10,000 open HTTP connections do not need 10,000 threads. The thread pool is for operations that have no good non-blocking OS API, like most file system calls. One subtle consequence: dns.lookup (used by http.get by default) goes through the pool, while dns.resolve* does not.

Q8. Five crypto.pbkdf2 calls start at once with the default pool. What do you see?

Four finish at roughly the same time, the fifth takes about twice as long, because only four pool threads exist. Raising UV_THREADPOOL_SIZE helps only up to the number of CPU cores. This exact experiment is a favourite way to probe whether a candidate has seen the pool or only read about it.

Q9. What does "blocking the event loop" mean? Give real examples.

Any synchronous work that keeps the main thread busy means no other request is served during that time. Common culprits in real codebases:

  • JSON.parse or JSON.stringify on a multi-megabyte payload.
  • fs.readFileSync, crypto.pbkdf2Sync, bcrypt.hashSync inside a request handler.
  • A regex with catastrophic backtracking on user input (ReDoS), such as /(a+)+$/.
  • Sorting or looping over a 1-million-element array per request.

If a handler spends 200 ms on CPU, that process can serve at most 5 such requests per second, no matter how many connections are open.

Q10. How do you detect event loop blocking in production?

Measure event loop lag: perf_hooks.monitorEventLoopDelay() gives a histogram of delays, and APM tools (Datadog, New Relic, Elastic) chart it. Alert when p99 lag exceeds roughly 100 ms. To find the cause, take a CPU profile with node --cpu-prof or attach Chrome DevTools via --inspect and look for wide synchronous frames in the flame graph.

JavaScript
const { monitorEventLoopDelay } = require('perf_hooks');
const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();
setInterval(() => {
  console.log('p99 loop delay ms:', (h.percentile(99) / 1e6).toFixed(1));
  h.reset();
}, 10_000);

Q11. Microtasks vs macrotasks?

Microtasks are promise reactions (.then, await continuations) and queueMicrotask. Macrotasks are callbacks scheduled into loop phases: timers, I/O, immediates. Node adds its own extra-priority queue, nextTick, that runs before promise microtasks. After any macrotask callback completes, both microtask queues drain completely before the next macrotask runs.

Q12. Why is setTimeout(fn, 100) not guaranteed to run at exactly 100 ms?

The delay is a minimum threshold. The callback runs on the first timers phase after the threshold, which can be late if the loop is busy with a slow callback, a long poll phase, or a GC pause. Timers are not a scheduler; for precise jobs use an external scheduler or a queue with delayed delivery.

Async Patterns: Callbacks, Promises, async/await (Q13-19)

Q13. Callback hell, and how promises fix it.

Nested callbacks make error handling and sequencing painful: each level needs its own if (err), and you cannot use try/catch across async boundaries. Promises make async results first-class values that can be chained, returned and combined, with a single .catch for the chain. async/await is syntax over promises that lets you write sequential-looking code with ordinary try/catch.

Q14. Promise.all vs allSettled vs race vs any.

CombinatorResolves whenRejects whenUse for
Promise.allAll fulfil (array of values)First rejection (fail fast)Fetch user, orders and wallet; all are needed
Promise.allSettledAll settle (array of status objects)NeverSend 50 notifications and report which failed
Promise.raceFirst to settle, either wayFirst to settle, if it rejectedTimeouts
Promise.anyFirst fulfilmentAll reject (AggregateError)Query 3 replicas, take the fastest success

Follow-up interviewers like: Promise.all does not cancel the other promises when one rejects. They keep running; you just stop waiting for them.

Q15. What's wrong with this code?

for (const id of userIds) {
  await sendEmail(id);   // 1,000 users x 200 ms = 200 s
}

It is sequential. If the calls are independent, run them concurrently, but bounded: firing 1,000 requests at once with Promise.all(userIds.map(sendEmail)) can exhaust sockets or hit the email provider's rate limit. A good answer uses a concurrency limit:

JavaScript
async function mapLimit(items, limit, fn) {
  const results = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const i = next++;            // safe: no await between read and increment
      results[i] = await fn(items[i]);
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
  return results;
}

await mapLimit(userIds, 10, sendEmail);   // ~20 s instead of 200 s

The next++ is safe without locks precisely because JS is single-threaded and there is no await between reading and incrementing. Saying that out loud shows you understand the execution model.

Q16. How do you add a timeout to a promise?

Race it against a timer, and clear the timer so it does not keep the process alive. For HTTP calls, prefer real cancellation: pass AbortSignal.timeout(ms) to fetch, which aborts the underlying request rather than just ignoring its result.

const res = await fetch(url, { signal: AbortSignal.timeout(2000) });

Q17. What does util.promisify do?

It converts an error-first callback function (args..., (err, value) => {}) into a promise-returning one. Most core modules now ship promise versions directly (fs/promises, timers/promises, stream/promises), so promisify is mainly for older libraries.

Q18. Does await inside forEach work?

No. forEach ignores the promise returned by an async callback, so the loop finishes immediately, rejections become unhandled, and code after the loop runs before the work completes. Use for...of for sequential work or Promise.all(arr.map(...)) for concurrent work.

Q19. What is AsyncLocalStorage used for?

It carries per-request context (request ID, user ID, tenant) through async calls without passing it as a parameter to every function. Logging libraries use it to stamp every log line from one request with the same correlation ID, which is essential when tracing a failed UPI-style payment across services.

Modules, V8 and Runtime Questions (Q20-25)

Q20. CommonJS vs ES modules?

CommonJSES Modules
Syntaxrequire, module.exportsimport, export
LoadingSynchronous, at call timeParsed and linked before execution; supports top-level await
BindingsCopy of the exported value at require timeLive bindings
Static analysisHard (require can be dynamic)Easy, enables tree-shaking
__dirnameAvailableUse import.meta.dirname (newer Node) or fileURLToPath(import.meta.url)

Q21. module.exports vs exports?

exports is just a variable that initially points to the same object as module.exports. Adding properties (exports.foo = 1) works. Reassigning (exports = { foo: 1 }) breaks the link, and the module exports an empty object, because require returns module.exports.

Q22. How does require caching work?

The first require of a resolved path executes the module and stores it in require.cache. Later requires return the same object, which is why modules behave like singletons (a DB pool created in db.js is shared). Circular requires return a partially populated exports object, a classic source of "undefined is not a function" bugs.

Q23. How does garbage collection work in V8, and what is the memory limit?

V8 uses a generational collector: a small young generation collected frequently with a fast copying "scavenge", and an old generation for survivors, collected with mark-sweep-compact, done mostly incrementally and concurrently to keep pauses short. The default old-space limit depends on Node version and system memory; raise it with --max-old-space-size=4096 (MB). In containers, set it below the container memory limit so V8 collects before the kernel OOM-kills you.

Q24. What are common memory leaks in Node services?

  • Unbounded in-process caches (a plain object or Map keyed by user ID that only grows). Use an LRU with a size cap.
  • Event listeners added per request on a long-lived emitter and never removed (Node warns at 11 listeners: "MaxListenersExceededWarning").
  • Closures holding large objects, such as timers or intervals that are never cleared.
  • Global arrays used for "temporary" debugging that ship to production.

Diagnose by taking two heap snapshots a few minutes apart under load and comparing retained sizes in Chrome DevTools.

Q25. What is a Buffer and why does it exist?

A Buffer is a fixed-size chunk of raw bytes allocated outside the V8 heap (it is a Uint8Array subclass). JavaScript strings are UTF-16 text, which is wrong for binary data like images, TCP packets or encrypted payloads. Buffer.alloc(n) zero-fills; Buffer.allocUnsafe(n) is faster but may contain old memory, so only use it if you overwrite every byte.

Node.js Streams and Backpressure Questions (Q26-32)

Q26. What are streams, and the four types?

Streams process data piece by piece instead of loading it all into memory. Readable (a file being read, an HTTP request body), Writable (a file being written, an HTTP response), Duplex (both directions independently, like a TCP socket) and Transform (a Duplex whose output is computed from its input, like gzip or a CSV parser).

Q27. Why stream a 2 GB file instead of fs.readFile?

readFile allocates the whole file in memory and you cannot start sending until it is all read. With 20 concurrent downloads, that is 40 GB. A stream reads in chunks (64 KiB per chunk by default for fs.createReadStream), so memory stays roughly constant per download and the first byte reaches the client immediately.

Q28. Explain backpressure with code.

When a Writable's internal buffer exceeds its highWaterMark (16 KiB by default for byte streams, 16 objects in object mode), write() returns false. A well-behaved producer stops writing until the 'drain' event. Ignoring the return value means the buffer grows without bound, the classic way a "streaming" service still runs out of memory.

JavaScript: manual backpressure
function writeMany(writable, lines, done) {
  let i = 0;
  function writeChunk() {
    let ok = true;
    while (i < lines.length && ok) {
      ok = writable.write(lines[i++] + '\n');   // false = buffer full
    }
    if (i < lines.length) writable.once('drain', writeChunk);  // resume later
    else done();
  }
  writeChunk();
}

Q29. pipe() vs stream.pipeline()?

Both handle backpressure. pipe() does not forward errors or destroy the other streams when one fails, which leaks file descriptors. pipeline() wires error handling and cleanup across the whole chain. Use the promise version:

JavaScript: gzip upload to disk
const { pipeline } = require('stream/promises');
const fs = require('fs');
const zlib = require('zlib');

app.post('/upload', async (req, res, next) => {
  try {
    await pipeline(req, zlib.createGzip(), fs.createWriteStream('/data/upload.gz'));
    res.status(201).send('stored');
  } catch (err) {
    next(err);   // client aborted, disk full, etc.
  }
});

Q30. Write a Transform stream.

JavaScript: uppercase transform
const { Transform } = require('stream');

const upper = new Transform({
  transform(chunk, encoding, callback) {
    callback(null, chunk.toString().toUpperCase());
  }
});

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

A follow-up: a real CSV transform must handle a row split across two chunks, so it keeps the trailing partial line in an instance variable and emits it in flush().

Q31. Flowing vs paused mode?

A Readable starts paused. Attaching a 'data' listener, calling resume() or pipe() switches it to flowing, where chunks are pushed as fast as they arrive. In paused mode you pull with read() on 'readable'. Readable streams are also async iterables, which is the cleanest modern style: for await (const chunk of stream) { ... }, and it respects backpressure automatically.

Q32. How would you process a 5 GB CSV and insert rows into Postgres?

Stream the file, parse with a streaming CSV parser, batch rows (say 1,000 per insert or use COPY), and await each batch before reading more so DB speed sets the pace. With for await this is natural: the loop does not pull the next chunk until the current batch's insert resolves. Memory stays flat regardless of file size. Mention idempotency (a unique key or a staging table) so a crash halfway can be resumed.

Cluster, Worker Threads and Child Processes (Q33-38)

Q33. How do you use all CPU cores in Node?

One Node process runs JS on one core. Options: the cluster module (a primary process forks workers that share the server port), a process manager like PM2 in cluster mode, or, most common in Kubernetes deployments, run one process per container and scale the replica count. The orchestrator then does the job cluster used to do.

Q34. How does the cluster module distribute connections?

By default (on all platforms except Windows), the primary accepts connections and hands them to workers round-robin. The alternative mode lets workers accept directly from a shared socket, which tends to load-balance unevenly. Workers do not share memory, so in-process state (sessions, rate-limit counters, caches) must move to Redis or similar.

Q35. worker_threads vs cluster vs child_process.

worker_threadsclusterchild_process
UnitThread in same process, own V8 isolate and event loopSeparate Node processesAny separate process
Memory sharingYes: SharedArrayBuffer, transferable ArrayBuffersNoNo
CommunicationpostMessage (structured clone)IPC messagesstdio, IPC with fork
Best forCPU-heavy tasks: image resize, PDF generation, hashingScaling an HTTP server across coresRunning other programs: ffmpeg, Python scripts, git

Q36. Offload a CPU-heavy function to a worker thread.

JavaScript: main.js
const { Worker } = require('worker_threads');

function runPrimeCount(limit) {
  return new Promise((resolve, reject) => {
    const w = new Worker('./prime-worker.js', { workerData: limit });
    w.once('message', resolve);
    w.once('error', reject);
    w.once('exit', code => { if (code !== 0) reject(new Error('exit ' + code)); });
  });
}
JavaScript: prime-worker.js
const { parentPort, workerData } = require('worker_threads');
let count = 0;
for (let n = 2; n <= workerData; n++) {
  let prime = true;
  for (let d = 2; d * d <= n; d++) if (n % d === 0) { prime = false; break; }
  if (prime) count++;
}
parentPort.postMessage(count);

In production, create a fixed pool of workers (libraries like Piscina do this) instead of spawning one per request, since starting a worker costs tens of milliseconds and its own memory.

Q37. exec vs spawn vs execFile vs fork?

spawn streams stdout/stderr, good for long-running or large-output processes. exec runs through a shell and buffers output (with a size cap), convenient for short commands but dangerous with user input because of shell injection. execFile runs a binary without a shell, safer. fork is a spawn of a Node script with an IPC channel for send()/on('message').

Q38. Why is Node good for I/O-bound but weak for CPU-bound work?

For I/O-bound APIs (a typical BFF that calls three services and a database), the thread mostly waits, and one event loop can juggle thousands of in-flight requests with little memory per connection. For CPU-bound work, every millisecond of computation is a millisecond nothing else runs. The fix is to move CPU work to worker threads or a separate service in a language better suited to it.

Express.js and Middleware Interview Questions (Q39-44)

Q39. What is middleware in Express?

A function (req, res, next) in the request pipeline. Each one can read or modify req/res, end the response, or call next() to pass control on. Middleware runs in registration order, which is why express.json() must come before routes that read req.body, and auth before protected routes.

Q40. Write an authentication middleware.

JavaScript
const jwt = require('jsonwebtoken');

function requireAuth(req, res, next) {
  const header = req.headers.authorization || '';
  const [scheme, token] = header.split(' ');
  if (scheme !== 'Bearer' || !token) {
    return res.status(401).json({ error: 'missing token' });
  }
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
    next();
  } catch {
    res.status(401).json({ error: 'invalid or expired token' });
  }
}

app.get('/orders', requireAuth, listOrders);

Points interviewers listen for: return after sending a response (otherwise "Cannot set headers after they are sent"), pin the algorithm list, and use 401 for missing/invalid credentials versus 403 for authenticated-but-forbidden. More on token design in our OAuth2 and JWT guide.

Q41. How does error-handling middleware work?

Express identifies error middleware by its four parameters (err, req, res, next). Calling next(err) anywhere skips normal middleware and jumps to it. Register it last.

JavaScript
class AppError extends Error {
  constructor(status, message) { super(message); this.status = status; }
}

app.use((err, req, res, next) => {
  const status = err.status || 500;
  if (status >= 500) logger.error({ err, reqId: req.id }, 'unhandled');
  res.status(status).json({
    error: status >= 500 ? 'internal error' : err.message   // never leak stack traces
  });
});

Q42. Async errors in Express 4 vs Express 5?

Express 4 does not catch rejected promises from async handlers: the request hangs and you get an unhandled rejection. You wrap handlers (const wrap = fn => (req, res, next) => fn(req, res, next).catch(next)) or use express-async-errors. Express 5 (stable since late 2024) forwards rejections from async handlers and middleware to next(err) automatically. Ask which version the team runs; many production codebases are still on 4.

Q43. app.use vs app.get, and router-level middleware?

app.use(path, fn) matches any method and any path that starts with path. app.get(path, fn) matches only GET and the exact route pattern. express.Router() creates a mini-app with its own middleware stack, so /admin routes can share an admin check without applying it globally.

Q44. Implement a simple rate limiter middleware. What breaks at scale?

JavaScript: fixed window, in memory
function rateLimit({ windowMs, max }) {
  const hits = new Map();   // key -> { count, resetAt }
  return (req, res, next) => {
    const key = req.ip;
    const now = Date.now();
    let entry = hits.get(key);
    if (!entry || now >= entry.resetAt) {
      entry = { count: 0, resetAt: now + windowMs };
      hits.set(key, entry);
    }
    entry.count++;
    if (entry.count > max) {
      res.set('Retry-After', Math.ceil((entry.resetAt - now) / 1000));
      return res.status(429).json({ error: 'too many requests' });
    }
    next();
  };
}

What breaks: with 4 cluster workers or 10 pods, each has its own Map, so the real limit is 10x. The Map never evicts old keys (a memory leak). Behind a load balancer, req.ip is the proxy's IP unless you configure trust proxy. The production answer moves counters to Redis with an atomic INCR plus EXPIRE or a token bucket in Lua; see rate limiter system design.

Error Handling and Production Node.js (Q45-50)

Q45. Operational errors vs programmer errors?

Operational errors are expected failures in a correct program: a downstream timeout, a duplicate key, invalid user input, a full disk. Handle them: retry, return 4xx/5xx, fall back. Programmer errors are bugs: reading a property of undefined, passing a string where a number was expected. You cannot handle a bug sensibly at runtime; log it, crash, restart, and fix the code.

Q46. What should an uncaughtException handler do?

Log the error synchronously with as much context as possible, flush metrics if you can, and exit with a non-zero code. After an uncaught exception, the process may hold half-updated state, leaked locks or open transactions. Continuing to serve traffic is the wrong answer even though it looks resilient. Your process manager (Kubernetes, PM2, systemd) restarts a clean instance.

Q47. What happens on an unhandled promise rejection?

Since Node 15, the default --unhandled-rejections=throw mode raises it as an uncaught exception, which crashes the process if nothing handles it. Older Node only printed a warning, which is why legacy services "silently" lost errors. Treat any unhandledRejection event as a bug: log it, then follow the same crash-and-restart policy.

Q48. Implement graceful shutdown.

Kubernetes sends SIGTERM, waits terminationGracePeriodSeconds (30 s by default), then SIGKILL. A graceful shutdown stops accepting new connections, finishes in-flight requests, closes DB pools and consumers, then exits.

JavaScript
const server = app.listen(3000);
let shuttingDown = false;

app.get('/healthz', (req, res) =>
  shuttingDown ? res.status(503).end() : res.send('ok'));

process.on('SIGTERM', () => {
  shuttingDown = true;                       // readiness probe starts failing
  server.close(async () => {                 // stops new connections; waits for in-flight
    await db.end();                          // close pool
    await consumer.disconnect();             // stop Kafka/SQS consumption
    process.exit(0);
  });
  server.closeIdleConnections?.();           // drop idle keep-alive sockets (Node 18.2+)
  setTimeout(() => process.exit(1), 25_000).unref();   // hard deadline before SIGKILL
});

Q49. How do you secure a Node.js API?

  • Validate every input with a schema (zod, joi, ajv); reject unknown fields.
  • Parameterised queries only; never build SQL with template strings.
  • Limit request body size (express.json({ limit: '100kb' })) to prevent memory abuse.
  • Security headers via helmet, strict CORS, rate limiting on auth endpoints.
  • Run npm audit in CI, commit the lockfile, and use npm ci for reproducible installs.
  • Avoid eval, child_process.exec with user input, and regexes vulnerable to ReDoS.

Q50. Your Node API's latency spiked after a deploy. Walk me through debugging.

  1. Scope it. All endpoints or one? All pods or one? If all endpoints are slow on a pod with one core pegged at 100%, suspect event loop blocking.
  2. Check event loop lag and GC. High lag means synchronous work; frequent long GC pauses with growing heap means a leak or a large allocation per request.
  3. Diff the deploy. Look for new sync calls, a new regex on user input, a larger payload being parsed, or a dependency upgrade.
  4. Profile. Capture a CPU profile under load (--cpu-prof or the inspector) and find the hot synchronous function.
  5. Check downstreams. If CPU is low but latency is high, the service is waiting: slow DB queries, exhausted connection pool, a dependency timing out. Distributed tracing shows which span grew.
  6. Mitigate first. Roll back, then root-cause. Interviewers reward candidates who restore service before investigating.

How to Prepare for a Node.js Backend Interview

Reading answers is not enough for Node, because the follow-ups ("what if the file is 10 GB?", "what if there are 4 pods?") test whether you have built things. A focused two-week plan:

Days 1-3

Event loop by experiment

Run the puzzle in Q3 with variations. Run 5 parallel pbkdf2 calls with pool sizes 1, 4 and 8 and time them. Block the loop with a sync loop and watch a second HTTP request stall.

Days 4-6

Streams project

Build an endpoint that uploads a large CSV, parses it with a streaming parser, and batch-inserts into Postgres. Watch memory with a 1 GB file.

Days 7-9

Production Express app

Auth middleware, central error handler, request IDs via AsyncLocalStorage, Redis rate limiter, graceful shutdown, health checks. Deploy it in Docker.

Days 10-14

Mock and system design

Practise explaining the event loop in 90 seconds. Then do two design problems end to end, such as a notification service and a URL shortener, reasoning about Node's strengths.

Two things to avoid in the interview itself. First, do not say Node is "faster" than Java or Go; say it is efficient for I/O-heavy workloads with many concurrent connections and explain why. Second, do not answer scaling questions with "add more threads": the right Node answers are more processes, worker threads for CPU work, caching, and moving state out of process.

💡
Connect it to the bigger pictureFor SDE-2 roles, Node knowledge is the entry ticket; the decision is usually made in design rounds. Once you are comfortable here, move to REST API design questions and microservices interview questions, and use the backend developer roadmap to fill gaps in databases and caching.

Frequently Asked Questions

What are the phases of the Node.js event loop?▾
libuv runs six phases in order: timers (setTimeout and setInterval callbacks), pending callbacks (some deferred system errors), idle/prepare (internal), poll (retrieve new I/O events and run their callbacks, blocking here if nothing else is scheduled), check (setImmediate callbacks), and close callbacks (such as socket close events). The process.nextTick queue and the promise microtask queue are drained after every individual callback, not as a phase.
What is the difference between process.nextTick and setImmediate?▾
process.nextTick queues a callback to run immediately after the current operation finishes, before any I/O or timers and even before promise callbacks in CommonJS code. setImmediate queues a callback in the check phase, which runs after the poll phase of the current loop iteration. Recursive nextTick calls can starve I/O, while recursive setImmediate calls cannot.
Is Node.js single-threaded?▾
Your JavaScript runs on a single main thread with one call stack. Node itself is not single-threaded: libuv keeps a thread pool (four threads by default) for file system work, DNS lookups, some crypto and zlib, and V8 uses background threads for garbage collection and compilation. You can also create your own threads with worker_threads.
When should I use worker threads instead of cluster in Node.js?▾
Use cluster (or multiple containers behind a load balancer) to scale an HTTP server across CPU cores, since each process handles independent requests. Use worker_threads to offload CPU-heavy tasks such as image processing, hashing or parsing large files inside one process so the event loop stays responsive. Worker threads can share memory through SharedArrayBuffer; cluster processes cannot.
How do you handle errors in Express middleware?▾
Define an error-handling middleware with four parameters (err, req, res, next) and register it after all routes. In Express 4 you must catch errors in async handlers and pass them to next(err), or use a wrapper. Express 5 forwards rejected promises from async handlers to the error middleware automatically.
What is backpressure in Node.js streams?▾
Backpressure is the mechanism that stops a fast producer from overwhelming a slow consumer. When a Writable stream's internal buffer exceeds its highWaterMark, write() returns false and the producer should pause until the drain event fires. pipe() and stream.pipeline() handle this automatically, which is why you should prefer them over manual data event handlers.
What happens on an unhandled promise rejection in Node.js?▾
Since Node.js 15 the default mode is throw: an unhandled rejection is raised as an uncaught exception and crashes the process unless an unhandledRejection handler is registered. The right response is to log it, fix the missing catch, and let a process manager restart the service rather than continuing in an unknown state.

Prepare for Backend Interviews with Prepflix

Prepflix's interview program pairs Node.js and backend fundamentals with DSA, system design and mock rounds modelled on Indian product-company interviews.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.