Node.js Interview Prep
Production and Scaling

Node.js Interview Questions

What They Actually Ask

LinkedIn Hook

"I have interviewed 200+ Node.js engineers in the last five years. The same 30 questions come up in 90% of loops — and most candidates fumble the same five."

Node.js interviews are not magic. There is a finite catalog of questions that hiring managers actually ask, and once you understand the catalog you can prepare for it surgically. The event loop. Streams. Cluster vs worker threads. Error handling in async code. "What happens when you call process.exit() mid-request?" "Design a rate limiter." "How would you find a memory leak in production?"

The candidates who fail are not the ones who lack experience. They are the ones who have shipped Node.js apps for years but never had to explain the runtime out loud. They know await works, but they cannot describe the microtask queue. They have used Express middleware, but they cannot explain what next(err) actually does. They have seen EMFILE: too many open files in production, but they cannot trace it back to the libuv thread pool.

In Lesson 10.5 — the final lesson of this course — I break down the 30 questions that actually get asked in Node.js interviews, plus the system-design prompts, the "what happens when..." scenarios, and the debugging walkthroughs that separate seniors from staff engineers.

Read the full lesson -> [link]

#NodeJS #InterviewPrep #BackendEngineering #SystemDesign #JavaScript #CareerGrowth


Node.js Interview Questions thumbnail


What You'll Learn

  • The 30 most common Node.js interview questions, grouped by category
  • Interviewer-quality answers that demonstrate depth, not memorization
  • How "what happens when..." scenarios test your runtime mental model
  • System-design prompts for rate limiters, file uploads, and notification systems
  • Debugging walkthroughs for memory leaks, CPU profiling, and event loop lag
  • The common mistakes that drop senior candidates to mid-level offers
  • A printable cheat sheet of every question covered

How a Node.js Interview Actually Flows

Most Node.js loops have three stages, and each stage tests a different muscle. Understanding the structure lets you calibrate your answers — a screen wants crisp definitions, while a system-design round wants tradeoffs.

+---------------------------------------------------------------+
|           THE TYPICAL NODE.JS INTERVIEW LOOP                   |
+---------------------------------------------------------------+
|                                                                |
|  STAGE 1: PHONE / RECRUITER SCREEN (30 min)                    |
|   - "Tell me about Node.js."                                   |
|   - "What is the event loop in one sentence?"                  |
|   - "Difference between Node and a browser runtime."           |
|   - Goal: confirm you are not lying on your resume.            |
|                                                                |
|  STAGE 2: TECHNICAL DEEP DIVE (60-90 min)                      |
|   - Event loop phases, microtasks, libuv thread pool           |
|   - Streams, buffers, backpressure                             |
|   - Async patterns, error propagation                          |
|   - Express middleware, error middleware                       |
|   - Live coding: build an endpoint, fix a bug, write a stream  |
|   - Goal: confirm you can build and debug real systems.        |
|                                                                |
|  STAGE 3: SYSTEM DESIGN (60 min)                               |
|   - "Design a rate limiter."                                   |
|   - "Design a file upload service."                            |
|   - "Design a real-time notifications system."                 |
|   - "How would you scale this to 1M concurrent users?"         |
|   - Goal: confirm you can reason about tradeoffs at scale.     |
|                                                                |
+---------------------------------------------------------------+

The candidates who do well treat each stage as its own conversation. In the screen, be concise. In the deep dive, be specific — name functions, name flags, name failure modes. In system design, lead with constraints and tradeoffs, not with technologies.


Category 1: Fundamentals & Event Loop

Q1. "Explain the Node.js event loop in detail. What are its phases?"

The event loop is libuv's mechanism for letting a single thread handle thousands of concurrent I/O operations without blocking. It runs in six phases that execute in a fixed order on every tick: timers (expired setTimeout/setInterval callbacks), pending callbacks (deferred I/O callbacks like TCP errors), idle/prepare (internal), poll (retrieves new I/O events and runs their callbacks — this is where most of your code runs), check (setImmediate callbacks), and close callbacks (socket.on('close')). Between every callback in every phase, Node drains the microtask queue (resolved promises, queueMicrotask) and the process.nextTick queue, with nextTick having higher priority than promises. Understanding this order explains why a setImmediate inside an I/O callback always runs before a setTimeout(fn, 0) from the same callback, and why a runaway process.nextTick chain can starve the event loop entirely.

Q2. "What is the difference between process.nextTick, setImmediate, and setTimeout(fn, 0)?"

process.nextTick schedules a callback to run after the current operation completes but before the event loop continues to the next phase — it is not technically part of the event loop at all, it is a microtask-like queue drained between every operation. setImmediate schedules a callback to run in the check phase of the next event loop iteration. setTimeout(fn, 0) schedules a callback in the timers phase with a minimum delay of 1ms (Node clamps it). The practical ordering depends on context: at the top level of a script their order is non-deterministic because the timer may not have expired, but inside an I/O callback setImmediate always fires before setTimeout(fn, 0). Use nextTick for cleanup that must happen before any I/O, setImmediate for "yield to I/O then continue," and avoid setTimeout(fn, 0) as a scheduling primitive — it is almost always the wrong tool.

Q3. "Is Node.js single-threaded? Defend your answer."

Yes and no. Your JavaScript runs on a single thread — there is exactly one V8 isolate executing your code, and at any given moment one and only one line of JS is running. But the runtime around it is multi-threaded: libuv maintains a thread pool (default size 4, configurable via UV_THREADPOOL_SIZE) that handles file system operations, DNS lookups, and CPU-bound crypto, and the kernel handles network I/O via epoll/kqueue/IOCP without using the pool at all. So when you call fs.readFile, the JS thread hands the work to a libuv worker thread, returns immediately, and the worker calls back into the event loop when done. Worker threads (worker_threads module) add a third dimension — actual JS threads with their own V8 isolate — for CPU-bound work. The honest answer is: "single-threaded JavaScript execution on top of a multi-threaded I/O substrate."

Q4. "What is the libuv thread pool used for, and why does it matter?"

Libuv maintains a fixed-size thread pool (default 4 threads) for operations that cannot be done asynchronously at the OS level. This includes all fs.* operations, dns.lookup (but not dns.resolve*, which uses async DNS directly), and CPU-bound crypto operations like pbkdf2 and randomBytes. Network I/O does not use the thread pool — it goes through the kernel's async event notification mechanism (epoll on Linux, kqueue on macOS, IOCP on Windows). This matters because if you fire 100 simultaneous fs.readFile calls with the default pool size of 4, only 4 run at a time and the other 96 queue up. The fix is to bump UV_THREADPOOL_SIZE (max 1024) or to redesign the workload — for example, replacing fs.readFile with streaming reads that yield faster.

Q5. "What is the difference between blocking and non-blocking I/O in Node.js?"

Blocking I/O halts the thread until the operation completes — fs.readFileSync literally pauses the entire event loop until the file is read. Non-blocking I/O hands the operation to the OS or libuv pool, returns immediately, and resumes via a callback when the data is ready. Because Node has only one JS thread, a single blocking call freezes the entire process — no requests get served, no timers fire, no promises resolve. This is why the cardinal rule of Node is "never block the event loop." The exceptions are startup code (config loading, schema compilation) where blocking is fine because no traffic is flowing yet, and sometimes synchronous reads inside CLI tools where there is no concurrency to lose.

Q6. "What is the microtask queue, and how does it interact with the event loop?"

The microtask queue holds callbacks scheduled by resolved promises (then, catch, finally, await) and queueMicrotask. It is not part of the event loop's six phases — instead, the microtask queue is drained between every individual callback in every phase, and it runs to completion before the event loop moves on. This means a chain of .then().then().then() all runs in a single "burst" without yielding to I/O. Combined with process.nextTick (which has even higher priority than microtasks), this creates a starvation risk: a recursive nextTick or an infinite promise chain can prevent the event loop from ever reaching the poll phase, freezing the server while CPU sits at 100%. The mental model is: "tasks (event loop phases) -> microtasks (promises) -> nextTick — and nextTick wins."


Category 2: Streams, Buffers & I/O

Q7. "What are the four types of streams in Node.js, and when would you use each?"

Node has four stream types: Readable (data flows out — fs.createReadStream, HTTP request body), Writable (data flows in — fs.createWriteStream, HTTP response), Duplex (both directions, independent — TCP socket), and Transform (Duplex where output is computed from input — gzip, encryption, CSV parser). You use Readable when you have a large source you want to process incrementally without loading it into memory, Writable when you have a sink that accepts chunks, Duplex for bidirectional protocols, and Transform when you need to modify data as it flows. The killer feature is pipe() (or modern pipeline()), which composes streams into a chain and propagates errors and backpressure automatically.

Q8. "What is backpressure, and how do streams handle it?"

Backpressure is what happens when a fast producer overwhelms a slow consumer — for example, reading from a fast SSD and writing to a slow network socket. Without flow control, the producer fills the consumer's internal buffer until the process runs out of memory. Streams handle backpressure via the highWaterMark option (default 16KB for byte streams, 16 objects for object mode) and the boolean return value of writable.write(chunk): if write returns false, the buffer is full and you should pause the producer until the writable emits a drain event. pipe() and pipeline() do this automatically — they pause the readable when the writable is full and resume on drain. Manual stream code that ignores the return value of write() is one of the top causes of Node memory leaks in production.

Q9. "What is the difference between a Buffer and a string in Node.js?"

A Buffer is a fixed-size chunk of raw binary memory allocated outside the V8 heap, used for handling binary data — file contents, network packets, image bytes, encrypted blobs. A string is a sequence of UTF-16 code units managed by V8. The two interconvert via encodings: buf.toString('utf-8') decodes bytes into a JS string, and Buffer.from('hello', 'utf-8') encodes a string into bytes. The reason Buffers exist is that JavaScript predates binary data — when Node was created, V8 strings were not suitable for byte manipulation, so Node added Buffer (now backed by Uint8Array). For interview purposes, the key insight is that Buffer memory is not subject to V8's heap limits, so you can hold gigabytes of binary data without running into the default 1.5GB heap cap.

Q10. "Explain how pipeline() is better than pipe() for chaining streams."

pipe() is the original API and has two well-known footguns: it does not propagate errors (an error on the readable does not destroy the writable, leaking file descriptors), and there is no completion callback. pipeline() (added in Node 10) fixes both: it forwards errors across the chain, automatically destroys all streams on failure, and invokes a completion callback (or returns a Promise via pipeline from stream/promises) so you know when the entire chain finishes. The modern pattern is await pipeline(source, transform, destination) — one line, error-safe, leak-free. Using pipe() in 2026 is a code smell unless you have a specific reason.

Q11. "How would you read a 10GB CSV file and insert each row into a database?"

You absolutely do not load it into memory. You create a readable file stream, pipe it through a CSV parser transform stream (csv-parse), and pipe that into a writable stream that batches rows and inserts them into the database. Backpressure ensures the file is read no faster than the database can absorb. You wrap the whole thing in pipeline() so any error tears down all streams and closes the file descriptor. For maximum throughput, you batch rows into groups of 500-1000 before each INSERT, use a connection pool with multiple writers, and disable autocommit for the duration. Memory usage stays under 50MB regardless of file size — that is the entire point of streams.


Category 3: Error Handling & Async Patterns

Q12. "What is the difference between an operational error and a programmer error?"

Operational errors are runtime failures the application can reasonably anticipate and recover from — network timeouts, database connection drops, invalid user input, file not found. Programmer errors are bugs — calling a function with the wrong arguments, dereferencing undefined, infinite recursion. The handling is fundamentally different: operational errors should be caught, logged, and returned to the caller (e.g., a 500 response or a retry); programmer errors should crash the process so a supervisor can restart it with a clean state. The reason is that a programmer error means your process is in an unknown, possibly corrupted state — continuing to serve traffic is worse than dying. This is the philosophy behind uncaughtException: log and exit, do not pretend nothing happened.

Q13. "How do you handle errors in async/await code, and what happens to unhandled promise rejections?"

You wrap await calls in try/catch, or you let the rejection propagate up to a higher-level handler. Unhandled rejections — promises that reject without a .catch() or try/catch — emit a process.on('unhandledRejection') event. Since Node 15, the default behavior is to crash the process (previously it just warned). The right pattern is: catch errors at the boundary where you can do something meaningful (e.g., the route handler), log them with structured context, and let everything else bubble up. Express 5 finally awaits async route handlers and forwards rejections to error middleware, but in Express 4 you must either wrap every handler in a try/catch or use a wrapper like express-async-errors. Never swallow errors with empty catch blocks — that is how production bugs go undiagnosed for months.

Q14. "What is callback hell, and how did Promises and async/await solve it?"

Callback hell is the deeply nested, rightward-drifting code that results from chaining multiple async operations using callbacks — each new step adds another level of indentation, error handling has to be repeated at every level, and control flow becomes impossible to follow. Promises flattened this by letting you chain .then() calls linearly and centralize errors in a single .catch() at the end. Async/await went further by letting you write asynchronous code that looks synchronous: const user = await getUser(id) reads top-to-bottom, errors propagate via normal try/catch, and loops/conditionals work the way you expect. The catch is that async/await hides the asynchrony — beginners forget that await inside a forEach does not actually wait, and that sequential awaits give up parallelism. The fix is Promise.all for parallel work and for...of for sequential.

Q15. "What does Promise.all do, and what are its failure modes?"

Promise.all([p1, p2, p3]) runs all promises in parallel and resolves with an array of their results once all have resolved. If any promise rejects, the entire Promise.all rejects immediately with that error — but the other promises keep running in the background, they are just unobserved. This is the killer footgun: a rejection in Promise.all does not cancel the other operations. If you need "wait for all to settle regardless of outcome," use Promise.allSettled, which returns an array of {status, value} or {status, reason} objects. For "first one wins, cancel the rest," use Promise.race — but again, "the rest" keep running. True cancellation in JS requires AbortController, which most modern APIs (fetch, timers, fs/promises) now support.

Q16. "Explain try/catch with async functions. Why doesn't this work as expected?"

// This DOES NOT catch the error
try {
  setTimeout(() => {
    throw new Error('boom');
  }, 100);
} catch (e) {
  console.log('caught:', e); // never runs
}

The try/catch only catches errors thrown synchronously while the try block is executing. By the time the setTimeout callback runs 100ms later, the try block has already completed and the catch is gone. The throw happens in a fresh stack with no try/catch above it, so it becomes an uncaughtException. The fix is to use Promises: wrap the timer in a promise, await it, and the rejection propagates correctly into a surrounding try/catch. This is the single most common async error-handling mistake in interview live-coding sessions.


Category 4: Express & Middleware

Q17. "What is middleware in Express, and how does the chain actually work?"

Middleware is a function with the signature (req, res, next) (or (err, req, res, next) for error middleware) that Express calls in registration order for each incoming request. Each middleware can read or mutate req and res, then either end the response, call next() to pass control to the next middleware, or call next(err) to skip ahead to error middleware. Internally Express maintains an array of middleware layers, and routing is just matching the request URL/method against this array and walking through it. Understanding the model is critical because subtle bugs come from middleware order: putting body parsing after your routes means req.body is undefined, putting authentication after the route means unauthenticated users hit your handler.

Q18. "How does error-handling middleware differ from regular middleware?"

Error middleware has four parameters instead of three: (err, req, res, next). Express detects the four-parameter signature and only calls it when an error has been forwarded via next(err) or thrown synchronously inside a handler. It must be registered after all your routes — Express walks middleware in order, and an error middleware before your routes will simply be skipped. The standard pattern is one global error middleware at the bottom of app.js that logs the error, classifies it (operational vs programmer), and sends an appropriate response. In Express 4, async errors do not automatically reach this middleware unless you forward them manually or use express-async-errors. Express 5 fixed this — async route handlers that reject are forwarded to error middleware automatically.

Q19. "What is the difference between app.use and app.get/post/etc?"

app.use(path, fn) mounts middleware that runs for every HTTP method matching the path prefix — so app.use('/api', auth) runs auth for any request to /api/anything. app.get, app.post, etc. register route handlers that match a specific method and an exact path pattern. Middleware registered with app.use is meant to be cross-cutting (logging, auth, body parsing); route handlers are meant to be terminal (they end the response). The other distinction is that app.use matches path prefixes by default while route handlers match the full path — app.use('/users') matches /users/123, but app.get('/users') does not.

Q20. "How would you implement request validation in Express?"

The clean pattern is a validation middleware that takes a schema (Zod, Joi, Yup) and returns a middleware function: validate(UserSchema) parses req.body, attaches the typed result to req.validated, and calls next() on success or next(err) on failure. The error middleware then catches validation errors and returns a 400 with the issue list. The wrong pattern is to validate inside the route handler — it scatters validation logic, makes routes long, and couples business logic to schema details. Centralizing validation as middleware also lets you generate OpenAPI docs from the same schemas, giving you a single source of truth for the API contract.


Category 5: Performance & Scaling

Q21. "What is the difference between cluster and worker_threads?"

cluster forks multiple Node processes that share a server port via the master process — each worker has its own V8 isolate, its own event loop, its own memory, and they communicate via IPC. It is for scaling I/O-bound workloads across CPU cores: with 8 cores you fork 8 workers and triple your request throughput. worker_threads creates threads inside a single process, each with its own V8 isolate but sharing memory via SharedArrayBuffer and communicating via MessagePort. It is for offloading CPU-bound work (image processing, parsing, hashing) without blocking the main event loop. Rule of thumb: cluster for I/O-bound web servers, worker threads for CPU-bound tasks within a single server. They compose — you can run cluster workers each with their own internal worker thread pool.

Q22. "How does the cluster module distribute incoming connections across workers?"

On Linux, cluster uses a round-robin policy by default: the master process accepts connections and hands them to workers in rotation. On Windows, the default is the operating system's choice, which historically led to load imbalance because the OS scheduler favored a small number of workers. You can force round-robin on any platform with cluster.schedulingPolicy = cluster.SCHED_RR. The alternative ("none") lets workers accept() directly on the shared socket, which is faster but can imbalance because the kernel may wake the same worker repeatedly. Modern setups often skip cluster entirely and run multiple Node processes behind a real load balancer (Nginx, HAProxy, or a Kubernetes service) — it is more flexible and decouples scaling from a single host.

Q23. "How would you find a CPU bottleneck in a Node.js application?"

You profile it. The simplest approach is node --prof app.js, which writes a V8 tick log; you process it with node --prof-process and read the flame data to see which functions dominated CPU time. For interactive analysis, use --inspect and open Chrome DevTools, which gives you a flame graph and call tree. In production, tools like clinic.js, 0x, or APM agents (Datadog, New Relic, Dynatrace) capture continuous profiles with low overhead. The signs of a CPU bottleneck are: event loop lag rising under load, high CPU usage on a single core, p99 latency much worse than p50 even with low traffic, and worker processes saturating before the database does. The fix is usually to move the hot work off the main thread — into a worker thread, a separate service, or a precomputed cache.

Q24. "What causes event loop lag, and how do you measure it?"

Event loop lag is the time the event loop spends between iterations — ideally zero, in practice a few milliseconds. It rises when something blocks: synchronous file I/O, JSON parsing of huge payloads, long regex (catastrophic backtracking), bcrypt with high cost factor, or a tight CPU loop. You measure it by scheduling a setImmediate and timing how long it takes to fire, or by using perf_hooks.monitorEventLoopDelay, which gives you a histogram of lag over time. Production targets are p99 lag under 20ms for latency-sensitive APIs; sustained lag over 100ms means users are seeing slow responses. The fix is to identify the blocking call and either move it off the main thread, batch/throttle it, or replace it with a non-blocking equivalent.

Q25. "How would you handle 1M concurrent WebSocket connections?"

You do not do it in one Node process. A single Node instance handles 50-100k concurrent connections comfortably; beyond that, you scale horizontally. The architecture is: a layer of WebSocket gateway servers (Node + ws or uWebSockets.js) behind a load balancer that supports sticky sessions (because WebSocket is connection-oriented), a shared pub/sub backbone (Redis pub/sub, NATS, Kafka) so messages broadcast across all gateways, and a session store for routing user-targeted messages. You tune the OS for high file descriptor limits (ulimit -n 1000000), enable TCP keepalive, and watch out for the libuv thread pool — DNS lookups during connection setup can stall under heavy churn. Memory becomes the constraint at scale: a Node WebSocket connection costs roughly 30-50KB, so 100k connections eat 3-5GB before you even count application state.


Category 6: "What Happens When..." Scenarios

Q26. "What happens when an uncaughtException is thrown in Node.js?"

By default, Node prints the stack trace to stderr and exits with code 1. If you have registered a process.on('uncaughtException', handler), that handler runs first and the process does not exit automatically — but you should still call process.exit(1) from inside the handler. The reason is that an uncaught exception means an unknown amount of state is now corrupted: open file descriptors, half-written database rows, in-flight HTTP responses. Continuing to serve traffic from a damaged process produces inconsistent data and confusing bugs. The correct pattern is: log the error with full context, attempt graceful shutdown of in-flight requests with a short timeout, and exit. A supervisor (systemd, Kubernetes, PM2) restarts you with a clean slate.

Q27. "What happens when you call process.exit(0) while requests are still being handled?"

process.exit is synchronous and immediate — it does not wait for pending I/O, does not drain queues, does not flush logs, does not close sockets gracefully. Any in-flight HTTP responses are cut off mid-stream, any open database transactions are abandoned, any unflushed writes to disk are lost. This is almost always wrong for a production server. The right pattern for graceful shutdown is: stop accepting new connections (server.close()), wait for in-flight requests to finish with a timeout (e.g., 30 seconds), close database pools, flush logs, and then call process.exit(0). Listening for SIGTERM (sent by Docker/Kubernetes during rollout) and running this sequence is the difference between zero-downtime deploys and angry users.

Q28. "What happens when the event loop is blocked for 5 seconds?"

Everything stops. No HTTP requests are accepted (the OS buffers them up to the listen backlog, then refuses), no timers fire (a 100ms setTimeout set 4 seconds ago will fire 5 seconds late), no I/O callbacks run, no promises resolve. WebSocket clients see no pings and may disconnect. Health checks fail. Other services calling you time out and may mark you as down. If the block lasts long enough, your load balancer drops you out of the rotation. The cause is almost always synchronous CPU work: a giant JSON parse, a tight loop, a regex with catastrophic backtracking, or accidentally calling fs.readFileSync on a multi-MB file. The fix is to identify it via event loop lag monitoring and move it off the main thread — worker thread, separate service, or streaming.

Q29. "What happens when a Node.js process leaks memory? How do you recognize it?"

Memory leaks in Node usually look like a slow, monotonic increase in RSS over hours or days, eventually triggering the V8 heap limit (default 1.5GB on 64-bit) and crashing with JavaScript heap out of memory. Common causes are: closures capturing large objects unintentionally, arrays/maps that grow without bound (caches without eviction), event emitter listeners added but never removed, timers holding references via closure, and global state that accumulates per-request data. You recognize leaks by monitoring RSS over time — flat RSS under steady load is healthy, rising RSS is suspicious. To diagnose, take heap snapshots at intervals (v8.writeHeapSnapshot()), open them in Chrome DevTools, and compare retainers between snapshots. The objects that grew between snapshots and are still retained are your leak.


Category 7: System Design Prompts

Q30. "Design a distributed rate limiter for an API."

Lead with constraints: how many requests per second, what is the limit (per user? per IP? per API key?), and how strict does it need to be — is approximate fine, or must it be exact across all instances? For an approximate per-user limit at moderate scale, the standard answer is token bucket in Redis: each user has a key holding their current token count and last-refill timestamp; on each request you atomically refill (proportional to elapsed time) and decrement using a Lua script to keep it race-free. For higher accuracy use a sliding window log (a sorted set of request timestamps, expire entries older than the window). For massive scale, push the rate limit to the edge (CloudFront, Nginx, a sidecar proxy) so the request never even reaches your Node process. Discuss tradeoffs: token bucket allows bursts, fixed window has boundary spikes, sliding window is most accurate but most expensive. Mention failure modes: what happens if Redis is down? Fail open (allow traffic, log) or fail closed (block everything)? Most APIs fail open for availability.

+---------------------------------------------------------------+
|           TOKEN BUCKET IN REDIS                                |
+---------------------------------------------------------------+
|                                                                |
|  Key: rate:user:42                                             |
|  Value: { tokens: 7.3, lastRefill: 1712345678 }                |
|                                                                |
|  On request:                                                   |
|   1. EVAL Lua script (atomic):                                 |
|      a. Load tokens, lastRefill                                |
|      b. tokens += elapsed * refillRate (cap at bucketSize)     |
|      c. If tokens >= 1: tokens -= 1, ALLOW                     |
|         Else: DENY with Retry-After header                     |
|      d. Save tokens, lastRefill                                |
|   2. Return decision to caller                                 |
|                                                                |
|  Tradeoffs:                                                    |
|   + Allows controlled bursts (good UX)                         |
|   + O(1) memory per user                                       |
|   - Approximate at very high QPS (clock drift)                 |
|                                                                |
+---------------------------------------------------------------+

Q31. "Design a file upload API that supports files up to 5GB."

Start with what you cannot do: you cannot buffer 5GB in memory, and you cannot let a single HTTP request fail and lose the entire upload. The architecture is direct-to-S3 with presigned URLs: the client requests an upload URL from your API, your API generates a presigned PUT (or multipart) URL pointing directly at S3, and the client uploads to S3 without ever touching your Node servers. For files over a few hundred MB, use S3 multipart upload — your API returns presigned URLs for each part, the client uploads parts in parallel with retry, and finally calls a "complete" endpoint on your API that finalizes the multipart upload via S3 and writes a row to your database. Discuss validation (you cannot trust client-provided MIME types — scan after upload), authorization (presigned URLs include the user's permissions), virus scanning (Lambda triggered by S3 event), and resumability (multipart parts can be retried independently). Mention why streaming through Node is the wrong answer at this size: it ties up a worker for minutes per upload, eats memory, and requires you to handle retries yourself.

Q32. "Design a real-time notification system for 10M users."

Constraints first: what is "real-time" — sub-second or sub-minute? Are notifications targeted to one user, broadcast to many, or both? Is delivery at-least-once acceptable? The reference architecture is: a WebSocket gateway tier (Node + uWebSockets.js, behind sticky load balancer) holds connections and tracks which userId is on which gateway via a Redis hash; a publish API writes notifications to a durable queue (Kafka, NATS JetStream); a dispatcher service reads the queue, looks up the user's gateway in Redis, and forwards the message via internal RPC to that gateway. For users who are offline, notifications go into a per-user inbox (DynamoDB, Cassandra, Redis Streams) that the user fetches on reconnect. Mention fallbacks: if WebSocket fails, fall back to long-polling or server-sent events; if the user is offline for too long, deliver via push notification (APNs/FCM) or email. Discuss scale numbers: 10M users with 10% concurrent = 1M open connections, divided across 20 gateway nodes at 50k connections each. Discuss back-of-envelope memory: 50k * 40KB = 2GB per node, leaving headroom for application state.


Category 8: Debugging Scenarios

Q33. "Walk me through how you would debug a memory leak in a production Node.js service."

First, confirm it is actually a leak and not just slow growth toward a steady state: graph RSS over a long enough window (hours to days) and look for monotonic increase under steady load. If confirmed, capture heap snapshots at regular intervals — v8.writeHeapSnapshot() works in production with a small pause. Move at least three snapshots to a dev machine and open them in Chrome DevTools' Memory tab. Use the Comparison view between snapshots to see which object types grew and are still retained. The retainer chain shows you exactly what is holding the leaked objects alive — usually a closure, a cache without eviction, or a forgotten event listener. Common culprits: an unbounded Map used as a cache, an EventEmitter with setMaxListeners(0) and listeners never removed, timers that close over large objects. Once you identify the retainer, fix the root cause (add eviction, remove the listener, break the closure) and verify the fix by running the same load test and observing flat RSS.

Q34. "How do you profile CPU usage in a running Node.js process?"

For ad-hoc profiling, run with --inspect and connect Chrome DevTools, then use the Profiler tab to record a CPU profile during a load test. The flame graph shows you which functions dominated CPU time — anything wide at the top is a hot path. For production, use --cpu-prof to write a profile to disk that you can analyze later, or an APM agent that captures continuous profiles with low overhead (Datadog continuous profiler, Pyroscope). For deeper analysis, clinic.js flame runs your app under load and produces a clickable flame graph, while clinic.js doctor correlates CPU, memory, and event loop lag to suggest the bottleneck class. The general workflow is: reproduce under load, capture profile, find the hottest function, ask "is this expected?" If it is your business logic, optimize the algorithm. If it is JSON parsing, switch to a faster parser or stream it. If it is regex, rewrite it.

Q35. "An endpoint suddenly has 5-second latency in production. How do you investigate?"

Start with the data you already have: APM traces (which span is slow?), structured logs around that endpoint, error rates, and event loop lag. Walk the request path mentally: Node receives the request, runs middleware, calls the database, calls downstream services, serializes the response. The slow span tells you which stage is the problem. If it is the database, check slow query logs and the query plan — a missing index after a recent migration is the classic cause. If it is a downstream service, check that service's metrics and circuit breaker state. If it is in Node itself, check event loop lag — if lag spiked at the same time, something is blocking. Common causes of sudden latency: a runaway query from a new feature, a downstream dependency degrading, a connection pool exhausted, a bad deploy that introduced a synchronous call, GC pressure from a memory leak. The skill being tested is not memorizing causes — it is having a systematic approach instead of guessing.


ASCII Diagrams Recap

+---------------------------------------------------------------+
|           EVENT LOOP PHASES (one tick)                         |
+---------------------------------------------------------------+
|                                                                |
|   +-> timers          (setTimeout / setInterval)               |
|   |                                                             |
|   |   pending callbacks (deferred I/O errors)                  |
|   |                                                             |
|   |   idle, prepare    (internal)                              |
|   |                                                             |
|   |   poll             (fetch new I/O, run their callbacks)    |
|   |                                                             |
|   |   check            (setImmediate)                          |
|   |                                                             |
|   |   close callbacks  (socket close, etc.)                    |
|   |                                                             |
|   +-- (loop)                                                    |
|                                                                |
|   Between EVERY callback in EVERY phase:                       |
|     1. Drain process.nextTick queue (highest priority)         |
|     2. Drain microtask queue (Promise callbacks)               |
|                                                                |
+---------------------------------------------------------------+

+---------------------------------------------------------------+
|           EXPRESS ERROR FLOW                                   |
+---------------------------------------------------------------+
|                                                                |
|   request -> middleware1 -> middleware2 -> route handler       |
|                                |                                |
|                                | next(err)  or throw            |
|                                v                                |
|                          (skip remaining normal middleware)     |
|                                |                                |
|                                v                                |
|                      error middleware (err, req, res, next)    |
|                                |                                |
|                                v                                |
|                          res.status(500).json({...})           |
|                                                                |
|   Note: in Express 4, async rejections do NOT auto-forward.    |
|   In Express 5 they do. Use express-async-errors for v4.       |
|                                                                |
+---------------------------------------------------------------+

Common Mistakes Candidates Make

1. Reciting definitions instead of explaining behavior. Saying "the event loop is non-blocking and uses libuv" tells the interviewer you read a blog post. Saying "the event loop has six phases, drains microtasks between every callback, and process.nextTick has higher priority than promises which can starve I/O if you recurse" tells them you have actually debugged this. Always go one level deeper than the textbook answer — name the function, the flag, the failure mode.

2. Using buzzwords without tradeoffs. "I would use Redis." Why Redis and not Memcached? "I would use Kafka." Why Kafka and not SQS? Senior interviewers want to see you reason about tradeoffs — latency, durability, ordering, cost, ops complexity. The right format is "I would use X because of Y, at the cost of Z." Anything less reads as cargo-cult engineering.

3. Forgetting backpressure when designing data pipelines. If your design says "read the file, transform each line, write to the database," the next question is "what happens when the database is slower than the file reader?" If you do not have an answer involving streams, pipeline(), and highWaterMark, you have just designed a memory leak. Always mention backpressure when streams are involved.

4. Treating async/await as a synchronous superpower. Candidates routinely write users.forEach(async (u) => await save(u)) and claim it waits. It does not — forEach ignores the returned promise, all saves run in parallel, and the function returns before any of them finish. The correct sequential form is for (const u of users) await save(u), and the parallel form is await Promise.all(users.map(save)). If you do not know the difference cold, you will get caught in live coding.

5. Not asking clarifying questions in system design. Jumping straight into "I would use Node, Redis, and Postgres" before the interviewer has told you the scale, the consistency requirements, or the budget is the fastest way to fail a system design round. The correct opening is always: "Before I sketch, can I confirm — how many users, what is the read/write ratio, what consistency model do we need, and what is the latency SLA?" Two minutes of clarification beats twenty minutes of designing the wrong system.


Quick Reference — The 30-Question Cheat Sheet

+---------------------------------------------------------------+
|           THE NODE.JS INTERVIEW CATALOG                       |
+---------------------------------------------------------------+
|                                                                |
|  Memorize the categories. Practice the answers out loud.       |
|  In real interviews, you will not get all 30 — but you will    |
|  get 6-10 from this list, every time.                          |
|                                                                |
+---------------------------------------------------------------+
#CategoryQuestion
1FundamentalsExplain the event loop and its phases
2FundamentalsnextTick vs setImmediate vs setTimeout(fn, 0)
3FundamentalsIs Node single-threaded? Defend it.
4FundamentalsWhat is the libuv thread pool used for?
5FundamentalsBlocking vs non-blocking I/O
6FundamentalsWhat is the microtask queue?
7StreamsFour stream types and when to use each
8StreamsWhat is backpressure?
9StreamsBuffer vs string
10Streamspipeline() vs pipe()
11StreamsRead a 10GB CSV into a database
12ErrorsOperational vs programmer errors
13Errorsasync/await error handling, unhandledRejection
14ErrorsCallback hell -> Promises -> async/await
15ErrorsPromise.all failure modes
16ErrorsWhy try/catch does not catch async throws
17ExpressWhat is middleware? How does the chain work?
18ExpressError-handling middleware signature and order
19Expressapp.use vs app.get/post
20ExpressHow to implement request validation
21Performancecluster vs worker_threads
22PerformanceHow does cluster distribute connections?
23PerformanceHow to find a CPU bottleneck
24PerformanceWhat causes event loop lag?
25PerformanceHow to handle 1M concurrent WebSockets
26What Happens WhenuncaughtException is thrown
27What Happens Whenprocess.exit(0) mid-request
28What Happens WhenEvent loop blocked for 5 seconds
29What Happens WhenA process leaks memory
30System DesignDesign a distributed rate limiter
31System DesignDesign a 5GB file upload API
32System DesignDesign a real-time notification system for 10M users
33DebuggingDebug a production memory leak
34DebuggingProfile CPU usage of a running process
35DebuggingSudden 5-second latency on an endpoint
+---------------------------------------------------------------+
|           HOW TO PREPARE                                       |
+---------------------------------------------------------------+
|                                                                |
|  1. Read each question. Answer OUT LOUD before reading mine.   |
|  2. Compare your answer to the lesson. Note what you missed.   |
|  3. Re-answer the same question 3 days later from memory.      |
|  4. For system design, sketch on paper with constraints first. |
|  5. Practice with a friend — interviews test communication     |
|     as much as knowledge. Saying it correctly matters.         |
|  6. Build something real. Every answer above came from pain.   |
|                                                                |
+---------------------------------------------------------------+

Prev: Lesson 10.4 -- Dockerizing Node.js Course Complete. Back to Roadmap


This is Lesson 10.5 of the Node.js Interview Prep Course -- 10 chapters, 42 lessons. Course complete.

On this page