How do you shut down a Node.js server gracefully?
On
SIGTERM, stop accepting connections, let in-flight requests finish, close database pools, then exit, with a hard timeout in case something hangs.const server = app.listen(3000); function shutdown(signal) { console.log(`${signal} received, closing`); server.close(async () => { // stop accepting, wait for open requests await db.end(); // close the database pool process.exit(0); }); setTimeout(() => process.exit(1), 10_000).unref(); // give up after 10 s } process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT'));Fail the readiness probe first so the load balancer stops sending traffic. On Node 18 and earlier, idle keep-alive connections can hold
server.close()open; callserver.closeIdleConnections()(18.2 and later) there.What is
AsyncLocalStorageand what problem does it solve?AsyncLocalStoragekeeps a value that follows one async call chain, throughawaitand timers, without passing it as an argument. The standard use is a request id in logs.JavaScriptconst { AsyncLocalStorage } = require('node:async_hooks'); const context = new AsyncLocalStorage(); function log(message) { const store = context.getStore(); console.log(`[${store ? store.requestId : 'no request'}] ${message}`); } const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function handleRequest(delay) { await sleep(delay); log('loaded user'); await sleep(delay); log('sent response'); } context.run({ requestId: 'req-1' }, () => handleRequest(20)); context.run({ requestId: 'req-2' }, () => handleRequest(5)); log('server started');The requests interleave, yet each line carries the right id. OpenTelemetry uses it for context propagation.
How do you find and fix a memory leak in Node.js?
Confirm it: heap usage that keeps rising across garbage collections under steady load. Then take two heap snapshots minutes apart (
--inspectorv8.writeHeapSnapshot()), compare them, and follow the retainers. Usual culprits: an unboundedMapcache, listeners added per request to a long-lived emitter (MaxListenersExceededWarning),setIntervaltimers never cleared, and closures that capture a whole request.What are the most common security issues in Node.js applications?
The common ones: injection (use parameterized SQL, and
execFilewith an argument array for shell commands), prototype pollution from deep-merging user JSON (below), ReDoS from a regex such as/(a+)+$/that blocks the event loop on a crafted string, path traversal through names like../../etc/passwd, vulnerable dependencies (npm audit, a committed lock file) and secrets in code or logs. Addhelmet, rate limiting, schema validation and error responses without stack traces.How do you prevent path traversal when serving files in Node.js?
Resolve the name against the base directory, then check the result is still inside it.
JavaScriptconst path = require('node:path'); const UPLOADS = '/srv/uploads'; function safePath(name) { const full = path.resolve(UPLOADS, name); if (!full.startsWith(UPLOADS + path.sep)) { throw new Error(`blocked: ${name}`); } return full; } for (const name of ['report.pdf', '../../etc/passwd', 'a/../b.txt', '/etc/hosts']) { try { console.log(safePath(name)); } catch (err) { console.log(err.message); } }path.resolvecollapses..and lets an absolute name replace the base, so check the resolved path, againstUPLOADS + path.sep: a plainstartsWith(UPLOADS)would accept/srv/uploads-old/secret.What is prototype pollution, and how does it happen in Node.js?
Prototype pollution is user input writing properties onto
Object.prototype, so every object inherits them, usually through a hand-written deep merge.function merge(target, source) { for (const key in source) { if (typeof source[key] === 'object' && source[key] !== null) { target[key] = merge(target[key] ?? {}, source[key]); } else { target[key] = source[key]; } } return target; } const payload = JSON.parse('{"__proto__": {"isAdmin": true}}'); merge({}, payload); const user = {}; console.log(user.isAdmin); // Node.js prints: trueJSON.parsemakes__proto__an ordinary key, buttarget['__proto__']reads the real prototype. Fix: skip__proto__,constructorandprototype, validate with a schema, or use aMapfor user-keyed data.How do you test a Node.js application?
Unit tests for pure logic, integration tests for routes against a real test database, a few end-to-end checks. Node has a built-in runner,
node:test(stable since Node 20); Jest and Vitest are common.// sum.test.js, run with: node --test const test = require('node:test'); const assert = require('node:assert/strict'); const { sum } = require('./sum'); test('adds two numbers', () => { assert.equal(sum(2, 3), 5); }); test('rejects bad input', async () => { await assert.rejects(() => Promise.reject(new TypeError('bad')), TypeError); });For routes, export the app without
listenand drive it withsupertest.What changed in recent Node.js versions that you should know for an interview?
Feature Since Global fetchNode 18 (stable in 21) Test runner node:testNode 18 (stable in 20) --env-file=.envNode 20.6 import.meta.dirnameNode 20.11 require()of ES modulesNode 22.12 Running .tsby stripping typesNode 23.6 (22.18 on the 22 line) Type stripping checks nothing and rejects syntax that needs rewriting, such as
enum(Node 22 and 24 accept it with--experimental-transform-types, a flag Node 26 removed). Through Node 26, even versions become LTS; from Node 27 there is one major a year and every one becomes LTS.How do you scale a Node.js application horizontally?
Run several identical, stateless instances behind a load balancer, with shared state outside the process: sessions and rate limits in Redis, uploads in object storage, cron on a queue or one elected leader, WebSocket broadcasts through pub/sub. Use every core first, then add machines. The test: you can kill any instance and users see at most one retried request.
A payment provider sends the same webhook twice. How do you make the handler safe?
Make it idempotent: webhooks are delivered at least once, so record each event id and skip ids already seen.
JavaScriptconst processed = new Set(); function handlePaymentWebhook(event) { if (processed.has(event.id)) return `ignored duplicate ${event.id}`; processed.add(event.id); return `marked order ${event.orderId} as paid`; } const deliveries = [ { id: 'evt_1', orderId: 101 }, { id: 'evt_2', orderId: 102 }, { id: 'evt_1', orderId: 101 }, // the provider retried the first one ]; for (const event of deliveries) console.log(handlePaymentWebhook(event));In production the
Setis a unique constraint in the database, written in the same transaction as the side effect. Answer a duplicate with200, or the provider keeps retrying.Practice it: Contains Duplicate
Advanced Node.js Interview Questions
Production behavior and design: shutdown, request context, leaks, security, testing and scaling out.