Login to manage your account

Please enter a valid email address.
Forgot Password?
Please enter a valid password.
OR

Don't have an account yet? Sign up

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.

jobs available in NodeJS
View jobs

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 setTimeout and setInterval whose 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.
  • checksetImmediate callbacks.
  • 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.

Free workshop by Jobaaj Learnings

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.createReadStream or an incoming HTTP request.
  • Writable — a destination, such as fs.createWriteStream or 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 codetry/catch.
  • Promises and async/awaittry/catch around the await, 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 to next(err); Express 5 handles it natively.
  • Callbacks — the error-first convention, (err, result). Check err before 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() and module.exports; ES modules use import and export.
  • Timing. require is synchronous and can be called anywhere, including conditionally inside a function. import is 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 dynamic import(), 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 .mjs extension. Under ESM there is no __dirname, __filename, or require; the equivalents come from import.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.use registers in sequence, so a body parser must come before the route that reads req.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, sameSite cookies 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 audit in 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().heapUsed over 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-gc and force a collection to be sure.
  • Capture heap snapshots. Start with --inspect and take a snapshot in Chrome DevTools, or use v8.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 fs methods — readFileSync, writeFileSync and 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 fs API 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.

Login to manage your account

Please enter a valid email address.
Forgot Password?
Please enter a valid password.
OR

Don't have an account yet? Sign up as