Skip to main content

Node.js Interview Questions (55+ Detailed Q&A)

1. Runtime & Core Architecture

Answer: No, Node.js is a Runtime Environment for executing JavaScript outside the browser — it is not a framework, not a language, and not a web server (though it can create one).Architecture stack:
  • Chrome V8 Engine: Compiles JS directly to machine code (no interpreter step). Uses JIT (Just-In-Time) compilation with TurboFan optimizing compiler and Sparkplug baseline compiler.
  • Libuv: A C library that provides the event loop, async I/O (file system, DNS, network), a thread pool (default 4 threads), and cross-platform abstractions over OS-level async primitives (epoll on Linux, kqueue on macOS, IOCP on Windows).
  • C++ bindings: Bridge between JS and native OS APIs (crypto, zlib, http_parser).
Key mental model: Node is a single-threaded orchestrator. Your JS runs on one thread, but the actual I/O work is delegated to the OS kernel or Libuv’s thread pool. This is what makes it non-blocking.What interviewers are really testing: Do you understand that Node is not “just JavaScript”? Can you articulate the relationship between V8, Libuv, and the event loop? Candidates who say “Node.js is a JavaScript framework” are an instant red flag.Red flag answer: “Node.js is a server-side framework for building web apps.” This conflates the runtime with Express/Koa frameworks and shows surface-level understanding.Follow-up:
  • “If V8 compiles JS to machine code, where does the bytecode step fit in? What is Ignition vs TurboFan?”
  • “Can you explain what Libuv’s thread pool is used for vs what goes directly to the OS kernel?”
  • “Why would Node choose to be single-threaded instead of multi-threaded like Java’s Tomcat?”
Answer: Node uses a Single Main Thread for JS execution (the Event Loop), but this is only half the story.How it actually scales:
  1. Event Loop (single thread): Orchestrates callbacks, runs your JS code, and dispatches I/O requests.
  2. OS Kernel Async Primitives: Network I/O (TCP sockets, HTTP) goes directly to the kernel via epoll/kqueue/IOCP — zero threads needed. This is how Node handles 10k+ concurrent connections on a single process.
  3. Libuv Thread Pool (default 4 threads, max 1024 via UV_THREADPOOL_SIZE): Used for operations the OS cannot do asynchronously — file system operations, DNS lookups (dns.lookup, not dns.resolve), and some crypto operations.
The C10K advantage: Traditional thread-per-request models (Apache) need ~2MB per thread. 10k connections = 20GB RAM just for threads. Node handles 10k connections with a single thread + kernel event notifications, using ~50-100MB total.Real-world example: A chat server handling 50k concurrent WebSocket connections. Each connection is just a file descriptor in the kernel’s epoll set — no thread allocation per connection.What interviewers are really testing: Can you distinguish between “single-threaded JS” and “Node uses threads behind the scenes”? Strong candidates know about the Libuv thread pool, UV_THREADPOOL_SIZE, and which operations use threads vs kernel async.Red flag answer: “Node is single-threaded so it can only do one thing at a time.” This misunderstands the delegation model entirely.Follow-up:
  • “If the thread pool default is 4, what happens when you have 100 concurrent fs.readFile calls? Does the 101st block?”
  • “When would you increase UV_THREADPOOL_SIZE and what’s the cost of making it too large?”
  • “How does the Cluster module change this picture? What about Worker Threads?”
Answer: The event loop is not a single queue — it is a series of phases, each with its own FIFO queue. Understanding this is critical for debugging timing issues.The 6 Phases (in order):
  1. Timers: Execute callbacks from setTimeout and setInterval whose threshold has elapsed. Note: timers are not guaranteed to fire at the exact time — they fire as soon as possible after the threshold, in this phase.
  2. Pending Callbacks: Execute I/O callbacks deferred from the previous loop iteration (e.g., TCP errors like ECONNREFUSED).
  3. Idle/Prepare: Internal use only by Libuv. You cannot interact with this phase.
  4. Poll: This is the heart of the event loop. It retrieves new I/O events from the kernel, executes their callbacks (file read complete, socket data received, etc.). If the poll queue is empty, it will block here waiting for new events (up to a calculated timeout based on pending timers).
  5. Check: setImmediate callbacks execute here. This is specifically designed to run callbacks after the poll phase completes.
  6. Close Callbacks: socket.on('close'), cleanup handlers.
Microtask Queues (run between every phase transition):
  • process.nextTick queue (higher priority — runs first)
  • Promise microtask queue (.then, .catch, .finally)
Execution Order Example:
Why Order Varies: setTimeout(fn, 0) actually means setTimeout(fn, 1) internally (minimum 1ms). If the event loop enters the timers phase in under 1ms, the timer hasn’t elapsed yet, so setImmediate (check phase) fires first. If it takes longer than 1ms to enter timers, setTimeout fires first. This is non-deterministic in the main module but deterministic inside an I/O callback (setImmediate always fires first there, because after I/O you’re in the poll phase, and check phase comes next).
What interviewers are really testing: Can you explain the phases without just listing names? Do you know why the poll phase blocks, what microtasks are, and the setTimeout(0) vs setImmediate indeterminacy? This question separates candidates who have read the docs from those who have debugged timing bugs.Red flag answer: “The event loop just processes callbacks in order” or listing phases without understanding what the poll phase actually does.Follow-up:
  • “What happens if you schedule a process.nextTick inside a process.nextTick recursively? How does that differ from recursive setImmediate?”
  • “If I have a setTimeout(fn, 5) and the poll phase is blocked waiting for I/O for 10ms, does the timer fire late?”
  • “In Node 11+, what changed about microtask execution order between phases vs inside a single phase?”
Answer: These are two fundamentally different scheduling mechanisms with different positions in the event loop.process.nextTick:
  • Runs immediately after the current operation completes, before the event loop continues to the next phase.
  • Part of the microtask queue (along with Promise callbacks), but nextTick has higher priority than Promises.
  • Starvation risk: Recursive nextTick will starve I/O because the event loop never advances past the microtask checkpoint.
  • Use case: Emitting events after a constructor returns (so the caller can attach listeners), ensuring a callback fires before any I/O.
setImmediate:
  • Runs in the Check phase of the event loop (after the poll phase).
  • Cannot starve I/O: Even recursive setImmediate allows the event loop to process I/O, timers, etc. between iterations.
  • Use case: Breaking up CPU work so I/O can still be processed; preferred for recursive scheduling.
Starvation Example:
Real-world war story: A production service had a middleware that called process.nextTick in a tight loop to “batch” database operations. Under load, this starved the HTTP server’s ability to accept new connections. The process appeared alive (health check port was open) but returned zero responses. Switching to setImmediate fixed the issue instantly.The naming is backwards: process.nextTick fires before the next tick of the event loop, and setImmediate fires on the next tick (check phase). The Node.js team has acknowledged this is confusing but cannot change it due to backward compatibility.What interviewers are really testing: Do you understand microtask starvation? Can you articulate when each is appropriate? Have you ever been bitten by nextTick in production?Red flag answer: “They’re basically the same thing” or “I always use setTimeout(fn, 0) instead.”Follow-up:
  • “If you needed to guarantee a callback fires before any pending I/O but after the current synchronous code, which would you use and why?”
  • “How does queueMicrotask() relate to process.nextTick? Which runs first?”
  • “In a real application, give me a concrete example where using nextTick instead of setImmediate caused a bug.”
Answer: V8 divides its heap into New Space (short-lived objects, ~1-8MB) and Old Space (long-lived objects). The total heap limit defaults to approximately 1.5GB on 64-bit systems and ~700MB on 32-bit systems.Adjusting the limit:
Why the limit exists: V8’s garbage collector must pause JS execution during major GC (mark-sweep-compact). Larger heaps mean longer GC pauses. At 4GB, you might see 500ms+ GC pauses, which is catastrophic for an HTTP server handling real-time requests.Real-world considerations:
  • Container environments: If your container has 2GB RAM and you set --max-old-space-size=4096, Node will be OOM-killed by the kernel. Always set the flag to ~75% of your container’s memory limit (leave room for native memory, Buffers, thread stacks).
  • Buffers and native memory: Buffer allocations live outside the V8 heap. A process can use 500MB of Buffers while reporting only 100MB heap usage. process.memoryUsage() shows both heapUsed and external (C++ objects including Buffers).
What interviewers are really testing: Do you know about memory regions beyond the heap? Can you reason about memory in containerized deployments?Red flag answer: “Just increase the memory flag if you run out” without considering GC pause impact or container limits.Follow-up:
  • “What’s the difference between heapUsed, heapTotal, rss, and external in process.memoryUsage()?”
  • “How would you size the --max-old-space-size flag for a Node process running in a Kubernetes pod with a 1GB memory limit?”
  • “What happens to GC pause times as you increase heap size? How does this affect P99 latency?”
Answer: V8 uses a Generational Garbage Collector based on the observation that most objects die young (the “generational hypothesis”).Heap Regions:
  • New Space (Young Generation): Split into two semi-spaces (~1-8MB each). New objects are allocated here.
  • Old Space (Old Generation): Objects that survive two GC cycles in New Space get promoted here.
  • Large Object Space: Objects too large for New Space go directly here.
  • Code Space: JIT-compiled code.
  • Map Space: Hidden class (Shape/Map) metadata.
GC Algorithms:
  1. Scavenge (Minor GC): Runs on New Space. Uses Cheney’s algorithm — copies live objects from one semi-space to the other, then clears the old semi-space. Very fast (~1-5ms) because New Space is small. Objects surviving 2 scavenges get promoted to Old Space.
  2. Mark-Sweep-Compact (Major GC): Runs on Old Space. Three stages — Mark (walk the object graph from roots, mark reachable objects), Sweep (free unmarked objects), Compact (defragment memory). This is the expensive one (50-500ms+) and causes “stop-the-world” pauses.
  3. Incremental Marking: V8 breaks the marking phase into small chunks interleaved with JS execution (increments of ~5ms) to reduce max pause time.
  4. Concurrent Sweeping/Compaction: V8 runs sweeping and some compaction on background threads while JS continues executing.
Orinoco (V8’s modern GC): Combines concurrent marking, parallel scavenging, and concurrent sweeping to minimize main-thread pauses. In practice, most GC pauses in modern Node (v18+) are under 10ms.Monitoring GC in production:
What interviewers are really testing: Can you explain why generational GC works (most objects die young)? Do you know the difference between minor and major GC? Can you reason about GC impact on latency?Red flag answer: “V8 handles garbage collection automatically, you don’t need to worry about it.” In production at scale, GC is one of the top causes of latency spikes.Follow-up:
  • “You see a Node process with 2GB heap and periodic 200ms latency spikes. How do you determine if GC is the cause?”
  • “What is an ‘old space expansion’ and how does it differ from a normal major GC?”
  • “How do closures and event listeners contribute to objects being unintentionally promoted to old space?”
Answer: Node.js provides several global objects and module-scoped variables (which look global but are not truly on the global object):Truly global (on the global object):
  • global itself (equivalent to window in browsers, or globalThis in ES2020+)
  • process — the current Node process (PID, env vars, stdin/stdout, exit codes, memory usage)
  • Buffer — binary data handling
  • console — logging
  • setTimeout, setInterval, setImmediate, queueMicrotask
Module-scoped (injected by the CommonJS module wrapper, NOT on global):
  • __dirname — absolute path of the directory containing the current file
  • __filename — absolute path of the current file
  • require — the module loader function
  • module — reference to the current module
  • exports — shorthand for module.exports
The module wrapper: Every CommonJS file is wrapped in:
This is why __dirname is not available in ESM modules — use import.meta.url with fileURLToPath instead.ESM equivalents:
What interviewers are really testing: Do you know the difference between truly global objects and module-scoped variables? Can you explain the CJS module wrapper?Red flag answer: Listing __dirname as a global object without knowing it’s module-scoped, or not knowing the ESM equivalents.Follow-up:
  • “Why is __dirname not available in ES modules? How do you get the equivalent?”
  • “What is globalThis and why was it introduced?”
  • “If you set global.myVar = 42 in one module, can another module access it? What are the dangers of this?”
Answer: These are two fundamentally different module systems with different loading semantics.CommonJS (CJS):
  • require() / module.exports
  • Synchronous loading — require blocks until the module is loaded and evaluated.
  • Dynamic — you can require() inside an if block or compute the module path at runtime.
  • Loads a copy (cached after first load in require.cache).
  • Circular dependency handling: Returns a partially filled module.exports (whatever was exported so far at the point of circular reference). This can cause subtle bugs where you get undefined for properties that haven’t been assigned yet.
  • Default in Node (.js files without "type": "module" in package.json).
ES Modules (ESM):
  • import / export
  • Asynchronous loading — parsed statically, loaded in parallel.
  • Static — imports must be at the top level (enables tree-shaking and static analysis).
  • Live bindings — importing a value gives you a live reference to the exporting module’s binding. If the export changes, the import sees the new value.
  • Circular dependency handling: Due to live bindings, circular deps work more predictably (but you can still access uninitialized bindings if the exporting module hasn’t run that code yet).
  • Enabled via "type": "module" in package.json or .mjs extension.
Interop gotchas:
  • CJS can require() CJS. ESM can import ESM.
  • ESM can import CJS (the module.exports object becomes the default export).
  • CJS cannot require() ESM directly (must use dynamic import() which returns a Promise).
  • __dirname, require.cache, module are not available in ESM.
What interviewers are really testing: Do you understand the semantic differences (sync vs async, copy vs live binding), not just the syntax? Can you navigate the interop challenges?Red flag answer: “CJS uses require, ESM uses import, they’re basically the same.”Follow-up:
  • “What is a ‘live binding’ in ESM and how does it differ from CJS’s value copy?”
  • “You have a library that only ships CJS. How do you use it in an ESM project? What about the reverse?”
  • “What are the implications of CJS’s synchronous loading for server startup time with hundreds of modules?”
Answer: This trips up many developers. The key insight: exports is just a convenience reference that initially points to the same object as module.exports.
What works:
What breaks:
Why it breaks: require() always returns module.exports, never exports. When you reassign exports, you break the reference but module.exports is unchanged.Rule: If you are assigning an entirely new object, class, or function as the module’s export, always use module.exports:
What interviewers are really testing: Do you understand JavaScript reference semantics? This is really a question about how object references work.Red flag answer: “I just always use module.exports and never use exports” — this works in practice but shows you don’t understand why.Follow-up:
  • “Can you draw a diagram showing what happens to the reference when you do exports = { a: 1 } vs exports.a = 1?”
  • “In ESM, does this same confusion exist? Why or why not?”
Answer: Node’s Achilles’ heel: CPU-bound work blocks the event loop, making the entire server unresponsive. A single 100ms CPU computation means every other request waits 100ms.Solutions (ordered by preference for most cases):
  1. Worker Threads (worker_threads module):
    • True parallel JS execution with shared memory (SharedArrayBuffer) or message passing.
    • Same process, lighter than child processes. Shares the V8 isolate’s native addons.
    • Best for: image processing, data parsing, cryptographic operations.
    • Gotcha: Each worker has its own V8 instance and event loop, so memory overhead is ~5-10MB per worker.
  2. Child Process (child_process.fork):
    • Spawns an entirely new Node.js process with its own V8 instance and memory space.
    • Communication via IPC (inter-process communication) channel.
    • Best for: isolating untrusted code, workloads that might crash (OOM), or when you need full process isolation.
    • Gotcha: Higher overhead (~30MB per process), slower IPC than shared memory.
  3. Offload to specialized service:
    • Send CPU work to a Go/Rust microservice or a job queue (Bull/BullMQ with Redis, RabbitMQ).
    • Best for: video transcoding, ML inference, PDF generation. Let Node do what it’s good at (I/O orchestration).
  4. Native Addons (N-API/NAPI):
    • Write the CPU-intensive function in C/C++ or Rust (via neon) and call it from Node.
    • Best for: tight loops, numerical computation, when you need both speed and integration.
How to detect Event Loop blockage:
What interviewers are really testing: Do you know why CPU tasks are problematic in Node, and can you pick the right solution based on the constraints? A staff-level answer includes trade-offs between the approaches.Red flag answer: “Just use async/await” — async/await does NOT help with CPU-bound work. await cpuIntensiveFunction() still blocks the event loop if the function is synchronous.Follow-up:
  • “What’s the difference between worker_threads and child_process.fork in terms of memory isolation, startup cost, and communication overhead?”
  • “If a CPU task takes 50ms, is that enough to worry about? How do you decide the threshold?”
  • “How would you implement a worker thread pool to process image thumbnails? What happens if all workers are busy?“

2. Streams, Buffers, I/O

Answer: A Buffer is a fixed-size chunk of memory allocated outside the V8 heap (on the C++ side via Libuv). It represents raw binary data — think of it as a byte array like Uint8Array (in fact, Buffer extends Uint8Array since Node 4).Why Buffers exist: JavaScript strings are UTF-16 encoded and immutable. When dealing with file I/O, network packets, or binary protocols (images, video, protobuf), you need raw byte manipulation — that’s what Buffers provide.Key operations:
Buffer.alloc vs Buffer.allocUnsafe: allocUnsafe skips zero-filling the memory — it’s faster but may contain leftover data from previous allocations (a security risk if the Buffer is sent to a client without being fully written). Use alloc unless you are immediately overwriting every byte and need the performance.Memory implications: Buffers are tracked by V8’s GC (the JS object is on the heap) but the actual data lives in native memory. This means process.memoryUsage().external increases, not heapUsed. A process can appear to have low heap usage while consuming gigabytes of native memory via Buffers.What interviewers are really testing: Do you understand where Buffers live in memory, why they exist separate from strings, and the security implications of allocUnsafe?Red flag answer: “Buffers are just arrays for storing data” — misses the native memory, encoding, and security aspects.Follow-up:
  • “When would you use Buffer.allocUnsafe over Buffer.alloc? What’s the risk?”
  • “If you have a Buffer containing a user’s password, how do you securely clear it from memory? Can you even do that reliably in Node?”
  • “What’s the relationship between Buffer and Uint8Array? Can you use a Buffer anywhere you’d use a typed array?”
Answer: Streams are Node’s abstraction for handling flowing data — they let you process data piece-by-piece instead of loading everything into memory at once.The 4 Types:
  1. Readable: Produces data. Examples: fs.createReadStream, http.IncomingMessage (request body), process.stdin. Has two modes: flowing (data events, automatic) and paused (must call .read() manually).
  2. Writable: Consumes data. Examples: fs.createWriteStream, http.ServerResponse, process.stdout. Returns false from .write() when its internal buffer is full (backpressure signal).
  3. Duplex: Both Readable and Writable, with independent buffers. Examples: TCP sockets (net.Socket), WebSocket connections. The read and write sides operate independently.
  4. Transform: A special Duplex where the output is a transformation of the input. The read and write sides are connected. Examples: zlib.createGzip() (compress), crypto.createCipheriv() (encrypt), CSV parsers. Data goes in one side, gets modified, comes out the other.
Object Mode: By default, streams handle Buffer/string data. In object mode (objectMode: true), streams can handle any JS value. Used in database cursors, JSON parsers, etc.The pipeline function (preferred over .pipe()):
What interviewers are really testing: Can you explain when to use each type, the difference between Duplex and Transform, and do you know about flowing vs paused mode?Red flag answer: Only knowing Readable and Writable, or not knowing that Transform is a subtype of Duplex.Follow-up:
  • “What is the difference between Duplex and Transform? Can you give an example where you’d use Duplex but not Transform?”
  • “What are the two modes of a Readable stream? How do you switch between them?”
  • “Why is stream.pipeline() preferred over .pipe() chaining?”
Answer: Backpressure is the mechanism that prevents a fast producer from overwhelming a slow consumer. Without it, data accumulates in memory and you get OOM crashes.How it works mechanically:
  1. Readable stream pushes data to the Writable stream via .write().
  2. .write() returns false when the Writable’s internal buffer exceeds highWaterMark (default 16KB for Buffer streams, 16 objects for object mode).
  3. The Readable must stop reading (.pause() in flowing mode, stop calling .push() in custom streams).
  4. When the Writable’s buffer drains, it emits a 'drain' event.
  5. The Readable resumes.
.pipe() handles this automatically:
The problem with .pipe(): It does NOT properly propagate errors through the chain or clean up streams on failure. A broken stream in a pipe chain can cause memory leaks.Solution — use pipeline:
Real-world war story: A file upload service used .pipe() without backpressure awareness. Users uploading over a fast network to a slow disk caused Node’s memory to spike to 8GB before the process was OOM-killed. The fix: switching to pipeline and setting appropriate highWaterMark values.What interviewers are really testing: Do you understand why backpressure exists, not just how to use .pipe()? Can you explain the highWaterMark and the drain event?Red flag answer: “Just use .pipe() and it handles everything.” This ignores error handling and shows no understanding of the underlying mechanism.Follow-up:
  • “What happens if you ignore the false return value from .write() and keep writing?”
  • “How would you implement custom backpressure in a Transform stream?”
  • “What is highWaterMark and how do you decide what value to set?”
Answer: This is the fundamental question of buffering vs streaming in Node.fs.readFile:
  • Reads the entire file into a single Buffer in memory.
  • Simple API: const data = await fs.promises.readFile('file.txt').
  • Memory usage = file size. A 2GB file requires 2GB of RAM.
  • Use for: small files (config, JSON, templates) — generally files under 10-50MB.
fs.createReadStream:
  • Reads in chunks (default 64KB each, configurable via highWaterMark).
  • Memory usage is constant (~64KB buffer) regardless of file size.
  • Must process data in streaming fashion (events or pipe).
  • Use for: large files, file uploads/downloads, anything where you’re transforming and forwarding data.
The numbers that matter:
The streaming pipeline for file processing:
What interviewers are really testing: Do you default to streaming for large data? This reveals whether you’ve built systems that handle real-world file sizes.Red flag answer: “I always use readFile because it’s simpler.” This works until someone uploads a 500MB file and your server crashes.Follow-up:
  • “You’re building an API that accepts CSV uploads and inserts rows into a database. Walk me through how you’d do this for a 2GB file.”
  • “What is the highWaterMark option on createReadStream and when would you tune it?”
  • “How does fs.promises.readFile differ from fs.readFileSync in terms of event loop impact?”
Answer: EventEmitter is the pub/sub backbone of Node.js. Almost every core module (Streams, HTTP Server, Process) inherits from it.
Key methods:
  • .on(event, listener) — subscribe (alias: .addListener)
  • .once(event, listener) — subscribe, auto-remove after first call
  • .off(event, listener) — unsubscribe (alias: .removeListener)
  • .emit(event, ...args) — fire synchronously (listeners run in order of registration)
  • .listenerCount(event) — how many listeners for an event
  • .removeAllListeners(event?) — nuclear option
Critical detail — synchronous execution: .emit() calls all listeners synchronously in the order they were added. This means a slow listener blocks subsequent listeners and the caller.Memory leak warning: By default, Node warns if you add more than 10 listeners to a single event (MaxListenersExceededWarning). This usually indicates a bug (adding a listener on every request without removing it).
Production pattern — typed events:
What interviewers are really testing: Do you know that emit is synchronous? Can you articulate the memory leak risk? Do you use EventEmitter for application-level decoupling?Red flag answer: “EventEmitter is just for listening to clicks” — conflating with browser events, or not knowing it’s the foundation of Node I/O.Follow-up:
  • “If emit is synchronous, what happens if one listener throws an error? Do other listeners still run?”
  • “You see MaxListenersExceededWarning in your logs. What’s your debugging approach?”
  • “When would you use EventEmitter vs a message queue like Redis Pub/Sub?”
Answer: Node provides two last-resort safety nets for unhandled errors. Getting this wrong can cause silent data corruption.process.on('uncaughtException'):
  • Fires when a synchronous error is thrown and not caught by any try/catch.
  • Critical rule: After an uncaught exception, the process is in an undefined state. Sockets may be half-written, database transactions may be uncommitted, in-memory state may be inconsistent.
  • Best practice: Log the error, flush logs, and exit the process. Let your process manager (PM2, systemd, Kubernetes) restart it.
process.on('unhandledRejection'):
  • Fires when a Promise is rejected and no .catch() or try/catch (in async/await) handles it.
  • Since Node 15+, unhandled rejections throw by default (same as uncaught exceptions). In older versions, they just logged a warning.
The “just catch it and continue” anti-pattern:
Defense in depth (what production services actually do):
  1. Wrap all async route handlers with error-catching middleware.
  2. Use domain (deprecated) or AsyncLocalStorage for request-scoped error context.
  3. Set process.on('uncaughtException') and process.on('unhandledRejection') as the last safety net — log and exit.
  4. Run behind PM2/Kubernetes that auto-restarts crashed processes.
What interviewers are really testing: Do you understand that uncaughtException means the process should die? Or do you treat it as a global catch-all?Red flag answer: “I use process.on('uncaughtException') to catch all errors so the server never goes down.” This is actively dangerous — it means you’re serving requests with potentially corrupted state.Follow-up:
  • “Why is it dangerous to continue running after an uncaught exception? Give a concrete scenario.”
  • “How does Express’s error-handling middleware relate to uncaughtException? If an error is caught by Express, does uncaughtException fire?”
  • “What changed in Node 15 regarding unhandled Promise rejections?”
Answer: Node provides both sync and async versions of most fs operations. Knowing when to use each is essential.fs.readFileSync (blocking):
  • Blocks the event loop until the file is fully read.
  • No other request, timer, or I/O callback can execute during this time.
  • On a server handling 1000 req/s, a 10ms sync read means 10ms of total blockage — ~10 requests delayed.
When sync is acceptable:
  • At startup before the server starts listening: loading config files, reading TLS certificates, parsing environment.
  • CLI tools that are not serving concurrent requests.
  • Build scripts and tooling.
When sync is a bug:
  • Inside any request handler, middleware, or event callback.
  • Inside any setInterval or recurring timer.
  • Anywhere after server.listen() is called.
Sneaky sync operations people miss:
  • require() is synchronous (file read + compile). Never call require() inside a hot path.
  • JSON.parse() on large strings (100MB+ JSON) is CPU-bound and blocks the loop.
  • crypto.pbkdf2Sync — use the async version crypto.pbkdf2 instead.
  • zlib.gzipSync — use streaming zlib.createGzip() instead.
What interviewers are really testing: Do you know which operations are secretly synchronous? Can you reason about the cascading impact on concurrency?Red flag answer: “I never use sync functions” (too absolute — sync at startup is fine and simpler) or “I use sync everywhere because my app isn’t high traffic” (will break the moment traffic increases).Follow-up:
  • “Is require() synchronous or asynchronous? What are the implications of calling require inside a request handler?”
  • “If JSON.parse is synchronous, how would you handle parsing a 500MB JSON file without blocking the event loop?”
  • “How can you detect synchronous operations that are blocking your event loop in production?”
Answer: The zlib module provides streaming compression/decompression using Gzip, Deflate, and Brotli algorithms. It is a Transform stream, making it composable with pipes.Streaming compression (the right way):
HTTP compression (Express):
Algorithm comparison:
  • Gzip: Universal support, decent ratio. Standard for HTTP (Accept-Encoding: gzip).
  • Brotli: ~15-20% better compression than Gzip, but slower to compress. Most modern browsers support it (Accept-Encoding: br). Use for static assets pre-compressed at build time.
  • Deflate: Legacy. Avoid — inconsistent implementations across clients.
What interviewers are really testing: Do you think about compression as a streaming operation? Do you know when to use Gzip vs Brotli?Red flag answer: “I just add the compression middleware and it handles everything.” Missing nuance about when NOT to compress (small responses, already-compressed content like images).Follow-up:
  • “When would you pre-compress assets at build time vs compress on-the-fly? What’s the trade-off?”
  • “What happens if you Gzip an already-compressed PNG? Does the file get smaller?”
  • “How does Brotli compare to Gzip in terms of compression ratio, CPU cost, and browser support?”
Answer: Read-Eval-Print-Loop — Node’s interactive shell for rapid prototyping and debugging.
Beyond basics — the REPL is a debugging tool:
  • Inspect live objects: Launch your app with --inspect, connect Chrome DevTools, use the console as a REPL against the running process.
  • Custom REPL: You can create a REPL that exposes your app’s internals (database connections, caches) for live debugging in development:
What interviewers are really testing: This is usually a throwaway question. The real test is whether you mention the custom REPL pattern for debugging.Follow-up:
  • “How would you expose a REPL in a running production service for emergency debugging? What are the security implications?”
Answer: The os module provides operating system-related utility methods. Critical for Cluster setup, monitoring, and capacity planning.
Production uses:
  • Cluster worker count: const workers = os.cpus().length (common pattern, but in containers os.cpus().length may report the host’s CPUs, not the container’s limit — use process.env.NUM_WORKERS or read from cgroup).
  • Health checks: Report freemem() / totalmem() ratio, loadavg().
  • Platform-specific behavior: Conditional logic based on os.platform() for path separators, shell commands, etc.
Container gotcha: In Docker/Kubernetes, os.cpus().length returns the host machine’s CPU count, not the container’s CPU limit. If the host has 64 cores but your pod has a 2-core limit, spawning 64 cluster workers wastes memory and causes excessive context switching.What interviewers are really testing: Do you know the container gotcha? This separates developers from operators.Red flag answer: Using os.cpus().length blindly for cluster workers in a containerized environment.Follow-up:
  • “In a Kubernetes pod with a 2-core CPU limit, os.cpus().length returns 64. How do you determine the correct number of worker processes?”
  • “How would you build a simple system health dashboard using only Node.js built-in modules?“

3. Web & Network (HTTP/Express)

Answer: Node’s http module provides a low-level HTTP server. Understanding it deeply is essential before using Express.
Key insight: req and res are streams. The request body is not immediately available — you must consume the Readable stream. This is why Express needs express.json() middleware (it does this consumption for you).What happens under the hood:
  1. Libuv receives a TCP connection.
  2. http_parser (C library, now llhttp since Node 12) parses the raw bytes into HTTP headers.
  3. Node creates IncomingMessage and ServerResponse objects.
  4. Your callback fires with these objects.
  5. Response is written back through the socket.
Connection handling: The server emits events — 'request' (most common), 'connection' (raw TCP socket), 'upgrade' (WebSocket handshake), 'close'.What interviewers are really testing: Do you understand that req/res are streams? Most developers jump to Express without understanding the raw HTTP server.Red flag answer: “I’ve never used http.createServer, I just use Express.” Express is built on top of this — understanding the foundation matters.Follow-up:
  • “What is the 'upgrade' event on an HTTP server and when would you use it?”
  • “How does http_parser (now llhttp) work, and why was it rewritten?”
  • “What’s the difference between res.end() and res.write() followed by res.end()?”
Answer: Middleware is Express’s core abstraction — a function with access to (req, res, next) that forms a chain of responsibility pattern.Types of middleware:
  1. Application-level: app.use(fn) — runs on every request.
  2. Route-level: router.use(fn) or app.get('/path', fn, handler) — scoped to routes.
  3. Error-handling: (err, req, res, next) — 4-parameter signature, Express detects the arity.
  4. Built-in: express.json(), express.static(), express.urlencoded().
  5. Third-party: cors, helmet, morgan, compression.
Execution flow:
The next() contract:
  • next() — pass to next middleware.
  • next('route') — skip remaining middleware in current route, go to next route.
  • next(err) — skip to error-handling middleware.
  • Not calling next() and not sending a response = request hangs until client timeout.
Real-world middleware stack (ordered):
What interviewers are really testing: Do you understand middleware ordering matters? Can you reason about what happens when next() is not called?Red flag answer: “Middleware is just a function that runs before the route handler.” This misses the chain-of-responsibility pattern, error middleware, and ordering significance.Follow-up:
  • “What happens if a middleware calls next() AND sends a response? What error do you get?”
  • “How does Express distinguish error-handling middleware from regular middleware?”
  • “You have 15 middleware functions and a request takes 500ms. How do you find which middleware is the bottleneck?”
Answer: Express error-handling middleware has a 4-parameter signature: (err, req, res, next). Express uses the function’s .length property (arity) to detect it.
Critical placement rule: Error middleware MUST be defined after all routes and other middleware. Express processes middleware in order; if the error handler is defined before a route, errors from that route won’t reach it.The async error problem: Express 4 does NOT catch errors thrown in async route handlers. You need a wrapper:
What interviewers are really testing: Do you know about the async error gap in Express 4? This is one of the most common production bugs in Node web servers.Red flag answer: “I just add a try/catch in every route handler.” While this works, it’s verbose and error-prone — missing one handler means an unhandled rejection.Follow-up:
  • “Why does Express check the function’s arity to detect error middleware? What happens if you use arrow functions with destructuring that changes the apparent parameter count?”
  • “How does Express 5 handle async errors differently from Express 4?”
  • “How would you implement different error responses for API routes (JSON) vs web routes (HTML error page)?”
Answer: HTTP request bodies are streams — the data arrives in chunks. Body parsing middleware consumes the stream, buffers it, parses it, and attaches the result to req.body.Native approach (no Express):
Express built-in parsers:
Security considerations:
  • Always set limit: Without a limit, an attacker can send a 1GB JSON body and OOM your server. Default is '100kb'.
  • extended: true vs false: extended: true uses the qs library which supports nested objects (user[name]=foo). extended: false uses querystring which only supports flat key-value pairs. Nested object parsing can be exploited for prototype pollution — validate inputs.
  • Content-Type spoofing: A request claiming Content-Type: application/json but sending garbage will cause JSON.parse to throw. The Express json() middleware handles this and returns 400.
What interviewers are really testing: Do you set body size limits? Do you understand that body parsing is a streaming operation?Red flag answer: Using express.json() without a limit option, or not knowing that req.body requires middleware to be populated.Follow-up:
  • “What happens if express.json() is not in the middleware stack and you try to access req.body?”
  • “How would you handle a request that sends Content-Type: application/json but the body is invalid JSON?”
  • “Why is setting a body size limit a security measure? What specific attack does it prevent?”
Answer: Cross-Origin Resource Sharing — the browser’s mechanism for controlling which origins can make requests to your API.How CORS actually works:
  1. Simple requests (GET, POST with form content types): Browser adds Origin header, server responds with Access-Control-Allow-Origin. If it doesn’t match, browser blocks the response (the request still hits your server!).
  2. Preflight requests (PUT, DELETE, custom headers, JSON content type): Browser sends an OPTIONS request first with Access-Control-Request-Method and Access-Control-Request-Headers. Server must respond with allowed methods/headers. Only then does the actual request fire.
Key headers:
  • Access-Control-Allow-Origin: Which origins can access (* or specific origin)
  • Access-Control-Allow-Methods: Allowed HTTP methods
  • Access-Control-Allow-Headers: Allowed custom headers
  • Access-Control-Allow-Credentials: Whether cookies/auth are allowed (true/omit)
  • Access-Control-Max-Age: How long to cache preflight results (seconds)
Critical gotcha: Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true are mutually exclusive. If you need cookies/auth, you must specify the exact origin.
CORS is NOT a server-side security measure: CORS is enforced by browsers. curl, Postman, and server-to-server requests completely bypass CORS. It protects users from malicious websites making requests on their behalf, not your API from all unauthorized access.What interviewers are really testing: Do you understand preflight requests? Do you know CORS is browser-enforced, not server-enforced?Red flag answer: “I just set Access-Control-Allow-Origin: * to fix CORS errors.” This disables the protection entirely and breaks credentialed requests.Follow-up:
  • “A frontend developer says ‘I’m getting a CORS error.’ Where is the problem — frontend or backend? How do you debug it?”
  • “Why can’t you use origin: '*' with credentials: true? What would happen if you could?”
  • “If CORS is browser-only, how do you protect your API from unauthorized server-to-server access?”
Answer: JWT is a stateless authentication token — the server doesn’t need to store session data. The token itself contains all the information needed to authenticate the user.Structure (three Base64URL-encoded parts separated by dots):
  1. Header: Algorithm and token type — { "alg": "HS256", "typ": "JWT" }
  2. Payload: Claims (data) — { "sub": "user123", "role": "admin", "exp": 1700000000 }
  3. Signature: HMACSHA256(base64(header) + "." + base64(payload), secret) — proves the token hasn’t been tampered with.
Critical security details:
  • The payload is NOT encrypted — it’s Base64 encoded (anyone can decode it). Never put passwords, SSNs, or sensitive data in the payload.
  • Storage options (ranked by security):
    1. HttpOnly + Secure + SameSite=Strict cookie (best — immune to XSS)
    2. In-memory variable (lost on page refresh, but very secure)
    3. LocalStorage (convenient but vulnerable to XSS attacks)
  • HS256 vs RS256: HS256 uses a shared secret (both sides know it). RS256 uses a public/private key pair (only the auth server has the private key — better for microservices where you don’t want to distribute secrets).
Token refresh pattern:
  • Short-lived access token (15 min) + long-lived refresh token (7 days).
  • Refresh token stored in HttpOnly cookie, access token in memory.
  • When access token expires, use refresh token to get a new one.
What interviewers are really testing: Do you know JWTs are not encrypted? Do you understand the storage trade-offs? Can you explain why refresh tokens exist?Red flag answer: “I store the JWT in localStorage” without acknowledging the XSS risk, or “JWTs are encrypted so they’re secure.”Follow-up:
  • “How do you revoke a JWT before it expires? Doesn’t that defeat the purpose of stateless auth?”
  • “What’s the difference between HS256 and RS256? When would you choose one over the other?”
  • “A JWT is stolen. What damage can the attacker do, and how do you limit the blast radius?”
Answer: This is about where state lives — client-side vs server-side.Sessions (server-side state):
  • A random session ID is stored in a cookie on the client.
  • The actual session data (user info, cart, preferences) lives on the server in a session store (memory, Redis, database).
  • Stateful: The server must look up the session on every request.
  • Pros: Data isn’t exposed to the client, easy to invalidate (delete server-side), no size limit.
  • Cons: Server must maintain state (scaling challenge), requires sticky sessions or shared store in clusters.
Cookies (client-side state):
  • Data is stored directly in the browser cookie.
  • Sent automatically with every request to the cookie’s domain.
  • Size limit: ~4KB per cookie.
  • Security flags: HttpOnly (no JS access), Secure (HTTPS only), SameSite (CSRF protection), Domain, Path.
The scaling question: Sessions in server memory break when you scale horizontally (load balancer sends request to different server). Solutions:
  1. Redis session store: All servers read/write sessions to shared Redis. Standard approach.
  2. Sticky sessions: Load balancer always routes a user to the same server. Fragile — if that server dies, all its sessions are lost.
  3. JWT (stateless): No server-side state needed. Trade-off: harder to revoke.
What interviewers are really testing: Can you reason about state management in distributed systems? Do you know why “sessions in memory” breaks at scale?Red flag answer: “Cookies store data, sessions store data, they’re basically the same.” Misses the fundamental architectural distinction.Follow-up:
  • “You have 10 Node servers behind a load balancer. How do you handle sessions without sticky sessions?”
  • “What’s the SameSite cookie attribute and how does it protect against CSRF?”
  • “When would you choose session-based auth over JWT, and vice versa?”
Answer: File uploads use multipart/form-data encoding — the body contains multiple parts separated by a boundary string. You need specialized parsing.Common approaches:
  1. Multer (Express middleware): The standard choice. Handles multipart/form-data parsing, stores files to disk or memory.
  2. Busboy/Busbody (low-level stream parser): Direct stream access, more control, works with any Node HTTP server.
  3. Formidable: Alternative to Multer with streaming support.
Production-grade upload pattern (stream to S3, never touch disk):
Security checklist:
  • Validate MIME type on the server (don’t trust Content-Type — read magic bytes).
  • Set file size limits — unbounded uploads = DoS vector.
  • Generate random filenames — never use the original filename (path traversal attacks: ../../etc/passwd).
  • Don’t serve uploads from the same domain — use a separate CDN/bucket to prevent XSS via uploaded HTML/SVG files.
  • Virus scan uploaded files in production (ClamAV or cloud-based scanning).
Serverless/container consideration: Don’t write files to local disk in ephemeral environments (Lambda, ECS Fargate). Stream directly to object storage (S3, GCS).What interviewers are really testing: Do you think about security, size limits, and storage strategy? Or do you just save to disk and serve from the same server?Red flag answer: “I save the file to the uploads/ directory and serve it from Express.” This has security, scaling, and reliability problems.Follow-up:
  • “How would you handle a 5GB video upload without running out of memory?”
  • “Why should you never serve user-uploaded files from the same origin as your application?”
  • “How do you validate that an uploaded file is actually a JPEG and not a renamed executable?”
Answer: WebSockets provide persistent, full-duplex communication over a single TCP connection. Unlike HTTP’s request-response model, either side can send data at any time.The handshake:
  1. Client sends an HTTP request with Upgrade: websocket header.
  2. Server responds with 101 Switching Protocols.
  3. The TCP connection is now a WebSocket — no more HTTP framing.
Raw WebSocket (ws library) vs Socket.IO:
  • ws: Thin wrapper over the WebSocket protocol. Lightweight, no fallbacks, no rooms, no auto-reconnection. ~40KB.
  • Socket.IO: Full-featured library with auto-reconnection, rooms/namespaces, binary support, fallback to HTTP long-polling, acknowledgements. ~300KB client bundle. Uses its own protocol on top of WebSocket.
  • Key decision: If all your clients support WebSocket (modern browsers, mobile apps), use ws. If you need reliability features and broadcasting, use Socket.IO.
Scaling WebSockets (the hard part):
  • WebSocket connections are stateful — each is tied to a specific server process.
  • With multiple servers, you need a pub/sub adapter (e.g., @socket.io/redis-adapter) so a message emitted on Server A reaches clients connected to Server B.
  • Load balancers must support WebSocket upgrades (sticky sessions or connection upgrade pass-through).
  • 10k connections per server process is reasonable; 100k+ requires tuning OS limits (ulimit -n).
What interviewers are really testing: Do you know the difference between ws and Socket.IO? Can you reason about scaling stateful connections?Red flag answer: “Socket.IO is WebSocket.” No — Socket.IO is a library that uses WebSocket as one of its transports (with HTTP long-polling as fallback).Follow-up:
  • “You have a chat app on 10 servers. User A is connected to Server 3, User B to Server 7. How does a message get from A to B?”
  • “What happens to WebSocket connections during a deployment/restart? How do you handle this gracefully?”
  • “When would you choose Server-Sent Events (SSE) over WebSocket?”
Answer: By default in older Node.js versions, every outgoing HTTP request creates a new TCP connection — that’s a 3-way TCP handshake + potential TLS handshake on every request. This is extremely wasteful.The solution — HTTP Agent with Keep-Alive:
Performance impact: For a service making 1000 req/s to a downstream API:
  • Without keep-alive: 1000 TCP handshakes/sec (~1ms each) = 1 second of cumulative handshake overhead, plus ephemeral port exhaustion risk.
  • With keep-alive: ~50 persistent connections handle all 1000 requests. Near-zero handshake overhead.
Node 19+ change: globalAgent has keepAlive: true by default. Before Node 19, you had to explicitly enable it.Common pitfall — socket exhaustion: If maxSockets is too low and request volume is high, requests queue behind the socket limit. If it’s too high, you might exhaust the downstream server’s connection limit.
What interviewers are really testing: Do you know about connection reuse and its impact on latency/throughput? Have you tuned socket pool settings?Red flag answer: “HTTP connections are handled automatically, you don’t need to configure anything.” True in modern Node, but this shows no understanding of what’s happening underneath.Follow-up:
  • “What is ephemeral port exhaustion and how does keep-alive prevent it?”
  • “You see ECONNRESET errors from a downstream service. What’s happening and how does the HTTP agent configuration relate?”
  • “How do you monitor the state of your connection pool (active, idle, pending connections)?“

4. Scaling & Performance

Answer: The Cluster module lets you create child processes (workers) that share the same server port, utilizing multiple CPU cores.How it works:
  1. Master process calls cluster.fork() to create worker processes.
  2. Workers are full Node.js instances with their own V8 and event loop.
  3. The master accepts incoming connections and distributes them to workers.
  4. Load balancing: Round-robin (default on all platforms except Windows) or OS-level (let the kernel decide).
What workers share: The listening port (via file descriptor passing). What workers do NOT share: Memory. Each worker has its own heap, its own V8 instance. Global variables in one worker are invisible to others. In-memory caches are duplicated per worker.Implications for state:
  • Session data in memory? Each worker has its own copy — requests to different workers get different sessions. Use Redis.
  • In-memory cache? Duplicated N times (N = workers). Use Redis.
  • Rate limiting in memory? Each worker tracks independently — actual rate is N times the limit. Use Redis.
Worker count heuristic: os.cpus().length for CPU-bound workloads, os.cpus().length * 1.5 - 2 for I/O-bound workloads (workers spend time waiting on I/O so more can be useful).What interviewers are really testing: Do you understand the memory isolation between workers? Can you reason about what breaks when you naively cluster a stateful app?Red flag answer: “Cluster makes your app use all cores and everything just works.” Missing the stateful implications.Follow-up:
  • “What happens to in-progress requests when a worker crashes? Are they lost?”
  • “How does the master process distribute connections to workers? What’s the difference between round-robin and OS scheduling?”
  • “Why might PM2’s cluster mode be preferred over using the cluster module directly?”
Answer: PM2 is a production process manager for Node.js that handles concerns you’d otherwise implement manually.Key features:
  • Cluster mode: pm2 start app.js -i max — forks workers for all CPU cores.
  • Auto-restart on crash: Worker dies, PM2 restarts it within milliseconds. Configurable restart limits.
  • Zero-downtime reload: pm2 reload app — restarts workers one at a time (new worker starts before old one stops). No dropped requests.
  • Log management: Aggregates stdout/stderr from all workers. pm2 logs, log rotation.
  • Monitoring: pm2 monit shows real-time CPU, memory, event loop lag per process.
  • Ecosystem file: ecosystem.config.js for declarative configuration.
PM2 vs Docker/Kubernetes: In containerized environments, you typically run one process per container and let the orchestrator handle scaling, restarts, and health checks. PM2’s cluster mode inside a container is usually redundant — instead, scale by increasing container replicas. However, PM2 is still useful for non-containerized deployments (bare EC2, traditional VMs).What interviewers are really testing: Do you know when PM2 is appropriate vs when the orchestrator handles it? Can you explain zero-downtime reload?Red flag answer: “I use PM2 in production inside my Docker container in cluster mode.” This is usually double-clustering and complicates debugging.Follow-up:
  • “How does PM2’s zero-downtime reload work? What happens to in-flight requests during the reload?”
  • “In a Kubernetes deployment, would you still use PM2? Why or why not?”
  • “How would you configure PM2 to auto-restart a worker that exceeds 500MB of memory?”
Answer: Worker Threads (worker_threads module) enable true parallel JavaScript execution within a single Node.js process. Unlike Cluster (separate processes), workers share the same process and can share memory.Key differences from Cluster:SharedArrayBuffer for zero-copy communication:
Gotcha — atomics and race conditions: If two threads write to the same SharedArrayBuffer location, you get race conditions. Use Atomics.add(), Atomics.compareExchange(), etc. for thread-safe operations.When to use Worker Threads:
  • Image resizing (sharp library already uses threads internally)
  • Parsing large JSON/CSV files
  • Cryptographic operations (bcrypt, key derivation)
  • Data compression
  • Number crunching, simulations
When NOT to use Worker Threads:
  • I/O-bound work (that’s what the event loop is for)
  • Simple request handling (overhead of creating/managing threads outweighs benefit)
What interviewers are really testing: Can you distinguish between Worker Threads and Cluster? Do you know about SharedArrayBuffer and the race condition risks?Red flag answer: “Worker Threads and Cluster are the same thing.” They have fundamentally different memory models and use cases.Follow-up:
  • “How does postMessage serialize data between threads? What types can’t be transferred?”
  • “What is a Transferable object and when would you use worker.postMessage(data, [transferList])?”
  • “You need to process 10,000 images. Do you create 10,000 worker threads? What’s your architecture?”
Answer: Memory leaks in Node.js are insidious — the process slowly consumes more memory over hours/days until it OOMs. Here are the most common causes ranked by how frequently they appear in production.Top causes:
  1. Event Listener Accumulation (most common):
Fix: Remove listeners when done, or use .once().
  1. Closures Holding Large Objects:
  1. Global Variables / Module-Level Caches Without TTL:
Fix: Use LRU cache with max size (lru-cache package) or Redis with TTL.
  1. Unreferenced Timers:
  1. Forgotten Streams: Readable streams that are created but never consumed or destroyed hold their internal buffer in memory.
Detection tools:
  • process.memoryUsage() logged over time (look for monotonic heapUsed growth)
  • --inspect + Chrome DevTools heap snapshots (compare two snapshots, look at “Objects allocated between snapshots”)
  • clinic doctor / clinic heapprofiler for automated analysis
  • node --heapsnapshot-signal=SIGUSR2 — take a heap snapshot on demand
What interviewers are really testing: Can you name specific patterns (not just “memory leaks happen”)? Do you know how to detect and diagnose them?Red flag answer: “Memory leaks don’t really happen in JavaScript because of garbage collection.” GC only collects unreachable objects — all the patterns above keep objects reachable.Follow-up:
  • “Walk me through how you’d diagnose a memory leak in a production Node service that’s slowly growing from 200MB to 2GB over 24 hours.”
  • “What’s the difference between a memory leak and high memory usage? How do you distinguish them?”
  • “How does the WeakRef / WeakMap help prevent certain kinds of memory leaks?”
Answer: The N+1 problem is a query explosion pattern where fetching a list of N items triggers N additional queries for related data.Example:
Solutions:
  1. Eager Loading (ORM-level): Load related data in a JOIN or second query.
  1. DataLoader (GraphQL): Batches individual lookups within a single event loop tick into one query.
  1. Database Views / Denormalization: For read-heavy paths, pre-compute the joined data.
Why this matters at scale: 51 queries at 2ms each = 102ms. Seems fine. But with 100 concurrent requests = 5,100 queries hitting your database simultaneously. With proper batching: 200 queries. That’s a 25x reduction in database load.What interviewers are really testing: Do you know DataLoader specifically? Can you reason about database load multiplication?Red flag answer: “I just add .include() to everything.” Eager loading everything causes over-fetching and can be worse than N+1 for deeply nested relationships.Follow-up:
  • “How does DataLoader batch requests? What’s the mechanism that collects individual .load() calls into a single batch?”
  • “DataLoader caches results per-request. What happens if you share a DataLoader across requests?”
  • “When is eager loading worse than N+1? Give a scenario.”
Answer: Every database query needs a TCP connection. Opening a new TCP connection involves a 3-way handshake (~1ms on LAN, 50-100ms cross-region), plus TLS negotiation (~5-30ms), plus database authentication. Connection pooling amortizes this cost.How pools work:
  1. At startup, the pool opens min connections.
  2. When your code needs a connection, it borrows one from the pool.
  3. After the query completes, the connection is returned to the pool (not closed).
  4. If all connections are in use, new requests wait in a queue until one is returned.
  5. If the queue exceeds a timeout, the request fails with a connection timeout error.
Sizing the pool (critical production decision):
  • Too small: Requests queue up waiting for connections. Latency spikes under load.
  • Too large: Each connection uses ~5-10MB on the database server. 100 connections x 10 workers = 1000 connections on the DB, overwhelming it.
  • Rule of thumb: Start with max = (2 * CPU cores) + effective_spindle_count (per PostgreSQL docs), typically 10-20 per Node process.
  • With Cluster mode: If you have 8 workers, each with a pool of 20 = 160 total connections. Make sure your database can handle this.
Connection pooler tools: For large deployments, use a connection pooler like PgBouncer (PostgreSQL) or ProxySQL (MySQL) between your app and the database. They multiplex hundreds of application connections onto a small number of actual database connections.What interviewers are really testing: Can you size a pool correctly? Do you know about the connection multiplication problem with Cluster/multiple instances?Red flag answer: “I set max: 100 to be safe.” More connections is not safer — it can kill your database.Follow-up:
  • “You have 10 Node instances, each with a pool of 20 connections. Your database allows 100 connections max. What happens?”
  • “What is PgBouncer and when would you use it?”
  • “How do you monitor pool health (idle connections, queue depth, wait time)?”
Answer: Caching is the most impactful performance optimization you can make, but choosing the wrong strategy creates consistency bugs that are hard to debug.Two categories:
  1. In-Process (Node memory):
    • Map, lru-cache, node-cache.
    • Pros: Fastest possible (~nanoseconds), no network hop.
    • Cons: Lost on restart, duplicated in every Cluster worker, no sharing between instances, bounded by V8 heap size.
    • Use for: Compiled regex, parsed config, tiny lookup tables that change rarely.
  2. Distributed (Redis/Memcached):
    • Shared across all instances, survives restarts (Redis with AOF/RDB persistence).
    • Pros: Shared, persistent, rich data structures (sorted sets for leaderboards, pub/sub, Lua scripting).
    • Cons: Network hop (~0.5-2ms per request), serialization overhead.
    • Use for: API response caching, session storage, rate limit counters, feature flags.
Cache invalidation strategies (the hard part):
  • TTL (Time-To-Live): Set expiry. Simple but allows stale data during TTL window.
  • Write-Through: Write to cache AND database simultaneously. Consistent but slower writes.
  • Write-Behind (Write-Back): Write to cache, asynchronously flush to database. Fast but data loss risk.
  • Cache-Aside (Lazy Loading): Check cache first, miss goes to DB, result written to cache. Most common pattern.
What interviewers are really testing: Do you know multiple caching strategies and their trade-offs? Can you reason about cache invalidation?Red flag answer: “I cache everything in a global object” — no eviction, no TTL, duplicated across workers, grows unbounded.Follow-up:
  • “What is a cache stampede (thundering herd) and how do you prevent it?”
  • “How do you decide what TTL to set? What’s the trade-off between a 10-second TTL and a 1-hour TTL?”
  • “Your cache and database are out of sync. How do you detect and fix this?”
Answer: Profiling is how you find the actual bottleneck instead of guessing. Node has excellent built-in and ecosystem profiling tools.CPU Profiling (finding slow functions):
Flame Graphs (visual CPU profiling):
Reading a flame graph: Width = time spent. Look for wide bars (functions that take the most time). Tall stacks = deep call chains. Plateau patterns = the same function called many times (potential optimization target).Memory Profiling:
Event Loop Profiling (finding blockage):
Production monitoring metrics to track:
  • Event loop lag (P50, P99)
  • Heap used / heap total
  • GC pause duration and frequency
  • Active handles and requests (process._getActiveHandles().length)
What interviewers are really testing: Have you actually profiled a Node app? Can you interpret a flame graph? Do you know the difference between CPU and memory profiling?Red flag answer: “I add console.time() everywhere.” This is manual and imprecise — real profiling tools show the full picture.Follow-up:
  • “You have a Node API where P99 latency jumped from 50ms to 500ms. Walk me through your profiling approach.”
  • “What’s the difference between a CPU profile and a flame graph? When would you use each?”
  • “How do you profile a Node app in production without significant performance overhead?”
Answer: Even without explicit fs.readFileSync calls, several common operations can block the event loop and cause latency spikes.Common blockers people miss:
  1. JSON.parse / JSON.stringify on large objects: These are synchronous and CPU-bound. A 50MB JSON string can block the loop for 200-500ms.
  1. Regex Denial of Service (ReDoS): Certain regex patterns with backtracking can take exponential time on crafted inputs.
  1. Sorting large arrays: Array.sort() on 1M+ elements blocks for 100ms+.
  2. Crypto operations: crypto.pbkdf2Sync, crypto.scryptSync — always use async versions.
  3. Template rendering: Rendering complex templates (EJS, Handlebars) with large datasets.
  4. require() in hot paths: Module loading reads files synchronously and compiles them.
How to detect event loop blocking:
Libraries: blocked-at (detects which call site is blocking), event-loop-lag (metrics).What interviewers are really testing: Can you identify non-obvious blockers? Do you have a strategy for detecting them?Red flag answer: “I avoid blocking by using async/await everywhere.” async/await only helps with I/O — await JSON.parse(huge) still blocks because JSON.parse is synchronous.Follow-up:
  • “How would you safely parse a 100MB JSON file without blocking the event loop?”
  • “What is ReDoS and how do you protect against it? Can you write a safe regex for email validation?”
  • “You notice periodic 200ms latency spikes every few minutes. How would you determine if the event loop is being blocked and by what?”
Answer: How services communicate is one of the most consequential architectural decisions. Each pattern has specific strengths and weaknesses.Synchronous (request-response):
  • HTTP/REST: Universal, human-readable (JSON), easy to debug with curl. Overhead: TCP connection + TLS + HTTP framing + JSON serialization. Latency: ~5-50ms per hop. Use for: CRUD APIs, simple service-to-service calls.
  • gRPC: Protocol Buffers (binary serialization, 5-10x smaller than JSON), HTTP/2 (multiplexing, streaming), strict contract via .proto files. Latency: ~1-5ms per hop. Use for: high-throughput internal communication, streaming data, polyglot environments.
Asynchronous (event-driven):
  • Message Queues (RabbitMQ): Point-to-point or fan-out. Guaranteed delivery with acknowledgements. Use for: task queues, work distribution, decoupling services that don’t need immediate responses.
  • Event Streaming (Kafka): Distributed log, ordered within partitions, replay capability, high throughput (millions of messages/sec). Use for: event sourcing, real-time analytics pipelines, audit logs, inter-service events at scale.
  • Redis Pub/Sub: Simple, fast, fire-and-forget (no persistence/replay). Use for: real-time notifications, cache invalidation broadcasts.
Choosing between them:Pattern: Saga for distributed transactions: When an order involves inventory, payment, and shipping services, you can’t use a single database transaction. Use the Saga pattern — each service performs its step and publishes an event. If any step fails, compensating transactions undo previous steps.What interviewers are really testing: Can you pick the right communication pattern for a given scenario? Do you understand sync vs async trade-offs?Red flag answer: “REST is always the best because it’s simple.” Simplicity matters, but using REST for everything leads to tight coupling and cascading failures.Follow-up:
  • “Service A calls Service B, which calls Service C. Service C is down. What happens with synchronous REST vs async messaging?”
  • “When would you choose Kafka over RabbitMQ? What’s the fundamental architectural difference?”
  • “How do you handle distributed transactions across microservices that each have their own database?“

5. Security & Testing

Answer: Injection attacks exploit the boundary between code and data — when user input is treated as executable query logic.SQL Injection:
NoSQL Injection (MongoDB):
Defense in depth:
  1. Parameterized queries (primary defense — prevents the attack entirely)
  2. Input validation (reject unexpected types/formats before they reach the query)
  3. Least privilege (DB user should have minimal permissions — no DROP TABLE)
  4. ORM/ODM (Sequelize, Prisma, Mongoose — they parameterize by default, but raw queries still need care)
  5. WAF rules (Web Application Firewall as an additional layer)
What interviewers are really testing: Do you know about NoSQL injection (not just SQL)? Do you rely on parameterized queries or just “sanitization”?Red flag answer: “I use an ORM so injection is impossible.” ORMs help, but raw queries within an ORM are still vulnerable. Also, ORM query builder methods can still be exploited if you pass unsanitized objects.Follow-up:
  • “Can injection happen with an ORM like Prisma or Sequelize? How?”
  • “What’s the difference between parameterized queries and escaping/sanitizing input? Which is more reliable?”
  • “How would you test for injection vulnerabilities in an existing codebase?”
Answer: XSS occurs when an attacker injects malicious JavaScript that executes in another user’s browser. It’s the most common web vulnerability.Three types:
  1. Stored XSS: Attacker saves malicious script in the database (e.g., a comment containing a script tag that steals cookies via document.cookie). Every user who views the comment executes the script.
  2. Reflected XSS: Malicious script is in the URL and reflected in the response: https://site.com/search?q=<script>alert(1)</script>.
  3. DOM-based XSS: Client-side JS inserts untrusted data into the DOM without sanitization: document.innerHTML = userInput.
Backend defenses (Node.js perspective):
  1. Output encoding/escaping: Use a templating engine that auto-escapes (EJS with <%= %>, Handlebars, React’s JSX). Never use <%- %> (raw output) with user data.
  2. Content Security Policy (CSP) header: Restricts which scripts can execute.
  1. HttpOnly cookies: Prevents JavaScript from accessing cookies (document.cookie returns nothing).
  2. Input sanitization (secondary defense): Libraries like DOMPurify (client-side) or sanitize-html (server-side) for when you must accept HTML input (rich text editors).
What interviewers are really testing: Do you know all three types? Do you rely on CSP, not just escaping? Can you articulate why HttpOnly cookies matter?Red flag answer: “I just escape all user input.” Escaping prevents stored/reflected XSS but doesn’t protect against DOM-based XSS. Also, CSP is the strongest defense layer.Follow-up:
  • “What is Content Security Policy and how does it prevent XSS even if your escaping has a bug?”
  • “How does React prevent XSS by default? Can you still have XSS in a React app?”
  • “What’s the difference between encoding/escaping and sanitizing? When do you use each?”
Answer: CSRF tricks a user’s browser into making an unwanted request to a site where the user is already authenticated. The browser automatically includes the user’s cookies (session, JWT).Attack scenario:
  1. User is logged into bank.com (session cookie is set).
  2. User visits evil-site.com which has: <img src="https://bank.com/transfer?to=attacker&amount=10000">.
  3. Browser sends the request to bank.com WITH the user’s session cookie.
  4. bank.com processes the transfer because the cookie is valid.
Defenses:
  1. SameSite Cookie Attribute (strongest, simplest):
  1. CSRF Tokens (traditional approach):
  1. Double Submit Cookie: Send CSRF token in both a cookie and a request header. Server checks they match. Works because evil-site.com can trigger sending cookies but cannot read them or set custom headers on cross-origin requests.
  2. Origin/Referer Header Validation: Check that the Origin or Referer header matches your domain. Less reliable (can be stripped by proxies).
Modern reality: With SameSite=Lax as the default in modern browsers (Chrome 80+), CSRF attacks are largely mitigated for POST requests. But you should still use CSRF tokens for defense in depth, especially for older browser support.What interviewers are really testing: Do you understand the attack mechanism? Do you know SameSite cookies are the modern primary defense?Red flag answer: “CSRF is the same as XSS.” They’re completely different attacks — XSS executes code in the victim’s browser, CSRF tricks the browser into making authenticated requests.Follow-up:
  • “With SameSite=Strict, what breaks? Why might you choose Lax instead?”
  • “How does CSRF differ from XSS? Can you combine them for a more powerful attack?”
  • “Is CSRF possible with JWT authentication stored in localStorage instead of cookies?”
Answer: Helmet is a collection of 15+ middleware functions that set HTTP security headers. It’s the fastest security win for any Express app — a single line of code addresses multiple attack vectors.
Key headers Helmet sets:Customization (you should customize CSP for your app):
What interviewers are really testing: Can you name specific security headers and what they protect against? Do you customize Helmet or just use defaults?Red flag answer: “I add app.use(helmet()) and security is handled.” Helmet is a starting point, not complete security.Follow-up:
  • “What is HSTS and what problem does it solve? What happens if you set HSTS and then lose your TLS certificate?”
  • “What is clickjacking and how does X-Frame-Options prevent it?”
  • “If you’re using a CDN for your JavaScript, how do you configure CSP to allow it while still preventing XSS?”
Answer: Rate limiting controls how many requests a client can make in a time window, protecting against DDoS, brute-force attacks, and API abuse.Strategies:
  1. Fixed Window: Count requests in fixed time windows (e.g., 100 requests per 15 minutes). Simple but has burst problems at window boundaries — a client can send 100 requests at 14:59 and 100 at 15:00 = 200 in 2 minutes.
  2. Sliding Window: Calculates rate based on a sliding window. Smoother than fixed window. express-rate-limit uses this.
  3. Token Bucket: Tokens accumulate at a fixed rate (e.g., 10/sec). Each request consumes a token. If no tokens, request is rejected. Allows bursts up to the bucket size. Used by AWS, Stripe, most production APIs.
  4. Leaky Bucket: Requests enter a queue (bucket) and are processed at a fixed rate. Excess requests overflow (rejected). Smooths traffic to a constant rate.
Implementation layers (defense in depth):
Application-level with Redis (for distributed systems):
Why Redis is essential: With express-rate-limit using memory store and Cluster mode with 8 workers, the actual rate limit is 8x what you configured (each worker tracks independently). Redis provides a single shared counter.What interviewers are really testing: Do you know the difference between rate limiting strategies? Do you understand the distributed system implications?Red flag answer: “I use express-rate-limit with default memory store in production.” This breaks with multiple processes/servers.Follow-up:
  • “What’s the difference between Token Bucket and Leaky Bucket? When would you choose one over the other?”
  • “How do you rate limit by API key for authenticated users vs by IP for anonymous users? What about users behind a NAT?”
  • “You rate limit at 100 req/min, but an attacker uses 1000 different IPs. How do you handle this?”
Answer: Unit testing in Node.js means testing functions/modules in isolation by mocking their dependencies.Testing pyramid for Node backends:
  1. Unit tests (70%): Individual functions, service methods, utility functions. Fast, no I/O.
  2. Integration tests (20%): Multiple modules together, often hitting a real test database.
  3. End-to-end tests (10%): Full HTTP request lifecycle with supertest.
Jest vs Mocha:
  • Jest: Zero-config, built-in mocking, snapshot testing, parallel test runner, code coverage. The default choice for most Node projects.
  • Mocha: More flexible, needs separate assertion (Chai) and mocking (Sinon) libraries. Preferred in some enterprise environments.
Testing patterns for Node services:
Common mistakes:
  • Testing implementation details instead of behavior (checking which internal methods were called vs verifying the output).
  • Not testing error paths (only happy path coverage).
  • Mocking too much — if everything is mocked, you’re testing your mocks, not your code.
What interviewers are really testing: Do you write tests that test behavior, not implementation? Do you mock at the right boundaries?Red flag answer: “I test by running the app manually and checking the output.” No automated testing means no confidence in changes.Follow-up:
  • “What’s the difference between a mock, a stub, and a spy? When do you use each?”
  • “How do you test an Express route handler? What is supertest?”
  • “When is mocking harmful? Give an example where too much mocking hid a real bug.”
Answer: Dependency Injection (DI) means passing dependencies into a function/class from the outside rather than creating them internally. This is the single most important pattern for testable Node.js code.Without DI (hard to test):
With DI (testable):
DI approaches in Node:
  1. Constructor injection (classes): new Service(db, cache, logger). Most explicit.
  2. Factory functions: createService({ db, cache }). Flexible, functional style.
  3. DI containers (InversifyJS, tsyringe, awilix): Automatic resolution of dependencies. More magic but handles complex graphs.
What interviewers are really testing: Do you design for testability? Can you explain DI without it being “just passing arguments”?Red flag answer: “I use jest.mock() to mock the require calls.” This works but is a testing workaround for bad design — proper DI is a better architectural solution.Follow-up:
  • “What are the trade-offs of using a DI container vs manual dependency injection in Node?”
  • “How does DI relate to the SOLID principles, specifically the Dependency Inversion Principle?”
  • “In Express, how would you inject dependencies into route handlers without a DI container?”
Answer: TDD is a development methodology: write the test first, watch it fail, write the minimal code to pass, then refactor. The cycle is Red -> Green -> Refactor.The cycle:
  1. Red: Write a test for the next piece of functionality. Run it — it fails (because the code doesn’t exist yet).
  2. Green: Write the minimum code to make the test pass. No more, no less.
  3. Refactor: Clean up the code (remove duplication, improve naming) while keeping tests green.
Example — building a URL shortener:
When TDD works well: Pure functions, business logic, utility functions, data transformations, algorithms.When TDD is harder: UI code, integration with external services, rapid prototyping where requirements are unclear.What interviewers are really testing: Do you practice TDD or just know the theory? Can you articulate when it helps and when it hurts?Red flag answer: “I always write tests after the code is done” (not TDD, just testing) or “TDD is too slow for real-world development” (shows rigidity — TDD is a tool, not a religion).Follow-up:
  • “Walk me through how you’d TDD an endpoint that creates a user, hashes their password, and sends a welcome email.”
  • “What’s the difference between TDD and BDD (Behavior Driven Development)?”
  • “When would you choose NOT to use TDD? What are its costs?”
Answer: Environment variables separate configuration from code — the same application binary behaves differently based on the environment it runs in.The dotenv pattern:
Best practices:
  1. Never commit secrets to git: Add .env to .gitignore. Use .env.example (with placeholder values) as a template.
  2. Validate env vars at startup: Fail fast if required variables are missing.
  1. Use typed/validated config (not raw process.env everywhere):
  1. In production, use secrets managers (not .env files): AWS Secrets Manager, HashiCorp Vault, Kubernetes Secrets. .env files are fine for development.
What interviewers are really testing: Do you validate env vars at startup? Do you use a secrets manager in production or just .env files?Red flag answer: “I hardcode the database password in my config file and just change it on each server.” This is a security incident waiting to happen.Follow-up:
  • “What’s the difference between .env, .env.local, .env.production? How do they layer?”
  • “How do you manage secrets in a Kubernetes deployment? What are Kubernetes Secrets and their limitations?”
  • “What happens if process.env.PORT is the string 'undefined'? How does your validation catch this?”
Answer: Production logging in Node requires structured, leveled, performant logging — not console.log.Why console.log is bad for production:
  • Synchronous when writing to a TTY (terminal) — blocks the event loop.
  • No log levels — can’t filter debug messages in production.
  • No structureconsole.log('User created', userId) is not machine-parseable.
  • No context — which request triggered this log? Which user?
Production logging stack:
Log levels (from most to least severe):
  • fatal: App is crashing, needs immediate attention.
  • error: Something failed but the app continues.
  • warn: Unexpected situation, but handled.
  • info: Normal business events (user created, payment processed).
  • debug: Development-time detail.
  • trace: Very verbose, rarely used in production.
Correlation IDs (essential for microservices):
Log pipeline: Application -> stdout -> Log collector (Fluentd/Vector/Filebeat) -> Log aggregator (ELK/Datadog/Grafana Loki) -> Dashboards/Alerts.What interviewers are really testing: Do you use structured logging? Do you know about correlation IDs? Can you explain why Pino is faster than Winston?Red flag answer: “I use console.log and redirect stdout to a file.” No structure, no levels, no correlation, and file I/O issues.Follow-up:
  • “Why is Pino faster than Winston? What architectural decision makes the difference?”
  • “How do you trace a single request across 5 microservices? What headers/IDs do you propagate?”
  • “You’re generating 10GB of logs per day. How do you manage log storage, rotation, and retention?“

6. Node.js Medium Level Questions

Answer: Middleware executes strictly in the order it is defined. This is not a detail — it is fundamental to how Express works, and getting it wrong causes subtle bugs.
Common ordering bugs:
  • Putting errorHandler before routes: errors thrown in routes never reach it.
  • Putting authMiddleware after routes: routes execute unauthenticated.
  • Putting express.json() after routes: req.body is undefined in handlers.
  • Putting cors() after routes: preflight OPTIONS requests get 404.
What interviewers are really testing: Can you reason about why order matters and diagnose ordering bugs?Red flag answer: “I just put middleware in whatever order and it works.” It works until it doesn’t, and the bugs are subtle.Follow-up:
  • “You add express.json() but req.body is always undefined. What went wrong?”
  • “Should compression() go before or after express.static()? Why?”
Answer:
Key gotcha: Both req.params and req.query values are always strings. req.params.id is "42", not 42. Always parse and validate.Security consideration: Query strings can be manipulated. Never trust req.query.isAdmin without server-side authorization.What interviewers are really testing: Do you know params are strings? Do you validate and sanitize?Follow-up:
  • “What is req.params vs req.query vs req.body? When do you use each?”
  • “How would you handle a route parameter that must be a valid MongoDB ObjectId?”
Answer:
What happens without body parsing: req.body is undefined. This is the number one “Express isn’t working” complaint from beginners.What interviewers are really testing: Do you always set size limits? Do you understand the different content types?Follow-up:
  • “What’s the difference between extended: true and extended: false? When does it matter?”
  • “How does body parsing relate to streams internally?”
Answer:
What interviewers are really testing: Do you distinguish between operational errors (expected, e.g., 404) and programmer errors (bugs)? Do you have a centralized error handler?Follow-up:
  • “What’s the difference between an operational error and a programmer error? How do you handle each differently?”
  • “In Express 5, do you still need the asyncHandler wrapper?”
Answer:
Dynamic origin is important because origin: '*' cannot be used with credentials: true. The function-based approach lets you whitelist multiple origins while still supporting credentials.What interviewers are really testing: Do you use dynamic origin checking in production?Follow-up:
  • “Why does the function receive origin: undefined for some requests? Which requests have no origin?”
  • “How do you debug CORS errors? Where do you look?”
Answer:
Important: In production with multiple servers, use a Redis store (see question 45 for details).What interviewers are really testing: Do you apply different limits to different endpoints? Do you know about skipSuccessfulRequests?Follow-up:
  • “How do you handle rate limiting for authenticated users vs anonymous users?”
  • “What’s the RateLimit-* header standard (RFC 6585) and how does the client use it?”
Answer: Never trust client input. Validate every field for type, format, range, and length.
What interviewers are really testing: Do you validate on the server even if the client validates? Do you sanitize in addition to validating?Red flag answer: “I validate on the frontend so the backend doesn’t need to.” Client-side validation is UX — server-side validation is security.Follow-up:
  • “What’s the difference between validation (is this valid?) and sanitization (make this safe)?”
  • “How does Zod compare to express-validator? When would you choose one over the other?”
Answer:
The client.release() trap: If you forget to release a client, it’s “leaked” — the pool thinks it’s still in use. After max leaks, all new queries hang forever waiting for a connection.What interviewers are really testing: Do you handle transactions correctly? Do you always release connections?Follow-up:
  • “What happens if you forget to call client.release() after checking out a client from the pool?”
  • “How do you monitor pool health to detect connection leaks?”
Answer:
What interviewers are really testing: Do you use schemas with validation? Do you know about pre/post hooks, select: false, and indexing?Follow-up:
  • “What’s the difference between Mongoose validation and MongoDB schema validation? When would you use each?”
  • “How do you handle the unique constraint in Mongoose — is it a validator or an index?”
Answer:
What interviewers are really testing: Do you separate authentication from authorization? Do you handle token expiry gracefully?Follow-up:
  • “How do you implement token refresh? What happens if the refresh token is stolen?”
  • “How would you revoke all tokens for a user who changes their password?”
Answer:
Why bcrypt specifically:
  • Adaptive cost: Increase SALT_ROUNDS as hardware gets faster (double cost every 18 months to match Moore’s Law).
  • Built-in salt: Each hash includes a random salt — identical passwords produce different hashes.
  • Intentionally slow: 12 rounds = ~300ms per hash. An attacker trying 10 billion passwords needs ~95 years. MD5/SHA would take minutes.
Alternatives: argon2 (winner of the Password Hashing Competition, recommended for new projects), scrypt (built into Node’s crypto module).What interviewers are really testing: Do you know why bcrypt is slow on purpose? Can you explain adaptive cost?Red flag answer: “I use SHA256 to hash passwords.” SHA is a fast hash designed for integrity checks, not password storage.Follow-up:
  • “Why is bcrypt better than SHA-256 for passwords? They’re both hash functions.”
  • “What salt rounds would you choose and how do you decide when to increase them?”
Answer:
What interviewers are really testing: Do you validate file types, set size limits, and generate random filenames?Follow-up:
  • “How would you stream uploads directly to S3 without saving to disk?”
  • “Why do you generate random filenames instead of using the original filename?”
Answer:
Production pattern: Never send emails synchronously in a request handler. Use a job queue:
What interviewers are really testing: Do you send emails asynchronously via a queue? Do you use connection pooling?Follow-up:
  • “Why should emails be sent via a queue instead of directly in the request handler?”
  • “How do you handle email delivery failures and retries?”
Answer:
What interviewers are really testing: Do you test both happy and error paths? Do you use beforeEach to reset state?Follow-up:
  • “How do you test a route that requires authentication? How do you mock the auth middleware?”
  • “What’s the difference between unit testing a service and integration testing an endpoint?”
Answer:
Mock vs Stub vs Spy:
  • Mock: Replace a function entirely, control its behavior and verify calls.
  • Stub: A mock with pre-programmed return values (focused on output, not call verification).
  • Spy: Wraps the real function, records calls but still executes the original.
What interviewers are really testing: Do you know when to use jest.mock vs jest.spyOn? Can you mock at the right granularity?Follow-up:
  • “When would you use jest.spyOn instead of jest.mock?”
  • “How do you mock a function that’s imported using ES module named exports?”
Answer:
Always provide .env.example:
What interviewers are really testing: Do you validate env vars at startup? Do you centralize config?Follow-up:
  • “How do you handle different configs for development, staging, and production?”
  • “What’s the security risk of logging process.env for debugging?”
Answer:
What interviewers are really testing: Do you know why Pino is faster? Do you redact sensitive fields?Follow-up:
  • “Why does Pino recommend writing to stdout instead of files?”
  • “How do you add request context (requestId, userId) to every log line in a request lifecycle?”
Answer:
Production consideration: Morgan writes to stdout synchronously. For high-throughput APIs (10k+ req/s), consider using Pino’s built-in HTTP logging instead (pino-http), which is asynchronous and integrates with request context.What interviewers are really testing: Do you know about piping Morgan output to a proper logger?Follow-up:
  • “What’s the performance difference between Morgan and pino-http?”
  • “How do you avoid logging sensitive data (passwords, tokens) in request/response logs?”
Answer:
Production reality: In production, you typically don’t serve static files from Node at all. Use a CDN (CloudFront, Cloudflare) or Nginx reverse proxy. Node’s event loop should be reserved for dynamic API responses, not static file I/O.What interviewers are really testing: Do you know that Node shouldn’t serve static files in production? Do you set proper cache headers?Follow-up:
  • “Why shouldn’t Node.js serve static files in production? What should serve them instead?”
  • “What is the immutable cache directive and when should you use it?“

7. Node.js Advanced Level Questions

Answer: Understanding custom streams shows deep knowledge of Node’s I/O model.
Key methods to implement:
  • Readable._read(size): Called when the consumer wants more data. Call this.push(data) or this.push(null) to end.
  • Writable._write(chunk, encoding, callback): Called for each chunk. Call callback() when done, callback(err) on error.
  • Transform._transform(chunk, encoding, callback): Process and push modified data. Call callback() when done.
  • Transform._flush(callback): Called when all input has been consumed. Emit any remaining buffered output.
What interviewers are really testing: Can you implement streams from scratch, not just use built-in ones?Red flag answer: Only knowing how to use fs.createReadStream without understanding how to build custom streams.Follow-up:
  • “What is the _flush method in a Transform stream and when would you use it?”
  • “How do you handle errors in a custom Readable stream? What happens if _read throws?”
  • “When would you use objectMode: true and what changes about the stream’s behavior?”
Answer: Transform streams are the workhorses of data pipelines — they sit between a Readable and Writable, modifying data as it flows through.
What interviewers are really testing: Can you handle partial data across chunks (the buffer pattern)? Do you implement _flush for remaining data?Follow-up:
  • “What happens if a chunk splits in the middle of a UTF-8 multi-byte character? How do you handle it?”
  • “How would you add parallel processing to a Transform stream (process multiple chunks concurrently)?”
Answer: When you cannot use .pipe() or pipeline(), you need to handle backpressure manually.
Why this matters: Without backpressure handling, a fast network read (100MB/s) writing to a slow disk (10MB/s) will buffer 90MB/s in memory. In 10 seconds, you’ve consumed 900MB of RAM. In a minute, you’re OOM.highWaterMark determines when backpressure kicks in:
What interviewers are really testing: Can you implement backpressure without .pipe()? Do you understand highWaterMark?Follow-up:
  • “What is the highWaterMark and how does choosing the wrong value affect performance?”
  • “How does pipeline() handle backpressure differently from .pipe()?”
Answer: Node provides three ways to create child processes, each with different characteristics.
When to use each:Security: exec runs in a shell, making it vulnerable to command injection if user input is interpolated:
What interviewers are really testing: Do you know the shell injection risk with exec? Can you choose the right method for a given scenario?Red flag answer: Using exec for everything, especially with user-provided input.Follow-up:
  • “Why is exec vulnerable to command injection while spawn is not?”
  • “How does fork’s IPC channel work? What serialization format does it use?”
  • “How would you implement a process pool for CPU-intensive tasks using fork?”
Answer:
Use WEB_CONCURRENCY env var instead of os.cpus().length for container environments where CPU count may not reflect the container’s allocation.What interviewers are really testing: Do you handle graceful shutdown? Do you handle the container CPU issue?Follow-up:
  • “How does the master distribute connections to workers? What load balancing algorithm is used?”
  • “What happens to WebSocket connections when a worker dies?”
Answer:
What interviewers are really testing: Can you design a worker pool? Do you understand the task queuing and worker lifecycle?Follow-up:
  • “What happens if a worker crashes? How do you make the pool resilient?”
  • “How would you add a timeout to tasks so a hung worker doesn’t block the pool forever?”
Answer:
Use in production: Wrap critical operations (DB queries, external API calls, template rendering) with performance marks. Export histograms to Prometheus/Datadog for P50/P95/P99 monitoring.What interviewers are really testing: Do you use built-in performance tools or only Date.now()? Do you think in percentiles?Follow-up:
  • “Why is P99 latency more important than average latency? What does it tell you?”
  • “How do you export performance metrics from Node to a monitoring system?”
Answer:
Analyzing snapshots in Chrome DevTools:
  1. Open chrome://inspect -> “Open dedicated DevTools for Node”
  2. Memory tab -> Load snapshot
  3. Key views:
    • Summary: Objects grouped by constructor. Look for unexpectedly large counts.
    • Comparison: Load two snapshots, see what grew between them. This is the most powerful memory leak detection tool.
    • Containment: Shows the retention tree — who is keeping an object alive.
    • Statistics: Pie chart of memory by type.
The “three snapshot technique” for finding leaks:
  1. Take snapshot after warmup (baseline).
  2. Perform the suspected leaky operation 5-10 times.
  3. Force GC (if --expose-gc is enabled) and take snapshot.
  4. Compare snapshots 1 and 3 — objects that grew are leak candidates.
What interviewers are really testing: Have you actually used heap snapshots to find a real memory leak? Can you describe the workflow?Follow-up:
  • “You have a production Node service that’s leaking memory. How do you take a heap snapshot safely without impacting users?”
  • “What is ‘retained size’ vs ‘shallow size’ in a heap snapshot?”
Answer:
Reading a flame graph:
  • X-axis: Not time! It’s sorted alphabetically. Width = percentage of total CPU time.
  • Y-axis: Call stack depth. Bottom = entry point, top = leaf functions.
  • Hot path: Follow the widest bars from bottom to top — that’s where most CPU time is spent.
  • Plateau: A wide bar at the top = a single function consuming lots of CPU (optimization target).
Common findings:
  • Wide JSON.parse bars: Parse large JSON on a worker thread.
  • Wide regex bars: Optimize or replace the regex.
  • Wide GC bars: Memory pressure — reduce allocations.
  • Wide _read or _write bars in streams: Stream processing bottleneck.
What interviewers are really testing: Can you interpret profiling output? Have you used these tools on a real performance issue?Follow-up:
  • “How does V8’s sampling profiler work? What’s the difference between sampling and instrumented profiling?”
  • “You see a flame graph where 40% of CPU time is in JSON.stringify. What do you do?”
Answer:
HTTP/2 advantages over HTTP/1.1:
  • Multiplexing: Multiple requests/responses over a single TCP connection (no head-of-line blocking at the HTTP level).
  • Header compression (HPACK): Reduces overhead for repeated headers.
  • Server Push: Proactively send resources before the client requests them.
  • Binary framing: More efficient than text-based HTTP/1.1.
In practice: Most Node apps don’t implement HTTP/2 directly. Instead, a reverse proxy (Nginx, Cloudflare) handles HTTP/2 with clients and uses HTTP/1.1 to communicate with Node. This is simpler and Nginx handles HTTP/2 more efficiently.What interviewers are really testing: Do you know HTTP/2’s multiplexing benefit? Do you understand the typical deployment pattern with a reverse proxy?Follow-up:
  • “What is HTTP/2 server push and why has it been controversial? Why did Chrome remove push support?”
  • “What is head-of-line blocking and how does HTTP/2 partially solve it? What about HTTP/3?”
Answer:
Production concerns: Heartbeat for dead connection detection, authentication on upgrade, message size limits, rate limiting per connection, and scaling across multiple servers via Redis pub/sub.What interviewers are really testing: Do you implement heartbeat? Do you handle connection cleanup?Follow-up:
  • “How do you authenticate a WebSocket connection? Can you use cookies?”
  • “What happens if a client sends messages faster than the server can process them? How do you handle backpressure on WebSocket?”
Answer:
What interviewers are really testing: Do you use DataLoader to solve N+1? Do you create per-request context?Follow-up:
  • “Why is a new DataLoader created for each request instead of sharing one globally?”
  • “What is query complexity analysis and how do you prevent abusive queries (deeply nested, huge)?”
Answer:
Key concepts: Durable queues, persistent messages, prefetch (concurrency control), acknowledgments, dead letter queues (DLQ) for failed messages.What interviewers are really testing: Do you handle message acknowledgment? Do you know about dead letter queues? Do you set prefetch limits?Follow-up:
  • “What happens if a consumer crashes while processing a message but before acknowledging it?”
  • “What is a dead letter queue and when would you use it?”
  • “How does RabbitMQ’s prefetch relate to consumer concurrency?”
Answer:
What interviewers are really testing: Do you know about cache stampede prevention? Can you use Redis beyond simple get/set?Follow-up:
  • “What is a cache stampede and how does your locking pattern prevent it?”
  • “Why use a Lua script for rate limiting instead of multiple Redis commands?”
  • “How does Redis Cluster differ from Redis Sentinel? When do you need each?”
Answer: Graceful shutdown ensures in-flight requests complete before the process exits. Without it, users get broken responses and database transactions get left uncommitted.
Why this matters in Kubernetes: When a pod is terminated, Kubernetes sends SIGTERM, then waits terminationGracePeriodSeconds (default 30s), then sends SIGKILL. Your app must handle SIGTERM to drain connections within that window.What interviewers are really testing: Do you drain connections? Do you close resources in the right order? Do you have a force-kill timeout?Red flag answer: “I just let the process die, it restarts quickly.” This drops in-flight requests and can corrupt data.Follow-up:
  • “In what order should you close resources during shutdown? Why?”
  • “What happens to WebSocket connections during graceful shutdown?”
  • “How does Kubernetes’s pod termination lifecycle interact with your graceful shutdown?”
Answer:
Liveness vs Readiness (Kubernetes terminology):
  • Liveness: “Is the process stuck?” Failure -> Kubernetes kills and restarts the pod.
  • Readiness: “Can it handle traffic?” Failure -> Kubernetes removes the pod from the Service load balancer (no traffic routed) but doesn’t kill it. Pod can recover and become ready again.
Anti-pattern: Making the liveness check depend on external services (database, Redis). If the database is down, killing and restarting your pod won’t fix it — now you have a restart loop on top of a database outage.What interviewers are really testing: Do you separate liveness from readiness? Do you avoid the “cascading restart” anti-pattern?Follow-up:
  • “Why should the liveness check NOT depend on the database? What happens if it does?”
  • “What is a startup probe in Kubernetes and when do you need it?”
Answer:
The keepAliveTimeout gotcha: If your load balancer (ALB, Nginx) has a 60s idle timeout but Node’s keepAliveTimeout is the default 5s, Node closes the connection before the load balancer expects it, causing 502 errors. Always set keepAliveTimeout > load_balancer_timeout.What interviewers are really testing: Do you set timeouts on both incoming and outgoing requests? Do you know about the keepAliveTimeout issue?Follow-up:
  • “What is the difference between server.timeout and server.keepAliveTimeout?”
  • “How do you handle a timeout when a database query is already in progress? Does the query get cancelled?”
Answer:
The three-snapshot workflow:
  1. Take snapshot at baseline (after warmup).
  2. Reproduce the suspected leak (run load test for 5 minutes).
  3. Take snapshot after forced GC.
  4. Compare: sort by “Objects allocated between Snapshot 1 and 2.” Look for unexpected growth in specific constructors.
Common findings:
  • (closure) growing: Event listeners or closures holding references.
  • (string) growing: Unbounded string concatenation or logging.
  • (array) growing: Cache without eviction.
  • Specific class names growing: Code-level leak in that class.
What interviewers are really testing: Do you have a systematic approach to leak detection, or do you just restart the process when memory gets high?Follow-up:
  • “Your production service’s heap grows from 200MB to 1.5GB over 6 hours, then OOMs. Walk me through your debugging approach.”
  • “How do you take a heap snapshot in production without impacting user requests?”
Answer: N-API (Node-API) provides a stable C ABI for building native addons that work across Node.js versions without recompilation.
When to write native addons:
  • Performance-critical code (100x+ speedup over JS for numerical work).
  • Interfacing with system libraries (libcurl, OpenCV, system calls).
  • Reusing existing C/C++ code.
Alternatives to C++: Use Rust via neon or napi-rs for memory-safe native addons. Increasingly popular in the Node ecosystem.What interviewers are really testing: Do you know when native addons are justified? Do you understand the async worker pattern?Follow-up:
  • “What is the difference between N-API and the older NAN (Native Abstractions for Node)?”
  • “Why can’t you access V8/JavaScript objects from within the Execute() method of an AsyncWorker?”
  • “When would you choose a Rust native addon over C++?”
Answer:
The circuit breaker pattern prevents cascading failures: when a downstream service is failing, stop sending requests to it (open the circuit). This prevents your service from hanging on timeouts and using up all its connections.Service mesh (Istio, Linkerd): In production Kubernetes environments, these concerns (circuit breaking, retries, timeouts, mTLS) are often handled at the infrastructure layer via a sidecar proxy, not in application code.What interviewers are really testing: Do you know about circuit breakers? Can you choose between synchronous and event-driven communication?Follow-up:
  • “What are the three states of a circuit breaker? How does the half-open state work?”
  • “When would you use an API gateway vs direct service-to-service communication?”
  • “How does a service mesh differ from implementing resilience patterns in application code?“

8. Gap-Filling Questions (Critical Topics)

Answer: AsyncLocalStorage (from async_hooks module) provides a way to store context that is automatically propagated through the entire async call chain of a request — without passing it explicitly through every function parameter.The problem it solves: In a microservice handling concurrent requests, you need to associate logs, metrics, and database queries with the specific request that triggered them. Without AsyncLocalStorage, you’d have to pass a requestId through every function call.
How it works internally: AsyncLocalStorage hooks into Node’s async resource tracking (async_hooks). When an async operation (Promise, setTimeout, I/O callback) is created within a .run() context, the context is automatically associated with that async operation and all its descendants.Performance: In Node 16+, AsyncLocalStorage has minimal overhead (~2-5% in benchmarks). Earlier versions had higher overhead due to the async_hooks implementation.What interviewers are really testing: Do you know how to propagate request context without prop-drilling? This is essential for observability in production services.Red flag answer: “I pass requestId as a parameter to every function.” This works but doesn’t scale and makes function signatures noisy.Follow-up:
  • “How does AsyncLocalStorage differ from thread-local storage in Java? What’s the Node equivalent?”
  • “What are async_hooks and what performance concerns do they have?”
  • “How would you use AsyncLocalStorage to implement distributed tracing across microservices?”
Answer: Supply chain attacks exploit the npm ecosystem — compromising a package that thousands of projects depend on. This is one of the most dangerous attack vectors for Node.js applications.Notable incidents:
  • event-stream (2018): Malicious code injected into a popular package (2M weekly downloads) that targeted a specific Bitcoin wallet. The attacker gained maintainer access through social engineering.
  • ua-parser-js (2021): Compromised package ran cryptominers on developer machines. 8M weekly downloads affected.
  • colors/faker (2022): Maintainer intentionally sabotaged their own packages in protest, breaking thousands of projects.
Defense layers:
  1. Lock files (package-lock.json / yarn.lock): Ensures reproducible installs. Always commit lock files.
  2. npm audit / Snyk / Socket.dev: Scan dependencies for known vulnerabilities.
  1. Lockfile-only installs in CI:
  1. Pin exact versions (debatable but safer):
  1. Review new dependencies:
    • Check npm download counts, GitHub stars, maintainer history.
    • Use npm pack to inspect what’s actually published (can differ from GitHub source).
    • Check for install scripts: "preinstall": "node malicious.js" is a red flag.
  2. Disable install scripts for untrusted packages:
  1. Use a private registry (Artifactory, Verdaccio) that proxies npm and caches approved packages.
What interviewers are really testing: Are you aware of supply chain risks specific to npm? Do you have a strategy beyond npm audit?Red flag answer: “I just run npm install and trust the packages.” This is how supply chain attacks succeed.Follow-up:
  • “How would you detect if a dependency update introduces malicious code?”
  • “What is the difference between npm install and npm ci? Why does it matter in CI/CD?”
  • “A critical vulnerability is found in a transitive dependency 4 levels deep. How do you fix it?”
Answer: Understanding how Node.js processes interact with the OS and container orchestrators is critical for production reliability.Process signals:
  • SIGTERM (15): Polite “please terminate.” Sent by Kubernetes, Docker, PM2. Your app should handle this for graceful shutdown.
  • SIGINT (2): Interrupt from terminal (Ctrl+C). Handle like SIGTERM.
  • SIGKILL (9): Forced kill. Cannot be caught or handled. The process dies immediately. Kubernetes sends this after terminationGracePeriodSeconds.
  • SIGHUP (1): Terminal hangup. Some apps use it to reload config.
  • SIGUSR1 (10): Node uses this to activate the debugger. Don’t override.
  • SIGUSR2 (12): User-defined. Good for triggering heap snapshots.
The PID 1 problem in Docker:
When Node runs as PID 1, it doesn’t have default signal handlers for SIGTERM/SIGINT — signals are ignored unless you explicitly process.on('SIGTERM', ...). Tini acts as a proper init process that forwards signals correctly.Container lifecycle in Kubernetes:
beforeExit vs exit events:
What interviewers are really testing: Do you know the PID 1 problem? Can you reason about the container termination lifecycle?Red flag answer: “I just deploy the Docker image and it works.” Missing signal handling means dropped requests and data loss on every deployment.Follow-up:
  • “What is the PID 1 problem in Docker and how does tini solve it?”
  • “Your Node app in Kubernetes restarts frequently with exit code 137. What does that mean?”
  • “How do you ensure zero-downtime deployments in Kubernetes with a Node.js app?”
Answer: AbortController provides a standard mechanism to cancel async operations — network requests, database queries, timers, or any long-running work.
What interviewers are really testing: Do you handle client disconnection? Do you use AbortController for timeouts instead of manual setTimeout + cleanup?Red flag answer: Not knowing AbortController exists, or using manual timeout patterns with potential cleanup issues.Follow-up:
  • “How do you propagate cancellation through a chain of async operations (e.g., HTTP handler -> service -> database)?”
  • “What happens to a database query that’s already executing when you abort the signal?”
  • “How does AbortSignal.any() simplify complex cancellation scenarios?”
Answer: diagnostics_channel (Node 16+) is a publish/subscribe API for diagnostic data within a Node.js application. It provides a low-overhead way to instrument code without modifying it.
Modern Node.js observability stack:
  1. Metrics: prom-client (Prometheus) or custom metrics -> Grafana dashboards.
  2. Tracing: OpenTelemetry SDK -> trace requests across services.
  3. Logging: Pino -> stdout -> log collector -> centralized search.
  4. Profiling: --inspect + Chrome DevTools for ad-hoc, clinic.js for automated.
OpenTelemetry integration (the emerging standard):
What interviewers are really testing: Do you think about observability as a first-class concern? Do you know OpenTelemetry? Can you set up distributed tracing?Red flag answer: “I check the logs when something goes wrong.” Reactive debugging without proactive observability means you only find problems after users report them.Follow-up:
  • “What is the difference between metrics, logs, and traces? When do you use each?”
  • “How does OpenTelemetry’s auto-instrumentation work? What does it instrument in a Node.js app?”
  • “What is context propagation in distributed tracing and why is it essential for microservices?”
Answer: In production, you need to actively monitor the event loop and tune the thread pool — these are the two most impactful performance levers.Event Loop Monitoring:
Libuv Thread Pool Tuning:
What uses the thread pool:
  • fs.* operations (all file system calls)
  • dns.lookup() (NOT dns.resolve() which uses c-ares and the network directly)
  • crypto.pbkdf2(), crypto.scrypt(), crypto.randomBytes()
  • zlib compression
  • Custom C++ addons using uv_queue_work
What does NOT use the thread pool (uses OS kernel async directly):
  • TCP/UDP sockets (net, http, https)
  • dns.resolve(), dns.resolveAny() (uses c-ares library)
  • Pipes, signals, child process I/O
Sizing heuristic: If you’re seeing event loop lag and your workload is I/O-heavy (lots of file operations, crypto), try doubling UV_THREADPOOL_SIZE. Monitor the impact. Too many threads waste memory and cause context-switching overhead.What interviewers are really testing: Can you distinguish between what uses the thread pool and what uses kernel async? Can you monitor event loop health?Red flag answer: “I just set UV_THREADPOOL_SIZE=1024 to be safe.” Too many threads wastes memory and increases context switching.Follow-up:
  • “Why does dns.lookup() use the thread pool but dns.resolve() does not?”
  • “You notice your Node app’s P99 latency increases when you add more concurrent fs.readFile calls. Why might that happen?”
  • “How does monitorEventLoopDelay differ from the simple setInterval lag detection approach?”
Answer: Understanding Node.js release cycles is important for production decision-making and interview discussions about technical leadership.Release schedule:
  • Even-numbered releases (18, 20, 22): Become LTS (Long-Term Support). Get 30 months of support (12 months Active LTS + 18 months Maintenance LTS).
  • Odd-numbered releases (19, 21, 23): Current release. Only supported for 6 months. Never becomes LTS. Meant for testing new features before the next LTS.
Version manager: Use nvm (Unix) or fnm (cross-platform, faster) to manage multiple Node versions:
Key features by version (interview-relevant):
  • Node 18 LTS: fetch API built-in, test runner (node:test), AbortSignal.timeout().
  • Node 20 LTS: Stable test runner, import.meta.resolve, permission model (--experimental-permission).
  • Node 22 LTS: require() for ESM modules (experimental), WebSocket client, glob and matchesGlob in fs.
Upgrade strategy for production:
  1. Run tests on new LTS version in CI before upgrading.
  2. Check for deprecated APIs (node --pending-deprecation app.js).
  3. Test native addon compatibility (C++ modules may need recompilation).
  4. Roll out to staging, monitor for 1-2 weeks, then production.
What interviewers are really testing: Do you make informed decisions about Node versions? Do you stay on LTS? Can you articulate what changed between major versions?Red flag answer: “I just use whatever version comes with the Docker image” or “I’m still on Node 14.” Running unsupported versions in production is a security risk.Follow-up:
  • “You’re starting a new project today. Which Node.js version do you choose and why?”
  • “What is the Node.js permission model (--experimental-permission) and what problem does it solve?”
  • “How do you handle Node.js upgrades in a monorepo with 20 services?”