Node.js interviews test whether you understand the single-threaded, non-blocking model and its consequences. Expect questions on the event loop phases, the difference between process.nextTick and setImmediate, streams and backpressure, error handling across callbacks, promises and EventEmitters, and how to scale across CPU cores. Employers also probe production experience: memory leaks, blocking the event loop, and securing an API. The questions below cover the runtime, the Express layer and the operational concerns.
Behavioural Questions
1. Tell me about a Node.js service you built or maintained. What was it responsible for and what was hard about it?
Note: Give scale numbers. "An API" tells the interviewer nothing; "an order service handling about 400 requests a second at peak" tells them what problems you have actually met.
Cover four things in order:
- What it did and who depended on it. One sentence. Was it user-facing, or a background worker other services relied on?
- The shape of the load. Steady traffic, spiky traffic, long-running jobs, or lots of small I/O. This determines every interesting decision that follows.
- The genuinely hard part. Good candidates: keeping the event loop unblocked when a CPU-heavy task appeared, handling a downstream service that became slow rather than failing outright, memory growth you had to trace, or making retries safe when the same message could arrive twice.
- What you would do differently. This is the part that separates a senior answer from a junior one.
2. Describe a production incident you were involved in. How did you diagnose it and what did you change afterwards?
Tell it as a timeline, and resist the urge to make yourself the hero.
- Detection. What alerted you — error rate, latency, memory, or a customer? If it was a customer, say so; that itself was a finding.
- Triage. What you did to stop the bleeding before you understood the cause. Rolling back, scaling out, disabling a feature flag, or shedding load are all legitimate first moves.
- Diagnosis. The evidence you used: structured logs with a correlation id, a heap snapshot, event loop lag metrics, or a flame graph from
--prof. - The fix, and then the real fix. The immediate patch, and the systemic change — a timeout that was missing, a circuit breaker, a bounded queue, or a health check that actually checked something.
Note: Mention the blameless postmortem if your team ran one. Interviewers read that as maturity, and it lets you describe a failure without it sounding like someone was at fault.
3. How do you decide when a Node.js service should be split apart, or when separate services should be merged?
Answer with criteria rather than a preference for microservices or monoliths — strong candidates are known for resisting both dogmas.
Reasons to split:
- Different scaling profiles. A video transcoder and a login endpoint do not want the same instance count or the same machine.
- Different failure requirements. If the reporting dashboard falling over must never take payments down, that boundary is real.
- Different teams and release cadences. If two teams block each other on every deploy, the seam is organisational as much as technical.
Reasons to merge, or to never split:
- The two services always change together, so every feature becomes a two-repository, two-deploy exercise.
- They share a database, in which case they are one service wearing two hats.
- You cannot afford the operational cost — tracing, deploy pipelines, and on-call all multiply.
Note: Saying you would start with a well-structured single service and split when a specific pain appears is a defensible and increasingly common position.
4. How do you approach onboarding to an unfamiliar Node.js codebase?
Describe a repeatable method, because you will be doing exactly this in the first week of the job.
- Get it running first. Before reading anything, install, run, and hit one endpoint. What is missing from the README is itself the first useful contribution.
- Follow one request end to end. Pick a real route and trace it through routing, middleware, service layer, and database. One vertical slice teaches you more than reading every folder.
- Read the tests and the package.json. Tests document intent, and the dependency list tells you the architecture — an ORM, a queue library, and a validation library between them describe the shape of the system.
- Find out how it is deployed and observed. Where the logs go, what the alerts are, how a rollback works.
- Start with a small real change. A bug fix teaches you the workflow; reading teaches you the map.
Note: Mention writing down what confused you and turning it into documentation. It is a genuinely valued habit and easy to demonstrate.
5. How do you balance shipping quickly against writing code that will be maintainable?
The honest answer is that it depends on how reversible the decision is, and saying so directly is stronger than claiming you always do both.
Where speed is fine: anything cheap to change later — internal tooling, a feature behind a flag, a first version of an endpoint whose shape nobody has agreed yet. Here the risk of over-designing for requirements that never arrive is higher than the risk of rework.
Where it is not: decisions that are expensive to reverse. Database schemas, public API contracts, message formats, and anything that touches money or authentication. These deserve the extra day.
How to keep the trade-off visible:
- Write the shortcut down where it will be found — a TODO with a ticket number, not a comment nobody reads.
- Keep tests on the parts that are hard to verify by hand, even when you are moving fast.
- Raise it as a cost, not a complaint: "we can ship Thursday, and it will cost us about two days in January."
Technical Questions
1. How does the Node.js event loop work, and what are its phases?
Node runs your JavaScript on one thread and hands I/O to the operating system through libuv. The event loop is what decides which completed callback runs next, and it cycles through fixed phases:
- timers — callbacks scheduled by
setTimeoutandsetIntervalwhose time has come. - pending callbacks — some system-level callbacks deferred from the previous cycle.
- idle, prepare — internal use only.
- poll — the important one. Node retrieves new I/O events and runs their callbacks, and will block here waiting for work if there is nothing else to do.
- check —
setImmediatecallbacks. - close callbacks —
'close'events, such as a socket being destroyed.
Between every phase, and between individual callbacks, Node drains two queues completely: process.nextTick first, then the promise microtask queue.
Note: The consequence worth stating is that any CPU-heavy synchronous work blocks the entire loop — no other request is served while it runs. That is why hashing, image processing, or large JSON parsing belong in a worker thread or a separate process.
2. What is the difference between process.nextTick, setImmediate and setTimeout with zero delay?
They are three different queues with three different priorities, and the distinction comes up constantly in Node interviews.
process.nextTick— not part of the event loop at all. Its queue is drained after the current operation completes and before the loop continues to the next phase. It has the highest priority, and it runs before promise microtasks.setImmediate— runs in the check phase, which is immediately after the poll phase. If you want to yield to I/O and then run, this is the correct choice.setTimeout(fn, 0)— runs in the timers phase. The zero is treated as one millisecond, so it is not really immediate.
The subtlety interviewers look for: at the top level of a script, the order of setTimeout(fn, 0) and setImmediate(fn) is non-deterministic, because it depends on how long the process took to start. Inside an I/O callback, however, setImmediate always fires first, because the loop is already past the timers phase and heading into check.
Note: Recursive process.nextTick calls will starve the event loop entirely. setImmediate will not.
3. What are streams in Node.js, what types exist, and why would you use one instead of reading a whole file?
A stream processes data in chunks as it arrives, instead of loading everything into memory first. Reading a two gigabyte file with fs.readFile needs two gigabytes of RAM; reading it as a stream needs only the size of one chunk.
The four types:
- Readable — a source you consume from, such as
fs.createReadStreamor an incoming HTTP request. - Writable — a destination, such as
fs.createWriteStreamor an HTTP response. - Duplex — both, with the two sides independent. A TCP socket is the standard example.
- Transform — a duplex stream where the output is a function of the input, such as
zlib.createGzip().
Backpressure is the reason streams are worth understanding. If the destination is slower than the source, data piles up in memory. pipe, and better still pipeline, handle this for you by pausing the source when the destination's buffer is full.
const { pipeline } = require('stream/promises');
await pipeline(
fs.createReadStream('in.csv'),
zlib.createGzip(),
fs.createWriteStream('out.csv.gz')
);Note: Prefer pipeline over pipe. pipe does not forward errors or clean up the remaining streams when one fails, which leaks file descriptors.
4. How do you handle errors in Node.js, including in async code and at the process level?
Node has several error channels and you need to cover all of them.
- Synchronous code —
try/catch. - Promises and async/await —
try/catcharound theawait, or.catch()on the chain. In Express 4 an async route handler that throws is not caught by the framework, so you need a wrapper that forwards the rejection tonext(err); Express 5 handles it natively. - Callbacks — the error-first convention,
(err, result). Checkerrbefore anything else and return early. - EventEmitters — listen for the
'error'event. An unhandled'error'event throws and crashes the process, which is deliberate.
At the process level: attach handlers for uncaughtException and unhandledRejection, but use them only to log and then exit cleanly — not to keep running. After an uncaught exception the process is in an unknown state, and continuing risks corrupting data.
Note: Distinguish operational errors, such as a failed network call, which you handle and retry, from programmer errors such as a TypeError, which you should let crash so a supervisor restarts the process.
5. What is the difference between CommonJS and ES modules in Node.js?
They are two module systems with different loading semantics, and mixing them is where the pain lives.
- Syntax. CommonJS uses
require()andmodule.exports; ES modules useimportandexport. - Timing.
requireis synchronous and can be called anywhere, including conditionally inside a function.importis static and hoisted — the module graph is resolved before any code runs, which is what makes tree-shaking possible. For a conditional load you need dynamicimport(), which returns a promise. - Bindings. CommonJS gives you a copy of the exported value at the time of the require. ES modules give you a live binding, so if the exporting module reassigns the variable later, importers see the new value.
- Enabling them. Either
"type": "module"in package.json, or the.mjsextension. Under ESM there is no__dirname,__filename, orrequire; the equivalents come fromimport.meta.url.
Note: An ES module can import a CommonJS module, but CommonJS cannot require an ES module — because require is synchronous and ESM resolution is asynchronous. That asymmetry is the single most common migration problem.
6. How does middleware work in Express, and how would you write your own?
Express middleware is a function with the signature (req, res, next). Express keeps an ordered stack of them and passes the request down that stack; each one can read or modify req and res, then either end the response or call next() to hand control on.
function requestTimer(req, res, next) {
const start = process.hrtime.bigint();
res.on('finish', () => {
const ms = Number(process.hrtime.bigint() - start) / 1e6;
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${ms.toFixed(1)}ms`);
});
next();
}
app.use(requestTimer);The rules that matter:
- Order is everything.
app.useregisters in sequence, so a body parser must come before the route that readsreq.body, and an auth check must come before the handler it protects. - Forgetting
next()hangs the request — no error, just a client waiting until it times out. - Error middleware takes four arguments,
(err, req, res, next). Express identifies it by arity, and it must be registered last.
7. How would you scale a Node.js application across CPU cores, and what is the cluster module?
A single Node process uses one core for JavaScript, so on an eight-core machine you are using an eighth of the hardware. There are three answers, and the best one depends on where you deploy.
- The cluster module forks one worker process per core. The primary process holds the listening socket and distributes incoming connections; the workers each run your full application. Because they are separate processes they share nothing, so in-memory state and sessions must move to Redis or a database.
- A process manager such as PM2 in cluster mode does the same thing with restarts, zero-downtime reloads, and monitoring included.
- Container replicas. In Kubernetes or ECS the usual approach is one Node process per container and let the orchestrator run several containers — it gives you the same parallelism plus scheduling and health checks.
Worker threads are a different tool. Cluster scales I/O-bound work across processes; worker_threads moves a CPU-bound task off the main thread within one process, and can share memory through SharedArrayBuffer.
Note: Say explicitly that clustering does not fix a blocked event loop. If one request spends 500ms hashing, clustering just gives you eight event loops to block.
8. What are some common security issues in a Node.js API and how do you defend against them?
Work through the layers rather than listing tools.
- Injection. Never build SQL by string concatenation — use parameterised queries or an ORM. For NoSQL, reject object-valued query parameters, which is how
{ $gt: '' }becomes an authentication bypass. - Input validation. Validate and coerce every request body and query parameter against a schema at the edge, with something like Zod or Joi. Anything unvalidated eventually reaches a database or a shell.
- Authentication and sessions. Hash passwords with bcrypt or argon2, never a plain digest. Keep JWT lifetimes short, use refresh tokens, and store them in
httpOnly,secure,sameSitecookies rather than localStorage. - Rate limiting and payload limits. Cap request body size and rate limit authentication endpoints specifically, or you are shipping a credential-stuffing target.
- Headers and transport. Use helmet for sensible defaults, enforce HTTPS, and configure CORS to a real allowlist rather than
*. - Dependencies.
npm auditin CI, lockfiles committed, and as few transitive dependencies as you can manage. - Secrets. In environment variables or a secret manager, never in the repository.
9. How do you debug a memory leak in a Node.js process?
Approach it as measure, capture, compare, fix.
- Confirm it is real. Track
process.memoryUsage().heapUsedover hours. Node's heap grows and then plateaus normally; a leak keeps climbing and does not come back down after garbage collection. Run with--expose-gcand force a collection to be sure. - Capture heap snapshots. Start with
--inspectand take a snapshot in Chrome DevTools, or usev8.writeHeapSnapshot(). Take one early, apply load, take another. - Compare them. The comparison view sorts objects by what was allocated and never freed. Look at retained size and follow the retainer path to whatever is still holding the reference.
The usual culprits:
- A module-level array or Map that only ever grows — caches without eviction are the number one cause.
- Event listeners added per request but never removed, which is why Node warns at eleven listeners.
- Timers that are never cleared.
- Closures capturing a large object and stored somewhere long-lived.
Note: Mention WeakMap and WeakRef as the structural fix when you need to associate data with an object without keeping it alive.
10. What is the difference between blocking and non-blocking code in Node.js, and how do you keep the event loop responsive?
Blocking means the JavaScript thread cannot do anything else until the operation finishes. Non-blocking means the work is handed off and a callback runs when it completes. Because Node serves every request on one thread, one blocking call delays every other user.
What blocks:
- The synchronous
fsmethods —readFileSync,writeFileSyncand friends. - CPU-heavy work: hashing with a high cost factor, image resizing, large
JSON.parse, sorting a huge array, complex regular expressions on long strings. child_process.execSync.
How to keep the loop free:
- Use the promise-based
fsAPI for anything on a request path. Sync calls are acceptable only at startup. - Move CPU-bound work to
worker_threads, or to a queue consumed by a separate process. - Break long loops into chunks that yield with
setImmediate. - Stream large payloads rather than buffering them.
Note: Say how you would detect it. Measuring event loop lag — with perf_hooks monitorEventLoopDelay or a simple timer drift check — and alerting on it is what turns this from theory into operations.





