- 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:
- 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.
- Output puzzle. A snippet mixing
setTimeout,setImmediate, promises andnextTick. You must reason step by step. - 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.
- 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.
| Phase | What runs | Example |
|---|---|---|
| 1. timers | Callbacks whose setTimeout/setInterval threshold has elapsed | setTimeout(fn, 100) |
| 2. pending callbacks | Some system-level callbacks deferred from the previous iteration | Certain TCP errors such as ECONNREFUSED on some systems |
| 3. idle, prepare | Internal to libuv | None you write |
| 4. poll | Fetch new I/O events and run their callbacks; may block here waiting for I/O if nothing else is due | fs.readFile callback, incoming socket data |
| 5. check | setImmediate callbacks | setImmediate(fn) |
| 6. close callbacks | Close events | socket.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).
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).
.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.parseorJSON.stringifyon a multi-megabyte payload.fs.readFileSync,crypto.pbkdf2Sync,bcrypt.hashSyncinside 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.
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.
| Combinator | Resolves when | Rejects when | Use for |
|---|---|---|---|
Promise.all | All fulfil (array of values) | First rejection (fail fast) | Fetch user, orders and wallet; all are needed |
Promise.allSettled | All settle (array of status objects) | Never | Send 50 notifications and report which failed |
Promise.race | First to settle, either way | First to settle, if it rejected | Timeouts |
Promise.any | First fulfilment | All 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:
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?
| CommonJS | ES Modules | |
|---|---|---|
| Syntax | require, module.exports | import, export |
| Loading | Synchronous, at call time | Parsed and linked before execution; supports top-level await |
| Bindings | Copy of the exported value at require time | Live bindings |
| Static analysis | Hard (require can be dynamic) | Easy, enables tree-shaking |
__dirname | Available | Use 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.
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:
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.
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_threads | cluster | child_process | |
|---|---|---|---|
| Unit | Thread in same process, own V8 isolate and event loop | Separate Node processes | Any separate process |
| Memory sharing | Yes: SharedArrayBuffer, transferable ArrayBuffers | No | No |
| Communication | postMessage (structured clone) | IPC messages | stdio, IPC with fork |
| Best for | CPU-heavy tasks: image resize, PDF generation, hashing | Scaling an HTTP server across cores | Running other programs: ffmpeg, Python scripts, git |
Q36. Offload a CPU-heavy function to a worker thread.
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)); });
});
}
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.
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.
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?
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.
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 auditin CI, commit the lockfile, and usenpm cifor reproducible installs. - Avoid
eval,child_process.execwith user input, and regexes vulnerable to ReDoS.
Q50. Your Node API's latency spiked after a deploy. Walk me through debugging.
- 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.
- 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.
- Diff the deploy. Look for new sync calls, a new regex on user input, a larger payload being parsed, or a dependency upgrade.
- Profile. Capture a CPU profile under load (
--cpu-profor the inspector) and find the hot synchronous function. - 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.
- 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:
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.
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.
Production Express app
Auth middleware, central error handler, request IDs via AsyncLocalStorage, Redis rate limiter, graceful shutdown, health checks. Deploy it in Docker.
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.
Frequently Asked Questions
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 →