Node.js Interview Questions and Answers
27 questions9 beginner · 12 intermediate · 6 advancedLast reviewed
This set is for developers preparing for backend or full-stack roles that use Node.js, from first-job screens to senior rounds where the event loop, streams and production failure modes get probed. The questions run from runtime basics to scaling, shutdown and API-design trade-offs.
Practise by saying each answer aloud in under a minute before you open it, then compare: did you give the mechanism, a concrete example and one pitfall? Run the code samples yourself on a current Node release, change one line, and predict the output before you rerun. For the underlying ideas, the Node.js course and the REST API course go step by step.
1What is Node.js, and what kind of workloads is it a good fit for?Beginner
Node.js is a runtime that runs JavaScript outside the browser. It combines the V8 engine with libuv, which provides the event loop and asynchronous I/O.
Its sweet spot is I/O-bound work: API servers, proxies, real-time messaging, build tools, anything that spends most of its time waiting on networks or disks. One thread can serve thousands of open connections because it registers a callback and moves on instead of sitting idle.
The weak spot is long CPU-bound work on the main thread, such as resizing images or transforming a huge JSON document, because it stalls every other request. For that you offload to worker threads, a queue or a separate service.
What the interviewer is checking: Whether you can describe the runtime accurately and name both its strengths and its limits, not just repeat that it is fast.
Likely follow-up: Would you build a video-transcoding service in Node.js? How would you structure it if you had to?
2What is the difference between blocking and non-blocking code in Node.js?Beginner
Blocking code holds the main thread until it finishes, so nothing else in the process can run. Non-blocking code starts the work, hands back control immediately and gets a callback, promise resolution or event when the result is ready.
In the sample, the sync read finishes before the next line runs, while the callback version only runs after the current code has completed. In a server, a synchronous call in a request handler freezes every other client for as long as it takes.
Sync APIs such as
readFileSyncare fine at start-up, in scripts and in CLIs. They are a bug inside request handling.js const fs = require("node:fs"); console.log("1 start"); fs.readFile(__filename, "utf8", (err, text) => { if (err) throw err; console.log("3 async read finished"); }); const text = fs.readFileSync(__filename, "utf8"); console.log("2 sync read finished");Output
1 start 2 sync read finished 3 async read finished
What the interviewer is checking: Whether you understand that non-blocking is about not holding the thread, and can say where sync calls are acceptable.
Likely follow-up: Is
awaitblocking? What happens to the rest of the server while anawaitis pending?3Node is called single-threaded. What does that really mean, and what runs on other threads?Beginner
Your JavaScript runs on one main thread, one call stack at a time. That is why ordinary variables need no locks, and also why one slow function delays everything.
The process itself has more threads. libuv keeps a thread pool, four threads by default and adjustable with
UV_THREADPOOL_SIZE, which handles file system calls, DNS lookups through the system resolver, and some crypto and zlib work. Network sockets use the operating system's async mechanisms rather than that pool. V8 also uses helper threads for garbage collection.A common pitfall is saying only "single-threaded". If five slow pool-backed tasks arrive at once, the fifth waits for a free thread.
What the interviewer is checking: Depth beyond the slogan: JavaScript execution versus the runtime's own threads.
Likely follow-up: Four slow
crypto.pbkdf2calls are running. What happens to a fifth one, and to a file read issued right after?4What are the main differences between CommonJS and ES modules in Node.js?Beginner
CommonJS uses
require()andmodule.exports. It loads synchronously, andrequireis an ordinary function call, so you can use it conditionally. ES modules useimport/export, are statically analysable, load asynchronously and allow top-levelawait.A file is treated as ESM if it ends in
.mjsor its nearestpackage.jsonhas"type": "module". Inside ESM,require,module,__dirnameand__filenamedo not exist; you derive paths fromimport.meta.url. Relative imports must include the file extension.Interop is one-sided in older releases: ESM can import CommonJS, but
require()of an ES module was an error. Recent Node versions have been relaxing that, so check what your target version supports.What the interviewer is checking: Practical awareness of how the module system is chosen and what disappears in ESM.
Likely follow-up: Your package is CommonJS but a dependency is ESM-only. What are your options?
5What is a Buffer in Node.js and why do we need it?Beginner
A
Bufferis a fixed-size chunk of raw bytes, aUint8Arraysubclass. JavaScript strings are text, but files, sockets and cryptography deal in bytes, so Node needs a way to hold and manipulate binary data.The key point is that bytes and characters are different things. In UTF-8, "café" is four characters but five bytes. If a network chunk ends in the middle of a multi-byte character and you convert each chunk to a string separately, you get replacement characters. Use
setEncodingon the stream or aStringDecoder, or concatenate the buffers first.Prefer
Buffer.fromandBuffer.alloc; the oldnew Buffer()constructor is deprecated.js const b = Buffer.from("café", "utf8"); console.log(b.length, "café".length); console.log(b.toString("hex")); const { StringDecoder } = require("node:string_decoder"); const first = b.subarray(0, 4); // ends in the middle of "é" const second = b.subarray(4); console.log(JSON.stringify(first.toString() + second.toString())); const dec = new StringDecoder("utf8"); console.log(JSON.stringify(dec.write(first) + dec.write(second)));Output
5 4 636166c3a9 "caf��" "café"
What the interviewer is checking: Whether you separate bytes from characters and know the chunk-boundary pitfall.
Likely follow-up: How would you safely read a request body that arrives as many chunks and parse it as JSON?
6How do you handle errors in async/await code? What is wrong with
return promiseinside a try/catch?BeginnerWith async/await you wrap awaited calls in
try/catch, or attach.catchwhen you are using promises directly. The catch only sees rejections of promises that are awaited inside the try block.That is the trap with
return loadParcel(1). The function returns the pending promise without waiting, so the try block has already exited before it rejects, and thecatchnever runs.return await loadParcel(1)keeps the rejection inside the block. If you have afinallythat must run after the work, the same applies.Also remember an async function never throws synchronously; it always returns a promise, so callers must await or catch it.
js async function loadParcel(id) { throw new Error("parcel " + id + " not found"); } async function withoutAwait() { try { return loadParcel(1); // rejection leaves the try block unobserved } catch (e) { return "handled inside"; } } async function withAwait() { try { return await loadParcel(1); } catch (e) { return "handled inside"; } } (async () => { console.log(await withoutAwait().catch((e) => "escaped: " + e.message)); console.log(await withAwait().catch((e) => "escaped: " + e.message)); })();Output
escaped: parcel 1 not found handled inside
What the interviewer is checking: Whether you know exactly when a rejection is observed, which is where many real bugs hide.
Likely follow-up: When would you deliberately skip the
awaitin areturn, and what do you lose?7What is middleware in an Express-style framework, and what does
next()do?BeginnerMiddleware is a function that receives the request, the response and a
nextcallback. It can read or modify the request, end the response itself, or callnext()to pass control to the following function in the chain.Think of it as a pipeline each request walks through: logging, parsing the body, authentication, then the route handler. If a middleware neither responds nor calls
next(), the request hangs until the client times out.Calling
next(err)with an argument skips ahead to error-handling middleware, which is recognised by its four parameters(err, req, res, next). Common use cases are authentication, validation, compression, rate limiting and request IDs.What the interviewer is checking: That you understand the control flow of a request through the stack, including the hanging-request failure mode.
Likely follow-up: How would you write a middleware that measures how long each request takes and logs it?
8How do you manage configuration and secrets in a Node.js application?Beginner
Follow the twelve-factor idea: configuration that changes between environments comes from environment variables, not from code or committed files. Secrets such as database passwords and signing keys are injected by the platform, a secrets manager or CI, never committed to git.
Load and validate everything once at start-up, in one module, and fail fast with a clear message if something is missing or malformed. That turns a mysterious runtime failure at 2am into a failed deploy. Environment values are always strings, so convert and range-check numbers and booleans.
For local development, recent Node versions can read a
.envfile with--env-file, or you can use a library such as dotenv. Keep the.envfile out of version control.What the interviewer is checking: Operational maturity: fail-fast validation and keeping secrets out of the repository.
Likely follow-up: Why is it risky to log the whole config object at start-up?
9In a REST API, how do GET, POST, PUT, PATCH and DELETE differ? Which are idempotent?Beginner
GET reads a resource and must have no side effects. POST creates a resource or triggers an action, so repeating it can create duplicates. PUT replaces a resource at a known URL with the body you send. PATCH applies a partial change. DELETE removes a resource.
Idempotent means that making the same request several times leaves the server in the same state as making it once. GET, PUT and DELETE are idempotent by definition; so is PATCH in practice if you write it as "set this field to X", but not if it means "add 1". POST is not. Safe methods like GET also should not change anything.
Idempotency matters because clients and proxies retry on timeouts, so it decides which calls are safe to repeat automatically.
What the interviewer is checking: Whether you know the semantics and can connect idempotency to retry safety.
Likely follow-up: A client times out on a POST that creates an order. How do you make a retry safe?
10Walk me through the phases of the Node.js event loop. What do you actually need to know in practice?Intermediate
Each loop iteration visits phases in order: timers (expired
setTimeout/setIntervalcallbacks), pending callbacks, internal idle/prepare, poll (collect new I/O events and run their callbacks, waiting here if nothing else is due), check (setImmediatecallbacks) and close callbacks.Between callbacks, Node drains the
process.nextTickqueue and then the promise microtask queue. So a promise continuation always runs before the loop moves on.Practical consequences: a timer delay is a minimum, not a promise, because a busy loop delays it. Inside an I/O callback,
setImmediatealways runs before a zero-delay timer, because check follows poll. And a callback that runs for 300 ms delays every timer and every socket behind it.What the interviewer is checking: Whether you can map the phases onto observable behaviour instead of reciting a diagram.
Likely follow-up: A
setTimeoutof 100 ms fires after 400 ms in production. What could cause that?11What is the order of
process.nextTick, a resolved promise andsetImmediate? How do they differ?Intermediateprocess.nextTickcallbacks run first, right after the current operation, before the event loop continues. Promise callbacks (microtasks) run next.setImmediateruns later, in the check phase of the loop. In a CommonJS script that prints sync, nextTick, promise, setImmediate.One detail: in an ES module, the main module itself is evaluated inside a promise job, so the promise callback can run before nextTick. Do not build logic on that difference.
The warning is starvation. A
nextTickor promise that keeps rescheduling itself prevents the loop from ever reaching I/O. UsesetImmediateto yield to I/O, and keepnextTickfor rare cases like emitting an event after the caller has attached listeners.js setImmediate(() => console.log("setImmediate")); Promise.resolve().then(() => console.log("promise")); process.nextTick(() => console.log("nextTick")); console.log("sync");Output
sync nextTick promise setImmediate
What the interviewer is checking: Whether you know the three queues, their priority and the starvation risk.
Likely follow-up: Why is the order of
setTimeout(fn, 0)andsetImmediateunreliable in the main module but fixed inside an I/O callback?12Why use streams instead of reading a whole file into memory? How do you chain them safely?Intermediate
Streams process data in chunks, so memory stays flat no matter how large the input is, and the first bytes can leave before the last ones are read. Reading a multi-gigabyte export with
readFileper request would hold the whole thing in memory for each concurrent user.There are four kinds: Readable, Writable, Duplex and Transform. To connect them, prefer
stream.pipeline(or its promise version) over chained.pipe()calls. With plain.pipe()an error in one stream does not close the others, so file handles and sockets can leak.pipelineforwards the error and destroys every stream in the chain.Readable streams are also async iterables, so
for await (const chunk of stream)is a clean way to consume them.What the interviewer is checking: Whether you know why streams exist and the error-handling advantage of
pipeline.Likely follow-up: How would you stream a large database result set to an HTTP client as CSV?
13What is backpressure in streams, and how do you respect it when writing?Intermediate
Backpressure is the signal a slow consumer sends upstream to say "stop sending, I am full". In Node,
writable.write(chunk)returnsfalseonce its internal buffer has reachedhighWaterMark. You should stop writing and resume when thedrainevent fires.The return value is advice, not enforcement. Node will keep accepting data after
false, so a fast producer feeding a slow destination silently buffers everything in memory until the process dies.pipe()andpipeline()implement this automatically, which is another reason to use them. You only handle it by hand when writing your own loop, as in the sample, where the tiny buffer makes the pauses visible.js const { Writable } = require("node:stream"); const sink = new Writable({ highWaterMark: 4, write(chunk, _enc, done) { setTimeout(done, 5); }, }); let sent = 0; function pump() { while (sent < 10) { const ok = sink.write("x"); sent++; if (!ok) { console.log("paused after", sent, "writes"); sink.once("drain", pump); return; } } console.log("all", sent, "written"); sink.end(); } pump();Output
paused after 4 writes paused after 8 writes all 10 written
What the interviewer is checking: Whether you understand the mechanism and the consequence of ignoring it, not only the term.
Likely follow-up: What does
highWaterMarkmean for a stream in object mode?14Why does middleware order matter in Express-style apps? Give an example of an ordering bug.Intermediate
Middleware runs in the order it is registered, and each one decides whether to call
next()or finish the response. So the order is part of the design. A sensible stack is request ID and logging, security headers and CORS, body parsing, rate limiting and authentication, then routes, then a 404 handler, then the error handler last.Typical bugs: a body parser registered after the route that needs
req.body; an auth check registered after the routes it should protect; or, as in the sample, a rate limiter placed after the handler that already responded, so it never runs.In older Express versions a rejected promise in an async handler does not reach the error middleware unless you call
next(err)or wrap it. Recent major versions forward it, but verify for yours.js function createApp() { const stack = []; return { use(fn) { stack.push(fn); }, handle(req) { const res = { trail: [] }; let i = 0; const next = () => { const fn = stack[i++]; if (fn) fn(req, res, next); }; next(); return res.trail; }, }; } const app = createApp(); app.use((req, res, next) => { res.trail.push("logger"); next(); }); app.use((req, res, next) => { if (!req.token) { res.trail.push("auth: 401"); return; } // no next(): chain stops res.trail.push("auth: ok"); next(); }); app.use((req, res) => { res.trail.push("route handler"); }); // sends the response app.use((req, res, next) => { res.trail.push("rate limiter"); next(); }); // too late console.log(app.handle({ token: "abc" })); console.log(app.handle({}));Output
[ 'logger', 'auth: ok', 'route handler' ] [ 'logger', 'auth: 401' ]
What the interviewer is checking: Whether you can reason about a request's path through a middleware stack and spot ordering mistakes.
Likely follow-up: Where would you put a middleware that attaches a correlation ID, and why there?
15What happens to an unhandled promise rejection in modern Node.js, and how should you deal with it?Intermediate
Since Node 15 the default is to treat an unhandled rejection like an uncaught exception: the process prints the error and exits with a non-zero code. Older versions only warned, which hid bugs.
The real fix is to make sure every promise is awaited or has a
.catch. Fire-and-forget calls such as sending an analytics event need their own catch that logs the failure.A
process.on("unhandledRejection")handler is a safety net for logging and metrics, not a way to keep running. After an unexpected error the process state is uncertain, so log it, shut down gracefully and let the supervisor restart you. In the sample the handler stops the crash, andexitCode = 1records that something went wrong.js process.on("unhandledRejection", (reason) => { console.log("unhandled:", reason.message); process.exitCode = 1; }); async function refreshRates() { throw new Error("rates API timed out"); } refreshRates(); // no await, no catch setTimeout(() => console.log("process is still alive"), 20);Output
unhandled: rates API timed out process is still alive
What the interviewer is checking: Whether you know the current default behaviour and treat the handler as a last resort.
Likely follow-up: What is the difference between handling
unhandledRejectionanduncaughtException?16What baseline security measures would you add to a Node.js HTTP API?Intermediate
Start with transport and headers: serve over HTTPS and set hardening headers, which is what helmet-style middleware does, such as
X-Content-Type-Options: nosniff,Strict-Transport-Securityand a Content-Security-Policy for any HTML you serve. Hide the framework banner, for example withapp.disable("x-powered-by").Then: a strict CORS allowlist instead of
*for credentialed requests, body-size limits, schema validation on every input, parameterised database queries, rate limiting on login and expensive endpoints, and error responses that never leak stack traces.Operationally, keep dependencies patched with
npm auditor similar tools, run as a non-root user and keep secrets out of logs. The OWASP Top 10 course maps these to concrete attacks.What the interviewer is checking: Breadth across headers, input handling and operations, with the reason behind each measure.
Likely follow-up: What does CORS actually protect, and what does it not protect against?
17How do you validate request bodies in Node.js, and why is a TypeScript type not enough?Intermediate
TypeScript types are erased at compile time. Whatever JSON a client sends arrives as untyped data, so the type annotation on
req.bodyis only a hope. Validate at the edge of the system, before any business logic runs.Check shape, types, lengths and ranges, and allowlist the fields you accept. Rejecting unknown fields prevents mass-assignment bugs, such as a caller sneaking in
isAdmin: true. Return 400 or 422 with per-field messages, and do not silently "fix" bad input.Schema libraries such as zod, joi or ajv do this declaratively, or you can write an allowlist check by hand. Validation is not a substitute for parameterised queries or output encoding, which defend different layers.
What the interviewer is checking: Whether you treat runtime input as untrusted and know about allowlisting and mass assignment.
Likely follow-up: Should validation live in middleware, in the handler or in the service layer?
18How would you implement rate limiting in a Node.js API, and what are the limits of a simple approach?Intermediate
The simplest version is a fixed-window counter per key: allow N requests per window, reject the rest with 429 and a
Retry-Afterheader. The key is usually the API key or user ID, with the client IP as a fallback. Behind a proxy you must configure trusted proxies so you read the real IP.Weaknesses: a client can burst at the boundary between windows and get nearly double the limit; the in-memory map grows with distinct keys and needs cleanup; and each process counts separately, so four instances allow four times the limit. Sliding window or token bucket smooth the burst, and a shared store such as Redis fixes the multi-instance problem.
Rate limiting is also a defence for login endpoints. See the rate limiting course.
js function createLimiter({ limit, windowMs, now = Date.now }) { const hits = new Map(); // key -> { count, resetAt } return function allow(key) { const t = now(); const entry = hits.get(key); if (!entry || t >= entry.resetAt) { hits.set(key, { count: 1, resetAt: t + windowMs }); return true; } if (entry.count >= limit) return false; entry.count++; return true; }; } let clock = 0; const allow = createLimiter({ limit: 3, windowMs: 1000, now: () => clock }); const results = []; for (let i = 0; i < 5; i++) results.push(allow("203.0.113.9")); clock = 1000; // next window results.push(allow("203.0.113.9")); console.log(results);Output
[ true, true, true, false, false, true ]
What the interviewer is checking: Whether you can build a basic limiter and name its real weaknesses, especially across instances.
Likely follow-up: Compare token bucket with sliding window. When would you pick each?
19How do you choose HTTP status codes for errors? Compare 400 and 422, 401 and 403, 404 and 409.Intermediate
Use the code that tells the client what to do next. 400 means the request is malformed; 422 is commonly used when the syntax is fine but the content fails validation rules. Teams differ, so pick one convention and document it.
401 means "I do not know who you are": missing or invalid credentials. 403 means "I know who you are and you may not do this". 404 means the resource does not exist, and is sometimes used deliberately instead of 403 so you do not reveal that a record exists. 409 signals a conflict with current state, such as a duplicate booking or a stale version.
Return a consistent JSON error body with a machine-readable code, a human message and field details, and never use 200 for failures. Rate limits are 429. The REST API course covers error design.
What the interviewer is checking: Judgement about API contracts and consistency, not memorising a table.
Likely follow-up: How would you design error responses so mobile clients can show a field-level message?
20Offset or cursor pagination for a list endpoint? How do you decide?Intermediate
Offset pagination (
?page=3&limit=20, orOFFSETin SQL) is simple and lets users jump to a page. But the database still has to walk past all skipped rows, so deep pages get slow, and if rows are inserted or deleted while the user pages, items are skipped or repeated.Cursor (keyset) pagination returns an opaque token that encodes the last item's sort key, for example
created_atplusid, and the next query asks for rows after it. It stays fast on large tables with an index and is stable under inserts, but you cannot jump to page 50 and the sort must be deterministic, so always add a unique tiebreaker.Use offset for small admin tables, cursors for feeds and large datasets. Return the next cursor in the response body or a
Linkheader.What the interviewer is checking: Whether you can connect an API design choice to database behaviour and consistency.
Likely follow-up: How would you support sorting by a non-unique column such as price with cursor pagination?
21When would you use the cluster module and when worker_threads? What is the difference?Intermediate
clusterforks several full Node processes that can share one server port, so you use all CPU cores for handling HTTP requests. Each process has its own memory, so an in-memory cache, session map or counter is not shared between them.worker_threadsruns extra threads inside one process, each with its own V8 isolate and event loop. It suits CPU-heavy jobs like image processing, parsing or hashing, since the main thread stays responsive. Threads communicate by message passing, transferable buffers orSharedArrayBuffer.Neither helps I/O-bound code, which the event loop already handles. In containerised deployments many teams skip
clusterand run one process per container, letting the orchestrator scale replicas. Usechild_processwhen you need to run an external program.What the interviewer is checking: Whether you can match the tool to the bottleneck and know the shared-state consequence.
Likely follow-up: Why will an in-memory session store break as soon as you enable cluster mode?
22Your API's latency spikes for every endpoint at once and one CPU core is pinned. How do you diagnose and fix it?Advanced
That pattern points to the event loop being blocked by CPU work on the main thread. Confirm it by measuring event-loop delay (
perf_hooks.monitorEventLoopDelay) and by taking a CPU profile with--cpu-profor the inspector, then read the flame graph for the hot function.Usual culprits: synchronous crypto or file calls, parsing or serialising huge JSON, large in-memory sorts, and regular expressions with catastrophic backtracking. Fixes, in order: remove the work or make the algorithm cheaper; split it into chunks and yield with
setImmediate; move it to a worker thread pool; or push it to a queue and a separate service.The sample hands a Fibonacci calculation to a worker, so the main thread keeps serving. In real code reuse a pool of workers instead of spawning one per request.
js const { Worker, isMainThread, parentPort, workerData } = require("node:worker_threads"); function fib(n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); } if (isMainThread) { const worker = new Worker(__filename, { workerData: 32 }); worker.on("message", (v) => console.log("main: result", v)); console.log("main: still free to serve requests"); } else { parentPort.postMessage(fib(workerData)); }Output
main: still free to serve requests main: result 2178309
What the interviewer is checking: A systematic approach: measure, find the hot path, then pick the least costly fix.
Likely follow-up: How would you detect and prevent a ReDoS-prone regular expression in code review?
23A Node service's memory climbs steadily until it is killed. How do you find the leak?Advanced
First confirm it is a leak, not normal growth: watch
process.memoryUsage().heapUsedover time under steady load. Memory that climbs and never falls after garbage collection is a leak; a plateau is just a large working set.Then compare heap snapshots. Take one, apply load, take another (via
--inspectand Chrome DevTools, orv8.writeHeapSnapshot()), and look at which object types grew and what retains them.Common causes are unbounded caches or maps, listeners added per request and never removed, closures that capture large objects, arrays that only grow, and timers that are never cleared. The listener case is easy to spot because Node prints a
MaxListenersExceededWarningonce an emitter passes ten listeners for one event. Fix with bounded caches (LRU or TTL), removing listeners, orWeakMapfor per-object metadata.What the interviewer is checking: Whether you have a method (confirm, snapshot, diff, fix) and know the usual suspects.
Likely follow-up: What is the difference between a memory leak and the process simply needing a larger heap limit?
24How would you implement graceful shutdown for a Node HTTP server running behind a load balancer?Advanced
On
SIGTERM, stop taking new work but finish what is in flight. Fail the readiness probe first, so the load balancer stops routing to you, then callserver.close(), wait for active requests to finish, close database pools and queue consumers, and exit.server.close()only calls its callback once every connection has ended, and idle keep-alive sockets can hold it open, so also callserver.closeIdleConnections(), available in recent versions. Add a hard deadline timer so a stuck request cannot block the deploy forever.The pitfall is calling
process.exit()immediately, which kills in-flight requests. Make the deadline timerunrefed so it does not keep the process alive on its own.What the interviewer is checking: Production awareness: signals, draining, readiness and a hard deadline.
Likely follow-up: How does WebSocket traffic change your shutdown plan?
25How do you make a POST endpoint that charges a payment safe against client retries?Advanced
Use an idempotency key. The client generates a unique key per logical operation and sends it in a header such as
Idempotency-Key. The server records the key with the result of the first execution, and any retry with the same key returns that stored result instead of charging again.Two details matter. Concurrent duplicates must collapse too, so reserve the key atomically (a unique constraint or a compare-and-set) before doing the work. The sample stores the promise for that reason. And reject a reused key with a different request body, typically with 409 or 422.
Production storage must be shared and durable, such as a database table or Redis with a TTL, not process memory, because retries can hit another instance.
js const results = new Map(); // idempotency key -> promise of the first result let charges = 0; async function charge(amount) { charges++; await new Promise((r) => setTimeout(r, 10)); return { chargeId: "ch_" + charges, amount }; } function createPayment(key, amount) { if (!results.has(key)) results.set(key, charge(amount)); return results.get(key); // a retry (even a concurrent one) gets the same promise } (async () => { const [a, b] = await Promise.all([createPayment("k-1", 500), createPayment("k-1", 500)]); console.log(a.chargeId, b.chargeId, "charges made:", charges); })();Output
ch_1 ch_1 charges made: 1
What the interviewer is checking: Whether you can turn an at-least-once network into effectively-once behaviour and handle the race.
Likely follow-up: What should happen if the first attempt failed with a server error? Do you store that outcome?
26How do you add timeouts and cancellation to outbound calls in Node.js?Advanced
Every outbound call needs a deadline. Without one, a slow dependency makes requests pile up, holding sockets and memory until your service falls over with it. The modern tool is
AbortController: pass itssignalto the operation and callabort()when time is up or the client disconnects.AbortSignal.timeout(ms)creates a signal that aborts itself, andfetch, timers/promises, streams andfs/promisesall accept asignal. An aborted operation rejects with anAbortError, which you usually treat differently from a real failure.Remember that aborting only stops your side. The remote server may already have performed the work, which is why retries on non-idempotent calls need idempotency keys.
What the interviewer is checking: Resilience thinking: deadlines everywhere, and awareness that cancellation is not rollback.
Likely follow-up: How would you propagate one request-level deadline to several downstream calls?
27Your Node API works on one server but misbehaves on four behind a load balancer. What usually breaks, and how do you fix it?Advanced
The cause is almost always state kept in process memory. Sessions, caches, rate-limit counters, WebSocket rooms, uploaded files on local disk and scheduled jobs all behave differently when requests land on different instances. Cron-style jobs may run four times.
Make the services stateless and move shared state out. Use stateless tokens such as JWTs (see the JWT authentication course) or a shared session store; keep counters and caches in Redis; send uploads to object storage; run schedulers on one elected instance or in a dedicated worker.
WebSockets need either sticky routing or a pub/sub backplane so a message published on one instance reaches clients connected to another. Test with at least two instances locally, because single-instance testing hides all of this.
What the interviewer is checking: Whether you think in terms of horizontal scaling and can list concrete stateful features.
Likely follow-up: How do you roll out a change to a shared cache's data format with zero downtime?
Fill the gaps with a free course
If one of these Node.js answers felt shaky, these 3 courses cover the same ground step by step: Node.js, REST API Development, JWT Authentication.
More interview questions
- Java
- Spring Boot
- Python
- JavaScript
- React
- C++
- Data Structures & Algorithms
- SQL & Databases
- System Design
- DevOps
- AWS & Cloud
- Security
- Generative AI & LLMs
- Machine Learning
- Behavioral & HR
Practising Node.js for a real opening? Upload your resume to see which open jobs list Node.js skills you already have, or browse open roles.