This page gives you 28 Node.js interview questions with model answers about the runtime and the services built on it: event loop phases, streams and backpressure, the libuv thread pool, worker threads, shutdown, observability and upgrades across the Node 22 and 24 LTS lines. Language semantics are on our JavaScript page. At Ryz, senior Node.js engineers are sourced by recruiters, assessed in structured NTRVSTA AI interviews on runtime behavior and service design, and reviewed by recruiters before and after. AI scores are advisory, and every hiring decision is made by people.
Most Node.js interviews stop at "Node is single-threaded and non-blocking", which tells you nothing. Use the runtime section to find out whether a candidate knows what actually runs where, and the senior section to see whether they have operated Node services under real traffic.
setImmediate(() => console.log("immediate"));
setTimeout(() => console.log("timeout"), 0);
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));
console.log("sync");
"sync", "nextTick", "promise", then "timeout" and "immediate" in an order that is not guaranteed from the main module. The loop runs phases in order: timers, pending callbacks, poll (I/O), check (setImmediate) and close callbacks. The nextTick queue and then promise microtasks drain between callbacks. Inside an I/O callback, setImmediate always fires before a zero-delay timer.
What a strong answer shows: They know the timeout versus immediate order is not deterministic at the top level, and that in an ES module promise callbacks can run before nextTick callbacks because module evaluation is itself asynchronous.
That endpoint blocks the event loop with synchronous work: JSON.parse on a huge body, a sync fs call, crypto.pbkdf2Sync, a catastrophic regex, or a big loop. While it runs, no other request is processed. Confirm with event loop delay metrics from perf_hooks.monitorEventLoopDelay() and a CPU profile, then make the work async, chunk it, stream it or move it to a worker thread.
What a strong answer shows: They measure event loop lag in production as a standard metric.
uncaughtException?Express 5 forwards rejected promises from async handlers to error middleware, and Fastify handles async handlers natively. In Express 4 you needed a wrapper. For uncaughtException and unhandledRejection, log with context and exit, letting the orchestrator restart the process, because state may be corrupt.
What a strong answer shows: They separate expected operational errors, handled locally, from programmer errors, which should crash.
"type": "module" or .mjs selects ESM. ESM can import CommonJS. Since Node 22.12, CommonJS can require() synchronous ES modules without a flag, which removed a long-standing migration blocker, though modules using top-level await still cannot be required. In ESM, use import.meta.dirname and import.meta.filename instead of __dirname.
What a strong answer shows: They have handled the dual-package hazard, where a package loads twice with separate state.
Global fetch (built on undici), the node:test runner with mocking and coverage, --watch, --env-file, a WebSocket client, util.parseArgs, util.styleText, a stable permission model and, in recent releases, built-in type stripping for TypeScript.
What a strong answer shows: They weigh fewer dependencies against features the built-ins lack, rather than replacing everything on principle.
Read environment variables once at startup, validate them with a schema, fail fast with a clear error, and export a typed, frozen config object. --env-file works for local development, while production secrets come from the platform's secret manager. Never log the whole config.
What a strong answer shows: They avoid reading process.env throughout the code, which hides dependencies and makes testing harder.
npm install and npm ci, and how do you manage supply-chain risk?npm ci installs exactly what the lockfile says and fails if package.json disagrees, so CI builds are reproducible. For supply-chain risk: commit the lockfile, review dependency diffs, run npm audit or a scanner, avoid install scripts from unknown packages, and pin exact versions for critical tooling.
What a strong answer shows: They have reacted to a compromised package and know how to find where it is used transitively.
On SIGTERM, fail the readiness probe so traffic drains, stop accepting new connections, let in-flight requests finish, close database pools and queue consumers, then exit. Add a hard deadline below the pod's termination grace period.
process.once("SIGTERM", () => {
isReady = false; // readiness probe now returns 503
server.close(async (err) => {
await Promise.allSettled([pool.end(), consumer.stop()]);
process.exit(err ? 1 : 0);
});
setTimeout(() => process.exit(1), 25_000).unref();
});
What a strong answer shows: They make sure Node is PID 1 or behind an init that forwards signals (not npm start), and handle keep-alive connections that hold server.close open.
import { AsyncLocalStorage } from "node:async_hooks";
import { randomUUID } from "node:crypto";
import pino from "pino";
export const requestContext = new AsyncLocalStorage();
app.use((req, res, next) => {
const requestId = req.get("x-request-id") ?? randomUUID();
requestContext.run({ requestId }, next);
});
export const log = pino({
mixin: () => ({ requestId: requestContext.getStore()?.requestId }),
});
AsyncLocalStorage carries a store across async boundaries started inside run. Pino's mixin option adds it to every log line.
What a strong answer shows: They know OpenTelemetry uses the same mechanism for trace context and that some callback-based libraries can lose the context.
Pool exhaustion: connections checked out and not released, often from a code path that throws before release, or long transactions holding connections. Check pool metrics (total, idle, waiting), set an acquire timeout so requests fail fast, and use a helper that releases in finally.
What a strong answer shows: They size pools against the database's connection limit across all replicas.
Always set timeouts: fetch(url, { signal: AbortSignal.timeout(3000) }) or undici's dispatcher options for connect and body timeouts. Reuse connections with keep-alive, retry only idempotent requests with backoff and jitter, and add a circuit breaker for dependencies that fail slowly.
What a strong answer shows: They know that without a timeout a slow dependency exhausts sockets and memory in the caller.
Unit tests with node:test or Vitest, HTTP tests by injecting requests (Fastify's inject, or supertest), a real Postgres or Redis via Testcontainers, and fake timers for retry logic. Mock external HTTP at the network layer, for example with undici's MockAgent.
What a strong answer shows: They build the app with a factory function so tests can create isolated instances.
console.log in production?console.log can block when writing to files or pipes, and it is unstructured and has no levels. Pino writes structured JSON quickly, supports child loggers and redaction, and can move formatting to a separate transport worker.
What a strong answer shows: They redact tokens and personal data at the logger, and keep log volume under control on hot paths.
Express has the largest ecosystem and Express 5 fixed async error handling. Fastify is faster and has schema-based validation and serialization built in. NestJS adds structure, dependency injection and modules, which helps large teams but brings decorators and more abstraction.
What a strong answer shows: They decide based on team size and existing code, not benchmarks alone.
Backpressure happens when a writable consumer is slower than the readable producer. write() returns false when the buffer passes highWaterMark, and the producer should pause until 'drain'. Ignoring it buffers everything in memory. stream.pipeline handles backpressure, errors and cleanup for you.
import { pipeline } from "node:stream/promises";
import { createReadStream, createWriteStream } from "node:fs";
import { createGzip } from "node:zlib";
await pipeline(
createReadStream("events.log"),
createGzip(),
createWriteStream("events.log.gz"),
);
What a strong answer shows: They explain why .pipe() chains leak file descriptors on error and prefer pipeline.
Stream from a database cursor, transform rows into CSV lines with an async generator, and pipe to the response so memory stays flat.
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
async function* toCsv(rows) {
yield "id,email,total\n";
for await (const r of rows) {
yield `${r.id},${csvEscape(r.email)},${r.total}\n`;
}
}
app.get("/export.csv", async (req, res) => {
res.setHeader("content-type", "text/csv");
await pipeline(Readable.from(toCsv(streamOrders(req.query))), res);
});
What a strong answer shows: They handle client disconnects (pipeline destroys the source), escape CSV properly, and suggest an async job with a download link for very large exports.
libuv runs certain work on a thread pool with a default of four threads: most fs operations, dns.lookup, async crypto such as pbkdf2 and scrypt, and async zlib. Many concurrent hashes occupy every thread, so file reads queue behind them. Network sockets do not use the pool. Options: raise UV_THREADPOOL_SIZE at startup, limit concurrent hashing, or move hashing to dedicated worker threads.
What a strong answer shows: They know which APIs use the pool and that dns.lookup surprises people.
Worker threads for CPU-bound JavaScript that needs to share memory or exchange data quickly, such as image processing or parsing, usually through a pool like Piscina. Child processes for running other programs or isolating crash-prone work. Cluster forks processes sharing a port, which in containers is usually replaced by running more replicas.
What a strong answer shows: They account for message-passing cost and use transferList or SharedArrayBuffer to avoid copying large buffers.
Check whether growth is in the V8 heap or outside it (Buffers, native modules) with process.memoryUsage(). For heap growth, take heap snapshots over time or use --heapsnapshot-near-heap-limit, and compare retained objects. Common causes: unbounded caches, listeners added per request, closures held by timers. Size --max-old-space-size below the container limit.
What a strong answer shows: They know recent Node versions take the container's memory limit into account when sizing the heap by default, and still verify the actual limit.
Reproduce under load with autocannon, record with --cpu-prof or the inspector, and read the flame graph for wide frames. Typical finds: serialization of large objects, logging, validation on hot paths, and regex.
What a strong answer shows: They profile with production-like data, not an empty test database.
EventEmitter pitfalls?An emitted 'error' event with no listener throws and can crash the process. Listeners added per request and never removed leak memory and trigger MaxListenersExceededWarning. events.once and events.on with an AbortSignal integrate emitters with async code cleanly.
What a strong answer shows: They treat the max listeners warning as a leak signal, not something to silence.
Persist events, enqueue deliveries in a durable queue (SQS, or BullMQ on Redis), and run workers with per-destination concurrency limits so one slow customer cannot block others. Retry with exponential backoff and jitter, sign payloads with HMAC and timestamps, include an idempotency key, and move repeatedly failing deliveries to a dead-letter queue.
What a strong answer shows: Isolation between tenants, SSRF protection on customer URLs, and observability per destination.
Keep HTTP handlers stateless and scale replicas horizontally. WebSocket connections are stateful, so use a shared pub/sub layer such as Redis or NATS to broadcast across instances, configure load balancer idle timeouts, and plan reconnection with backoff for deploys.
What a strong answer shows: They plan for connection counts per instance and draining long-lived connections during deploys.
Read the changelogs for breaking changes, update base images in one place, run each service's tests on the new version in CI, upgrade native modules first, and roll out by risk with canaries while watching latency, memory and errors. Track progress per service.
What a strong answer shows: They plan for native addons and transitive dependencies with engine constraints, and keep a rollback image.
Prototype pollution through deep merges, ReDoS from user-controlled regex input, SSRF in URL-fetching features, path traversal in file handlers, command injection via child_process.exec (prefer execFile with argument arrays), and unsafe deserialization. The permission model (--permission) can restrict file system and child process access.
What a strong answer shows: They tie each risk to a concrete code pattern and a fix.
OpenTelemetry with auto-instrumentation loaded before application code, via --import for ESM, so HTTP, database and queue clients are patched. Add event loop delay, heap usage and GC metrics, structured logs with trace IDs, and alerts on latency and error rate per route.
What a strong answer shows: They know instrumentation loaded after the modules it patches silently does nothing.
Heavy CPU-bound computation, large in-memory numeric work, or workloads where a team's existing language and libraries are a better fit. Node does well at I/O-heavy APIs and gateways, real-time features and teams that share code with a TypeScript front end.
What a strong answer shows: Honest trade-offs instead of defending a favorite tool.
A slim or distroless base, a non-root user, npm ci --omit=dev in a multi-stage build, node as the entry point rather than npm so signals arrive, health and readiness endpoints, and memory limits matched to heap settings.
What a strong answer shows: They have debugged a container that ignored SIGTERM and know why.
uncaughtException and keeps the process running..pipe() chains without error handling.npm start and wonders why shutdown hangs.exec with user-supplied strings.Give a 3-hour take-home: build a small file-processing service. An endpoint accepts a large NDJSON upload, streams it through validation and a transform that enriches each record by calling a slow mock HTTP service with a concurrency limit, and writes results to a gzipped output file. Include a status endpoint, graceful shutdown that finishes or checkpoints in-flight work, and a load script that uploads a 1 GB file. Memory must stay roughly flat.
pipeline, with backpressure respected end to end.SIGTERM with no corrupted output.Ryz places Node.js engineers who have run services like these in production. Hire senior Node.js developers drawn from the top 1% of the candidates we interview, on your team and in your repos, working within ±1h of US time zones. You can review how our vetting process works, or adapt our Node.js developer job description for your own hiring.
The current LTS lines, Node 22 and Node 24, cover what most production teams run. A senior candidate should know what changed recently, such as require() of ES modules, built-in test tooling and the permission model, and how they plan version upgrades.
Use a take-home or pairing task that runs real code under load, such as streaming a large file or handling shutdown. Ask the candidate to share their terminal and show memory or event loop measurements, which is hard to fake.
Mid-level engineers build working endpoints. Senior engineers know what happens to those endpoints under load, during deploys and when dependencies fail, and they design timeouts, backpressure and observability in from the start.
Questions we didn't answer? Email info@ryzlabs.com.