30 Senior Node.js Interview Questions and Answers (2026)

Node.js runs a huge share of the backends, APIs, and tooling that ships in production today. Plenty of engineers write async Node.js every day without understanding how libuv feeds the V8 engine, what actually causes event loop starvation, or how backpressure works when a fast stream outpaces a slow one.
This kind of depth is rare, and it is what puts you ahead of the other people interviewing for the same senior or staff role. Study these well and you will walk in ready to land the offer.
Q1.What is the role of libuv in Node.js, and how does it interact with the V8 engine?
libuv in Node.js, and how does it interact with the V8 engine?libuv is the C library that gives Node its event loop and asynchronous I/O: it abstracts platform differences and manages the thread pool, while V8 executes the JavaScript that schedules and consumes that work.
Provides the event loop: Drives the phases (timers, pending callbacks, poll, check, close) that decide which callbacks run next.
Cross-platform async I/O: Wraps epoll/kqueue/IOCP for sockets and a thread pool for file, DNS, and crypto work.
How it meets V8:
V8 runs the JS; when JS calls something like fs.readFile, Node's C++ bindings hand the request to libuv.
When libuv signals completion, the binding invokes the JS callback back inside V8.
Division of labor: V8 = execute/compile JavaScript; libuv = scheduling and I/O. Together they form Node's async runtime.
Q2.Explain the role of the V8 engine within Node.js and how it interacts with the C++ bindings.
V8 engine within Node.js and how it interacts with the C++ bindings.V8 is Google's JavaScript engine that compiles and runs your JS code; Node embeds it and connects it to system capabilities through C++ bindings, so JavaScript can trigger native operations it could never do on its own.
V8 executes JavaScript:
JIT-compiles JS to machine code and manages memory (the heap and garbage collection).
Knows nothing about files, sockets, or timers by itself.
C++ bindings bridge the gap: Node exposes native functions to JS; calling a built-in like fs.read crosses into C++ that talks to the OS or libuv.
Two-way flow: JS values are converted to C++ types on the way in, and results are converted back to JS values (and callbacks invoked) on the way out.
Why it matters: This embedding is what turns a browser-style engine into a server runtime with full system access.
Q3.Explain the Reactor Pattern in the context of Node.js.
Node.js.The Reactor Pattern is the design behind Node's event loop: instead of blocking on I/O, you register handlers for events and a central dispatcher (the reactor) reacts by invoking the right callback when each operation completes.
Core idea: Each I/O request is submitted with a handler, then control returns immediately; nothing waits.
The demultiplexer: The OS/libuv watches many resources at once and reports which are ready (the event demultiplexer).
The event queue and loop: Ready events with their callbacks are queued; the event loop pulls them and runs each handler one at a time.
Why Node uses it: It lets a single thread handle thousands of concurrent connections cheaply, with no thread-per-request overhead.
Q4.What is the difference between hard and soft limits in the context of Node.js process resources?
Hard and soft limits are OS-level resource ceilings (file descriptors, memory, processes) that apply to the Node process. The soft limit is the currently enforced value; the hard limit is the maximum the soft limit may be raised to without elevated privileges.
Soft limit:
The value actively enforced; hitting it triggers errors like `EMFILE` (too many open files).
A process can raise its own soft limit up to the hard limit.
Hard limit: The ceiling for the soft limit; raising it usually requires root/admin privileges.
Relevance to Node:
A server with many sockets/files can exhaust the FD soft limit; check and tune with `ulimit -n` (Linux/macOS).
Node's V8 heap has its own separate cap, adjustable via `--max-old-space-size`.
Q5.How do you implement a graceful shutdown for a Node.js server to ensure no active requests are dropped, handling SIGTERM and SIGINT?
SIGTERM and SIGINT?Graceful shutdown means: stop accepting new connections, let in-flight requests finish, close resources (DB, sockets), then exit. You trigger it by listening for the `SIGTERM` and `SIGINT` signals and calling `server.close()` with a safety timeout.
The signals:
`SIGTERM`: sent by orchestrators (Docker, Kubernetes) to ask the process to stop.
`SIGINT`: sent on Ctrl+C in the terminal.
Steps:
`server.close()` stops accepting new connections but lets active ones complete.
Close dependencies (DB pools, message queues, file handles).
Exit with code `0` once done.
Safety timeout: Force-exit after a deadline so a stuck request can't hang the shutdown forever.
Q6.Why is process.nextTick() considered potentially 'dangerous' if used recursively?
process.nextTick() considered potentially 'dangerous' if used recursively?Because the nextTick queue is fully drained before the event loop is allowed to continue, recursively scheduling process.nextTick() keeps adding work that must run first, so the loop never reaches the I/O or timer phases: a form of starvation.
The mechanism: After each operation, Node empties the entire nextTick queue before proceeding; if each callback queues another, the queue never empties.
The symptom: I/O callbacks and timers never fire, the server appears hung, yet CPU is busy: it looks like a deadlock without an error.
The safer alternative: Use setImmediate() for recursive deferral: it runs on the next loop iteration, so I/O still gets serviced between calls.
Q7.How does the 'Poll' phase differ from the 'Check' phase in the event loop?
Both are sequential phases of one event loop tick: the poll phase retrieves and runs I/O callbacks (and may block waiting for I/O), while the check phase runs setImmediate() callbacks immediately after poll completes.
Poll phase:
Executes callbacks for completed I/O (file reads, sockets, etc.).
Can block here waiting for new I/O events if there's nothing else scheduled, respecting timer deadlines.
Check phase: Runs only setImmediate() callbacks, which are designed to fire right after the poll phase.
Ordering consequence: A setImmediate() queued inside an I/O callback always runs before timers, because check immediately follows poll.
Q8.What is 'event loop starvation,' and what kind of code causes it?
Event loop starvation is when a long-running synchronous operation monopolizes the single main thread, preventing the event loop from advancing to other phases so pending I/O callbacks, timers, and requests are delayed.
What causes it:
CPU-bound work on the main thread: large loops, heavy JSON parsing, synchronous crypto/compression.
Blocking sync APIs like fs.readFileSync or execSync in a request path.
Recursive process.nextTick() or microtask floods that never let the loop reach I/O phases.
The symptom: Throughput collapses and latency spikes even though the process isn't waiting on I/O.
How to avoid it: Offload CPU work to worker_threads or a child process, chunk large tasks, and prefer async APIs.
Q9.How does Node.js handle DNS resolution, and why can it sometimes block the thread pool?
Node.js handle DNS resolution, and why can it sometimes block the thread pool?Node resolves DNS in two ways: dns.lookup() uses the OS resolver via the libuv thread pool (blocking-style), while the dns.resolve*() family uses real async network queries via c-ares. The default lookup can saturate the thread pool.
dns.lookup():
Calls the system's getaddrinfo, which is synchronous, so libuv runs it on a thread pool worker.
Used implicitly by http, https, and net connections.
Why it can block: The thread pool defaults to 4 workers (UV_THREADPOOL_SIZE); many slow DNS lookups consume threads also needed by fs and crypto.
dns.resolve(): Performs queries on the network without the thread pool, so it scales better, but bypasses the OS hosts file/config.
Mitigation: Raise UV_THREADPOOL_SIZE, cache results, or use resolve where appropriate.
Q10.What are the common causes of Event Loop Lag, and how do you monitor it?
Event loop lag is the delay between when a callback is scheduled and when it actually runs, caused by the main thread being busy. It's the key health signal that work is blocking the loop.
Common causes:
Synchronous CPU work (parsing, serialization, crypto, regex backtracking).
Blocking sync I/O calls in the request path.
Thread pool saturation from too many concurrent fs/crypto/DNS tasks.
Huge garbage collection pauses from excessive allocation.
How to monitor it:
Built-in perf_hooks.monitorEventLoopDelay() gives a high-resolution histogram.
A simple setInterval that measures drift from its expected interval.
APM tools and clinic.js / flame graphs to find the blocking code.
Q11.What is 'Backpressure' in Node.js streams, and how do you conceptually handle it?
Node.js streams, and how do you conceptually handle it?Backpressure is the mechanism that prevents a fast producer from overwhelming a slower consumer: when a writable stream's internal buffer fills, it signals the source to pause until it drains, keeping memory bounded.
The core signal:
write() returns false when the buffer exceeds highWaterMark, telling you to stop writing.
The 'drain' event fires when it's safe to resume.
Why it matters: Ignoring it lets the buffer grow unbounded, causing memory bloat and crashes.
How to handle it conceptually:
Prefer pipe() or pipeline(), which manage pause/resume automatically.
If writing manually, pause the source on false and resume on 'drain'.
Q12.What is 'object mode' in a Node.js stream, and when would you enable it?
Object mode lets a stream carry arbitrary JavaScript objects as chunks instead of only Buffer or string data. You enable it when each unit of data is a logical record (a parsed row, a JSON object) rather than raw bytes.
Default vs object mode:
By default streams emit Buffer/string chunks and highWaterMark counts bytes.
In object mode chunks can be any value (except null), and highWaterMark counts objects instead of bytes.
How to enable: Pass { objectMode: true } to the stream constructor (or readableObjectMode/writableObjectMode for one side of a Transform).
When to use it:
CSV/JSON parsers that emit one record per chunk.
Database cursors or ORM streams yielding row objects.
Transform pipelines that map, filter, or enrich structured records.
Q13.Explain the concept of 'live bindings' in ES Modules vs. 'value copies' in CommonJS.
In ES Modules an import is a live, read-only view of the exported variable, so when the exporting module updates it, importers see the new value. In CommonJS require returns a snapshot copy of whatever module.exports held at require time, so later reassignments aren't reflected.
ESM live bindings:
Imports reference the original binding, not a copy; the value tracks changes in the source module.
Imported bindings are read-only in the consumer (you can't reassign them).
CommonJS value copies:
require() captures the current value of an export; if the module later reassigns it, the consumer keeps the old reference.
Object properties still mutate (shared reference), but reassigning the primitive/binding does not propagate.
Practical effect: A counter that increments over time is visible to ESM importers but frozen at import time for CJS consumers.
Q14.How does Node.js handle circular dependencies in CommonJS?
CommonJS?In CommonJS, a circular dependency does not error: when module A requires B and B requires A back, B receives A's module.exports as it exists at that moment, which may be incomplete (a partial export).
How it works: Node registers a module in the cache before running it, so a circular require returns the partially-populated exports instead of re-executing.
The risk: If B uses A's export at load time (top level), it may get undefined because A hasn't finished assigning it yet.
Why it often still works: If the export is only used later inside a function call, A has finished by then, so the reference is complete.
Mitigation: Restructure to break the cycle, require lazily inside functions, or export an object whose properties are filled in later.
Q15.How do you handle unhandledRejection and uncaughtException in a production Node environment?
unhandledRejection and uncaughtException in a production Node environment?Listen for the process events uncaughtException and unhandledRejection, but treat them as last-resort safety nets: log the error, then let the process exit and restart, because the app is in an undefined state.
uncaughtException:
Fires for a synchronous error that bubbled up with no handler; the process is no longer trustworthy.
Log it, flush, then process.exit(1) rather than trying to resume.
unhandledRejection: Fires when a Promise rejects with no .catch(); in modern Node this also crashes the process by default.
Don't keep running: Use a process manager (pm2, Kubernetes, systemd) to restart cleanly; these handlers are for graceful logging and shutdown, not recovery.
Graceful shutdown: Stop accepting new connections, close the server and DB pools, then exit so in-flight requests finish.
Q16.How does Express handle asynchronous errors in version 5.x compared to version 4.x?
Express handle asynchronous errors in version 5.x compared to version 4.x?In Express 5.x, rejected promises returned from route handlers are caught automatically and forwarded to the error-handling middleware, while in 4.x an async error had to be passed manually to next(err) or it would be silently lost (or crash the process).
Express 4.x: no awareness of promises:
If an async handler throws or rejects, Express doesn't catch it, so it becomes an unhandled rejection.
You had to wrap handlers in try/catch and call next(err), or use a helper like express-async-handler.
Express 5.x: built-in promise rejection handling:
A handler returning a rejected promise automatically routes the error to your error middleware, as if you'd called next(err).
This applies to handlers that return a promise; synchronous throws were already caught in 4.x too.
Caveat: it only works if the handler returns the promise: A fire-and-forget async call inside a handler (not returned/awaited) still escapes Express and becomes an unhandled rejection.
Q17.What is AsyncLocalStorage, and what problem does it solve for tracking context across asynchronous calls?
AsyncLocalStorage, and what problem does it solve for tracking context across asynchronous calls?AsyncLocalStorage (from the async_hooks module) provides a way to store data that stays available throughout an asynchronous call chain, like a per-request context, without manually passing it through every function argument.
The problem it solves:
Node.js is single-threaded but handles many requests concurrently, so you can't use globals or thread-locals to track "which request am I in?" across awaits and callbacks.
Without it you'd thread a context object (request ID, user, trace ID) through every function call.
How it works:
You call als.run(store, callback), and any async operation started within that callback can retrieve the store via als.getStore(), even across promises and timers.
Each logical execution chain keeps its own isolated store.
Common uses: Request-scoped logging (attaching a trace/correlation ID), auth context, and distributed tracing.
Caveat: It has some overhead and historically relied on async hooks; use it judiciously rather than for everything.
Q18.How does the cluster module distribute incoming connections across multiple CPU cores?
cluster module distribute incoming connections across multiple CPU cores?The cluster module forks the primary process into multiple worker processes that all share the same listening server port, letting Node use multiple CPU cores; the OS or Node itself decides which worker handles each incoming connection.
Shared server socket: The primary creates the listening socket and workers share its handle, so all workers accept connections on the same port.
Two distribution strategies:
Round-robin (default on most platforms except Windows): the primary accepts connections and distributes them to workers in rotation, avoiding overload imbalance.
OS-driven: workers accept directly and the operating system schedules connections, which can lead to uneven load.
Process model: Each worker is a separate process with its own memory and event loop; they communicate with the primary via IPC, not shared memory.
Typical use: Fork roughly one worker per CPU core (os.cpus().length) to scale a stateless HTTP server across cores.
Caveat: State (sessions, in-memory caches) isn't shared across workers, so use sticky sessions or an external store like Redis.
Q19.What are the differences between worker_threads, cluster, and child_process?
worker_threads, cluster, and child_process?All three enable parallelism beyond the single main event loop, but they differ in granularity: worker_threads are lightweight threads in one process, cluster forks identical processes that share a server port, and child_process launches arbitrary external programs or scripts.
worker_threads:
Multiple threads within a single process; lowest overhead and can share memory via SharedArrayBuffer.
Best for CPU-bound JavaScript work that would otherwise block the event loop.
cluster:
Forks multiple copies of the same Node app as separate processes that share a listening socket.
Best for scaling a stateless server horizontally across CPU cores.
child_process:
Spawns any external command or another Node script as a separate process (spawn, exec, fork).
Best for running non-Node tools or isolating untrusted/external work; communication via IPC or streams.
Memory and communication: Threads can share memory; processes (cluster, child_process) are isolated and rely on message passing, making them more robust but heavier.
Q20.What are the trade-offs of using Worker Threads for CPU-intensive tasks versus offloading them to a separate microservice?
Worker Threads for CPU-intensive tasks versus offloading them to a separate microservice?Worker Threads keep CPU-bound work inside the same Node process (low latency, simple deployment, shared memory), while a separate microservice isolates the work for independent scaling and fault tolerance at the cost of network overhead and operational complexity.
Worker Threads: advantages:
Low communication latency and the ability to share memory via SharedArrayBuffer, avoiding serialization for large data.
Single deployable unit: simpler infrastructure, no extra network hop.
Worker Threads: drawbacks:
Bounded by the host machine's CPU and memory; can't scale beyond one box.
A crash or runaway thread can affect the same process; shares the deploy/release cycle.
Microservice: advantages:
Independent scaling (scale the heavy service separately) and fault isolation (a crash doesn't take down the main app).
Can use a different language/runtime better suited to the computation.
Microservice: drawbacks: Network latency, serialization cost, and the operational burden of another deployable (monitoring, versioning, security).
Rule of thumb: Use Worker Threads for moderate, in-process CPU bursts; reach for a microservice when the workload needs independent scaling, isolation, or a specialized runtime.
Q21.What is Inter-Process Communication (IPC) in Node.js, and how is it implemented between a master and worker process?
Node.js, and how is it implemented between a master and worker process?IPC is the mechanism that lets separate Node processes (which don't share memory) exchange data. Node implements it over a dedicated channel (a libuv pipe/socket) wired up automatically when you use fork() or cluster, using send() and 'message' events.
Why it's needed: Each process has its own V8 heap and memory; they cannot read each other's variables directly.
How the channel is set up:
fork() (and cluster.fork()) creates an IPC pipe between parent (master) and child (worker).
Master sends with worker.send(msg); worker receives via process.on('message', ...) and replies with process.send(msg).
Data semantics:
Messages are serialized (JSON-like / structured clone), so objects are copied, not shared.
Node can also pass handles (a server or socket) over IPC, which is how cluster shares a listening port.
Q22.What are 'zombie processes' in Node.js, and how can they be prevented when using child processes?
Node.js, and how can they be prevented when using child processes?A zombie process is a child that has finished executing but whose exit status hasn't been read (reaped) by the parent, so it lingers in the process table. In Node you prevent them by properly handling child lifecycle events and cleaning up children when the parent exits.
Why they happen:
The OS keeps a terminated child's entry until the parent acknowledges its exit; if the parent ignores it, the entry stays as a zombie.
More commonly in Node you get orphaned/leaked children: the parent exits or crashes but spawned children keep running.
How to prevent them:
Listen for 'exit' and 'close' on each child so its status is consumed.
Always handle 'error' to avoid leaking references to failed spawns.
On parent shutdown, explicitly call child.kill() and clean up in process.on('exit') / signal handlers (SIGINT, SIGTERM).
Track child PIDs so you can terminate any still alive before exiting.
Q23.How does 'Zero-downtime deployment' work conceptually with the Node.js cluster module?
Node.js cluster module?Q24.How do worker_threads share memory, and what is the role of SharedArrayBuffer?
worker_threads share memory, and what is the role of SharedArrayBuffer?Q25.How do you identify and diagnose a memory leak in a Node.js application?
Node.js application?Q26.How does the V8 garbage collector work in Node.js (Scavenge vs. Mark-Sweep)?
V8 garbage collector work in Node.js (Scavenge vs. Mark-Sweep)?Q27.How does Node.js handle garbage collection, and how can you detect a memory leak in a running process?
Q28.What is the ReDoS (Regular Expression Denial of Service) attack, and why is it dangerous for Node.js?
Q29.What is 'Prototype Pollution' in the context of Node.js, and how can it be mitigated?
Q30.Explain the security implications of using eval() or vm.runInContext() in a Node.js server.
eval() or vm.runInContext() in a Node.js server.