Node.js Interview Questions (55+ Detailed Q&A)
1. Runtime & Core Architecture
1. What is Node.js? Is it a framework?
1. What is Node.js? Is it a framework?
- 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 (
epollon Linux,kqueueon macOS,IOCPon Windows). - C++ bindings: Bridge between JS and native OS APIs (crypto, zlib, http_parser).
- “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?”
2. Single Threaded but Scalable? How?
2. Single Threaded but Scalable? How?
- Event Loop (single thread): Orchestrates callbacks, runs your JS code, and dispatches I/O requests.
- 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. - 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, notdns.resolve), and some crypto operations.
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.readFilecalls? Does the 101st block?” - “When would you increase
UV_THREADPOOL_SIZEand what’s the cost of making it too large?” - “How does the Cluster module change this picture? What about Worker Threads?”
3. Event Loop Phases (Deep Dive)
3. Event Loop Phases (Deep Dive)
- Timers: Execute callbacks from
setTimeoutandsetIntervalwhose 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. - Pending Callbacks: Execute I/O callbacks deferred from the previous loop iteration (e.g., TCP errors like
ECONNREFUSED). - Idle/Prepare: Internal use only by Libuv. You cannot interact with this phase.
- 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).
- Check:
setImmediatecallbacks execute here. This is specifically designed to run callbacks after the poll phase completes. - Close Callbacks:
socket.on('close'), cleanup handlers.
process.nextTickqueue (higher priority — runs first)- Promise microtask queue (
.then,.catch,.finally)
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).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.nextTickinside aprocess.nextTickrecursively? How does that differ from recursivesetImmediate?” - “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?”
4. `process.nextTick` vs `setImmediate`
4. `process.nextTick` vs `setImmediate`
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
nextTickhas higher priority than Promises. - Starvation risk: Recursive
nextTickwill 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
setImmediateallows 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.
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 toprocess.nextTick? Which runs first?” - “In a real application, give me a concrete example where using
nextTickinstead ofsetImmediatecaused a bug.”
5. V8 Engine Memory Limit
5. V8 Engine Memory Limit
- 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:
Bufferallocations live outside the V8 heap. A process can use 500MB of Buffers while reporting only 100MB heap usage.process.memoryUsage()shows bothheapUsedandexternal(C++ objects including Buffers).
- “What’s the difference between
heapUsed,heapTotal,rss, andexternalinprocess.memoryUsage()?” - “How would you size the
--max-old-space-sizeflag 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?”
6. Garbage Collection in Node
6. Garbage Collection in Node
- 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.
- 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.
- 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.
- Incremental Marking: V8 breaks the marking phase into small chunks interleaved with JS execution (increments of ~5ms) to reduce max pause time.
- Concurrent Sweeping/Compaction: V8 runs sweeping and some compaction on background threads while JS continues executing.
- “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?”
7. Global Objects
7. Global Objects
global object):Truly global (on the global object):globalitself (equivalent towindowin browsers, orglobalThisin ES2020+)process— the current Node process (PID, env vars, stdin/stdout, exit codes, memory usage)Buffer— binary data handlingconsole— loggingsetTimeout,setInterval,setImmediate,queueMicrotask
global):__dirname— absolute path of the directory containing the current file__filename— absolute path of the current filerequire— the module loader functionmodule— reference to the current moduleexports— shorthand formodule.exports
__dirname is not available in ESM modules — use import.meta.url with fileURLToPath instead.ESM equivalents:__dirname as a global object without knowing it’s module-scoped, or not knowing the ESM equivalents.Follow-up:- “Why is
__dirnamenot available in ES modules? How do you get the equivalent?” - “What is
globalThisand why was it introduced?” - “If you set
global.myVar = 42in one module, can another module access it? What are the dangers of this?”
8. CommonJS vs ESM
8. CommonJS vs ESM
require()/module.exports- Synchronous loading —
requireblocks until the module is loaded and evaluated. - Dynamic — you can
require()inside anifblock 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 getundefinedfor properties that haven’t been assigned yet. - Default in Node (
.jsfiles without"type": "module"inpackage.json).
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"inpackage.jsonor.mjsextension.
- CJS can
require()CJS. ESM canimportESM. - ESM can
importCJS (themodule.exportsobject becomes the default export). - CJS cannot
require()ESM directly (must use dynamicimport()which returns a Promise). __dirname,require.cache,moduleare not available in ESM.
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?”
9. `exports` vs `module.exports`
9. `exports` vs `module.exports`
exports is just a convenience reference that initially points to the same object as module.exports.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: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 }vsexports.a = 1?” - “In ESM, does this same confusion exist? Why or why not?”
10. Handling CPU Intensive Tasks
10. Handling CPU Intensive Tasks
-
Worker Threads (
worker_threadsmodule):- 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.
- True parallel JS execution with shared memory (
-
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.
-
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).
-
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.
await cpuIntensiveFunction() still blocks the event loop if the function is synchronous.Follow-up:- “What’s the difference between
worker_threadsandchild_process.forkin 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
11. What is a Buffer?
11. What is a Buffer?
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.allocUnsafeoverBuffer.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
BufferandUint8Array? Can you use a Buffer anywhere you’d use a typed array?”
12. Streams: 4 Types
12. Streams: 4 Types
-
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). -
Writable: Consumes data. Examples:
fs.createWriteStream,http.ServerResponse,process.stdout. Returnsfalsefrom.write()when its internal buffer is full (backpressure signal). -
Duplex: Both Readable and Writable, with independent buffers. Examples: TCP sockets (
net.Socket), WebSocket connections. The read and write sides operate independently. -
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.
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 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?”
13. Pipe & Backpressure
13. Pipe & Backpressure
- Readable stream pushes data to the Writable stream via
.write(). .write()returnsfalsewhen the Writable’s internal buffer exceedshighWaterMark(default 16KB for Buffer streams, 16 objects for object mode).- The Readable must stop reading (
.pause()in flowing mode, stop calling.push()in custom streams). - When the Writable’s buffer drains, it emits a
'drain'event. - The Readable resumes.
.pipe() handles this automatically:.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:.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
falsereturn value from.write()and keep writing?” - “How would you implement custom backpressure in a Transform stream?”
- “What is
highWaterMarkand how do you decide what value to set?”
14. `fs.readFile` vs `fs.createReadStream`
14. `fs.readFile` vs `fs.createReadStream`
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.
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
highWaterMarkoption oncreateReadStreamand when would you tune it?” - “How does
fs.promises.readFilediffer fromfs.readFileSyncin terms of event loop impact?”
15. Event Emitter Pattern
15. Event Emitter Pattern
.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
.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).- “If
emitis synchronous, what happens if one listener throws an error? Do other listeners still run?” - “You see
MaxListenersExceededWarningin your logs. What’s your debugging approach?” - “When would you use EventEmitter vs a message queue like Redis Pub/Sub?”
16. Handling uncaught exceptions
16. Handling uncaught exceptions
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()ortry/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.
- Wrap all async route handlers with error-catching middleware.
- Use
domain(deprecated) or AsyncLocalStorage for request-scoped error context. - Set
process.on('uncaughtException')andprocess.on('unhandledRejection')as the last safety net — log and exit. - Run behind PM2/Kubernetes that auto-restarts crashed processes.
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, doesuncaughtExceptionfire?” - “What changed in Node 15 regarding unhandled Promise rejections?”
17. Synchronous vs Asynchronous API
17. Synchronous vs Asynchronous API
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.
- 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.
- Inside any request handler, middleware, or event callback.
- Inside any
setIntervalor recurring timer. - Anywhere after
server.listen()is called.
require()is synchronous (file read + compile). Never callrequire()inside a hot path.JSON.parse()on large strings (100MB+ JSON) is CPU-bound and blocks the loop.crypto.pbkdf2Sync— use the async versioncrypto.pbkdf2instead.zlib.gzipSync— use streamingzlib.createGzip()instead.
- “Is
require()synchronous or asynchronous? What are the implications of callingrequireinside a request handler?” - “If
JSON.parseis 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?”
18. Zlib and Compression
18. Zlib and Compression
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):- 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.
- “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?”
19. REPL
19. REPL
- 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:
- “How would you expose a REPL in a running production service for emergency debugging? What are the security implications?”
20. OS Module
20. OS Module
os module provides operating system-related utility methods. Critical for Cluster setup, monitoring, and capacity planning.- Cluster worker count:
const workers = os.cpus().length(common pattern, but in containersos.cpus().lengthmay report the host’s CPUs, not the container’s limit — useprocess.env.NUM_WORKERSor 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.
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().lengthreturns 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)
21. Anatomy of HTTP Server
21. Anatomy of HTTP Server
http module provides a low-level HTTP server. Understanding it deeply is essential before using Express.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:- Libuv receives a TCP connection.
http_parser(C library, nowllhttpsince Node 12) parses the raw bytes into HTTP headers.- Node creates
IncomingMessageandServerResponseobjects. - Your callback fires with these objects.
- Response is written back through the socket.
'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(nowllhttp) work, and why was it rewritten?” - “What’s the difference between
res.end()andres.write()followed byres.end()?”
22. Middleware (Express)
22. Middleware (Express)
(req, res, next) that forms a chain of responsibility pattern.Types of middleware:- Application-level:
app.use(fn)— runs on every request. - Route-level:
router.use(fn)orapp.get('/path', fn, handler)— scoped to routes. - Error-handling:
(err, req, res, next)— 4-parameter signature, Express detects the arity. - Built-in:
express.json(),express.static(),express.urlencoded(). - Third-party:
cors,helmet,morgan,compression.
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.
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?”
23. Error Handling Middleware
23. Error Handling Middleware
(err, req, res, next). Express uses the function’s .length property (arity) to detect it.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)?”
24. Body Parsing
24. Body Parsing
req.body.Native approach (no Express):- Always set
limit: Without a limit, an attacker can send a 1GB JSON body and OOM your server. Default is'100kb'. extended: truevsfalse:extended: trueuses theqslibrary which supports nested objects (user[name]=foo).extended: falseusesquerystringwhich 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/jsonbut sending garbage will causeJSON.parseto throw. The Expressjson()middleware handles this and returns 400.
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 accessreq.body?” - “How would you handle a request that sends
Content-Type: application/jsonbut the body is invalid JSON?” - “Why is setting a body size limit a security measure? What specific attack does it prevent?”
25. CORS in Node
25. CORS in Node
- Simple requests (GET, POST with form content types): Browser adds
Originheader, server responds withAccess-Control-Allow-Origin. If it doesn’t match, browser blocks the response (the request still hits your server!). - Preflight requests (PUT, DELETE, custom headers, JSON content type): Browser sends an
OPTIONSrequest first withAccess-Control-Request-MethodandAccess-Control-Request-Headers. Server must respond with allowed methods/headers. Only then does the actual request fire.
Access-Control-Allow-Origin: Which origins can access (*or specific origin)Access-Control-Allow-Methods: Allowed HTTP methodsAccess-Control-Allow-Headers: Allowed custom headersAccess-Control-Allow-Credentials: Whether cookies/auth are allowed (true/omit)Access-Control-Max-Age: How long to cache preflight results (seconds)
Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true are mutually exclusive. If you need cookies/auth, you must specify the exact origin.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: '*'withcredentials: true? What would happen if you could?” - “If CORS is browser-only, how do you protect your API from unauthorized server-to-server access?”
26. JWT (JSON Web Token)
26. JWT (JSON Web Token)
- Header: Algorithm and token type —
{ "alg": "HS256", "typ": "JWT" } - Payload: Claims (data) —
{ "sub": "user123", "role": "admin", "exp": 1700000000 } - Signature:
HMACSHA256(base64(header) + "." + base64(payload), secret)— proves the token hasn’t been tampered with.
- 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):
- HttpOnly + Secure + SameSite=Strict cookie (best — immune to XSS)
- In-memory variable (lost on page refresh, but very secure)
- 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).
- 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.
- “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?”
27. Cookies vs Sessions
27. Cookies vs Sessions
28. File Uploads
28. File Uploads
multipart/form-data encoding — the body contains multiple parts separated by a boundary string. You need specialized parsing.Common approaches:- Multer (Express middleware): The standard choice. Handles
multipart/form-dataparsing, stores files to disk or memory. - Busboy/Busbody (low-level stream parser): Direct stream access, more control, works with any Node HTTP server.
- Formidable: Alternative to Multer with streaming support.
- 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).
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?”
29. WebSockets (`socket.io`)
29. WebSockets (`socket.io`)
- Client sends an HTTP request with
Upgrade: websocketheader. - Server responds with
101 Switching Protocols. - The TCP connection is now a WebSocket — no more HTTP framing.
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.
- 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).
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?”
30. Keep-Alive Agent
30. Keep-Alive Agent
- 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.
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 is ephemeral port exhaustion and how does keep-alive prevent it?”
- “You see
ECONNRESETerrors 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
31. Cluster Module
31. Cluster Module
- Master process calls
cluster.fork()to create worker processes. - Workers are full Node.js instances with their own V8 and event loop.
- The master accepts incoming connections and distributes them to workers.
- Load balancing: Round-robin (default on all platforms except Windows) or OS-level (let the kernel decide).
- 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.
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?”
32. PM2 Process Manager
32. PM2 Process Manager
- 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 monitshows real-time CPU, memory, event loop lag per process. - Ecosystem file:
ecosystem.config.jsfor declarative configuration.
- “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?”
33. Worker Threads
33. 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:| Aspect | Cluster | Worker Threads |
|---|---|---|
| Isolation | Separate processes, separate memory | Same process, can share memory |
| Overhead | ~30MB per worker (full V8 instance) | ~5-10MB per worker (V8 isolate) |
| Communication | IPC (serialized messages) | postMessage (structured clone) or SharedArrayBuffer (zero-copy) |
| Use case | Scaling HTTP servers | CPU-intensive tasks |
| Crash impact | Worker crash doesn’t affect others | Worker crash can affect the process |
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
- I/O-bound work (that’s what the event loop is for)
- Simple request handling (overhead of creating/managing threads outweighs benefit)
- “How does
postMessageserialize data between threads? What types can’t be transferred?” - “What is a
Transferableobject and when would you useworker.postMessage(data, [transferList])?” - “You need to process 10,000 images. Do you create 10,000 worker threads? What’s your architecture?”
34. Memory Leaks Causes
34. Memory Leaks Causes
- Event Listener Accumulation (most common):
.once().- Closures Holding Large Objects:
- Global Variables / Module-Level Caches Without TTL:
lru-cache package) or Redis with TTL.- Unreferenced Timers:
- Forgotten Streams: Readable streams that are created but never consumed or destroyed hold their internal buffer in memory.
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 heapprofilerfor automated analysisnode --heapsnapshot-signal=SIGUSR2— take a heap snapshot on demand
- “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/WeakMaphelp prevent certain kinds of memory leaks?”
35. N+1 Problem (GraphQL/ORM)
35. N+1 Problem (GraphQL/ORM)
- Eager Loading (ORM-level): Load related data in a JOIN or second query.
- DataLoader (GraphQL): Batches individual lookups within a single event loop tick into one query.
- Database Views / Denormalization: For read-heavy paths, pre-compute the joined data.
.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.”
36. Database Connection Pooling
36. Database Connection Pooling
- At startup, the pool opens
minconnections. - When your code needs a connection, it borrows one from the pool.
- After the query completes, the connection is returned to the pool (not closed).
- If all connections are in use, new requests wait in a queue until one is returned.
- If the queue exceeds a timeout, the request fails with a connection timeout error.
- 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.
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)?”
37. Caching Strategies
37. Caching Strategies
-
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.
-
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.
- 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 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?”
38. Profiling Node App
38. Profiling Node App
- Event loop lag (P50, P99)
- Heap used / heap total
- GC pause duration and frequency
- Active handles and requests (
process._getActiveHandles().length)
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?”
39. Event Loop Blocking Pitfalls
39. Event Loop Blocking Pitfalls
fs.readFileSync calls, several common operations can block the event loop and cause latency spikes.Common blockers people miss:JSON.parse/JSON.stringifyon large objects: These are synchronous and CPU-bound. A 50MB JSON string can block the loop for 200-500ms.
- Regex Denial of Service (ReDoS): Certain regex patterns with backtracking can take exponential time on crafted inputs.
-
Sorting large arrays:
Array.sort()on 1M+ elements blocks for 100ms+. -
Crypto operations:
crypto.pbkdf2Sync,crypto.scryptSync— always use async versions. - Template rendering: Rendering complex templates (EJS, Handlebars) with large datasets.
-
require()in hot paths: Module loading reads files synchronously and compiles them.
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?”
40. Microservices Communication
40. Microservices Communication
- 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
.protofiles. Latency: ~1-5ms per hop. Use for: high-throughput internal communication, streaming data, polyglot environments.
- 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.
| Factor | REST | gRPC | Message Queue | Kafka |
|---|---|---|---|---|
| Latency | Medium | Low | Variable | Low-Medium |
| Coupling | Tight | Tight | Loose | Loose |
| Reliability | Retry needed | Retry needed | Built-in | Built-in |
| Debugging | Easy (curl) | Harder | Harder | Harder |
| Streaming | No (workarounds) | Native | No | Native |
- “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
41. SQL Injection & NoSQL Injection
41. SQL Injection & NoSQL Injection
- Parameterized queries (primary defense — prevents the attack entirely)
- Input validation (reject unexpected types/formats before they reach the query)
- Least privilege (DB user should have minimal permissions — no
DROP TABLE) - ORM/ODM (Sequelize, Prisma, Mongoose — they parameterize by default, but raw queries still need care)
- WAF rules (Web Application Firewall as an additional layer)
- “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?”
42. XSS (Cross Site Scripting)
42. XSS (Cross Site Scripting)
- 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. - Reflected XSS: Malicious script is in the URL and reflected in the response:
https://site.com/search?q=<script>alert(1)</script>. - DOM-based XSS: Client-side JS inserts untrusted data into the DOM without sanitization:
document.innerHTML = userInput.
- Output encoding/escaping: Use a templating engine that auto-escapes (EJS with
<%= %>, Handlebars, React’s JSX). Never use<%- %>(raw output) with user data. - Content Security Policy (CSP) header: Restricts which scripts can execute.
- HttpOnly cookies: Prevents JavaScript from accessing cookies (
document.cookiereturns nothing). - Input sanitization (secondary defense): Libraries like
DOMPurify(client-side) orsanitize-html(server-side) for when you must accept HTML input (rich text editors).
- “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?”
43. CSRF (Cross Site Request Forgery)
43. CSRF (Cross Site Request Forgery)
- User is logged into
bank.com(session cookie is set). - User visits
evil-site.comwhich has:<img src="https://bank.com/transfer?to=attacker&amount=10000">. - Browser sends the request to
bank.comWITH the user’s session cookie. bank.comprocesses the transfer because the cookie is valid.
- SameSite Cookie Attribute (strongest, simplest):
- CSRF Tokens (traditional approach):
-
Double Submit Cookie: Send CSRF token in both a cookie and a request header. Server checks they match. Works because
evil-site.comcan trigger sending cookies but cannot read them or set custom headers on cross-origin requests. -
Origin/Referer Header Validation: Check that the
OriginorRefererheader matches your domain. Less reliable (can be stripped by proxies).
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 chooseLaxinstead?” - “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?”
44. Helmet.js
44. Helmet.js
| Header | Purpose | Default |
|---|---|---|
Content-Security-Policy | Prevents XSS by restricting resource sources | Strict default |
Strict-Transport-Security (HSTS) | Forces HTTPS for future requests | max-age=15552000 |
X-Content-Type-Options | Prevents MIME type sniffing | nosniff |
X-Frame-Options | Prevents clickjacking via iframes | SAMEORIGIN |
X-XSS-Protection | Legacy XSS filter (modern browsers use CSP) | 0 (disabled — can cause issues) |
Referrer-Policy | Controls what’s sent in the Referer header | no-referrer |
X-DNS-Prefetch-Control | Disables DNS prefetching (privacy) | off |
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-Optionsprevent it?” - “If you’re using a CDN for your JavaScript, how do you configure CSP to allow it while still preventing XSS?”
45. Rate Limiting
45. Rate Limiting
- 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.
-
Sliding Window: Calculates rate based on a sliding window. Smoother than fixed window.
express-rate-limituses this. - 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.
- Leaky Bucket: Requests enter a queue (bucket) and are processed at a fixed rate. Excess requests overflow (rejected). Smooths traffic to a constant rate.
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?”
46. Unit Testing (Jest/Mocha)
46. Unit Testing (Jest/Mocha)
- Unit tests (70%): Individual functions, service methods, utility functions. Fast, no I/O.
- Integration tests (20%): Multiple modules together, often hitting a real test database.
- End-to-end tests (10%): Full HTTP request lifecycle with
supertest.
- 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 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’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.”
47. Dependency Injection
47. Dependency Injection
- Constructor injection (classes):
new Service(db, cache, logger). Most explicit. - Factory functions:
createService({ db, cache }). Flexible, functional style. - DI containers (InversifyJS, tsyringe, awilix): Automatic resolution of dependencies. More magic but handles complex graphs.
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?”
48. TDD (Test Driven Development)
48. TDD (Test Driven Development)
- Red: Write a test for the next piece of functionality. Run it — it fails (because the code doesn’t exist yet).
- Green: Write the minimum code to make the test pass. No more, no less.
- Refactor: Clean up the code (remove duplication, improve naming) while keeping tests green.
- “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?”
49. Environment Variables
49. Environment Variables
dotenv pattern:- Never commit secrets to git: Add
.envto.gitignore. Use.env.example(with placeholder values) as a template. - Validate env vars at startup: Fail fast if required variables are missing.
- Use typed/validated config (not raw
process.enveverywhere):
- In production, use secrets managers (not
.envfiles): AWS Secrets Manager, HashiCorp Vault, Kubernetes Secrets..envfiles are fine for development.
.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.PORTis the string'undefined'? How does your validation catch this?”
50. Logging Best Practices
50. Logging Best Practices
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 structure —
console.log('User created', userId)is not machine-parseable. - No context — which request triggered this log? Which user?
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.
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
51. Express Middleware Order
51. Express Middleware Order
- Putting
errorHandlerbefore routes: errors thrown in routes never reach it. - Putting
authMiddlewareafter routes: routes execute unauthenticated. - Putting
express.json()after routes:req.bodyisundefinedin handlers. - Putting
cors()after routes: preflight OPTIONS requests get 404.
- “You add
express.json()butreq.bodyis alwaysundefined. What went wrong?” - “Should
compression()go before or afterexpress.static()? Why?”
52. Route Parameters and Query Strings
52. Route Parameters and Query Strings
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.paramsvsreq.queryvsreq.body? When do you use each?” - “How would you handle a route parameter that must be a valid MongoDB ObjectId?”
53. Body Parsing (Code Example)
53. Body Parsing (Code Example)
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: trueandextended: false? When does it matter?” - “How does body parsing relate to streams internally?”
54. Cookie and Session Management
54. Cookie and Session Management
55. Error Handling in Express (Comprehensive)
55. Error Handling in Express (Comprehensive)
- “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
asyncHandlerwrapper?”
56. CORS Configuration (Production)
56. CORS Configuration (Production)
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: undefinedfor some requests? Which requests have no origin?” - “How do you debug CORS errors? Where do you look?”
57. Rate Limiting (Code Example)
57. Rate Limiting (Code Example)
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?”
58. Input Validation
58. Input Validation
- “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?”
59. Database Connection Pooling (Code)
59. Database Connection Pooling (Code)
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?”
60. MongoDB with Mongoose
60. MongoDB with Mongoose
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
uniqueconstraint in Mongoose — is it a validator or an index?”
61. JWT Authentication (Production)
61. JWT Authentication (Production)
- “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?”
62. Password Hashing with bcrypt
62. Password Hashing with bcrypt
- Adaptive cost: Increase
SALT_ROUNDSas 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.
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?”
63. File Uploads with Multer (Production)
63. File Uploads with Multer (Production)
- “How would you stream uploads directly to S3 without saving to disk?”
- “Why do you generate random filenames instead of using the original filename?”
64. Email Sending with Nodemailer
64. Email Sending with Nodemailer
- “Why should emails be sent via a queue instead of directly in the request handler?”
- “How do you handle email delivery failures and retries?”
65. Testing with Jest (Patterns)
65. Testing with Jest (Patterns)
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?”
66. Mocking with Jest
66. Mocking with Jest
- 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.
jest.mock vs jest.spyOn? Can you mock at the right granularity?Follow-up:- “When would you use
jest.spyOninstead ofjest.mock?” - “How do you mock a function that’s imported using ES module named exports?”
67. Environment Variables (Code)
67. Environment Variables (Code)
.env.example:- “How do you handle different configs for development, staging, and production?”
- “What’s the security risk of logging
process.envfor debugging?”
68. Logging with Winston / Pino
68. Logging with Winston / Pino
- “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?”
69. HTTP Request Logging (Morgan)
69. HTTP Request Logging (Morgan)
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?”
70. Static File Serving
70. Static File Serving
- “Why shouldn’t Node.js serve static files in production? What should serve them instead?”
- “What is the
immutablecache directive and when should you use it?“
7. Node.js Advanced Level Questions
71. Custom Stream Implementation
71. Custom Stream Implementation
Readable._read(size): Called when the consumer wants more data. Callthis.push(data)orthis.push(null)to end.Writable._write(chunk, encoding, callback): Called for each chunk. Callcallback()when done,callback(err)on error.Transform._transform(chunk, encoding, callback): Process and push modified data. Callcallback()when done.Transform._flush(callback): Called when all input has been consumed. Emit any remaining buffered output.
fs.createReadStream without understanding how to build custom streams.Follow-up:- “What is the
_flushmethod in a Transform stream and when would you use it?” - “How do you handle errors in a custom Readable stream? What happens if
_readthrows?” - “When would you use
objectMode: trueand what changes about the stream’s behavior?”
72. Transform Streams (Practical)
72. Transform Streams (Practical)
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)?”
73. Backpressure Handling (Manual)
73. Backpressure Handling (Manual)
.pipe() or pipeline(), you need to handle backpressure manually.highWaterMark determines when backpressure kicks in:.pipe()? Do you understand highWaterMark?Follow-up:- “What is the
highWaterMarkand how does choosing the wrong value affect performance?” - “How does
pipeline()handle backpressure differently from.pipe()?”
74. Child Processes (spawn vs exec vs fork)
74. Child Processes (spawn vs exec vs fork)
| Method | Shell | Output | IPC | Use Case |
|---|---|---|---|---|
spawn | No | Stream | No | Long-running processes, large output (ffmpeg, log tailing) |
exec | Yes | Buffer | No | Short commands, small output (git status, shell scripts) |
fork | No | Stream | Yes (built-in) | Node.js worker processes that communicate with parent |
exec runs in a shell, making it vulnerable to command injection if user input is interpolated: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
execvulnerable to command injection whilespawnis 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?”
75. Cluster Module (Production Pattern)
75. Cluster Module (Production Pattern)
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?”
76. Worker Threads (Practical Pattern)
76. Worker Threads (Practical Pattern)
- “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?”
77. Performance Hooks
77. Performance Hooks
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?”
78. Heap Snapshots & Memory Analysis
78. Heap Snapshots & Memory Analysis
- Open
chrome://inspect-> “Open dedicated DevTools for Node” - Memory tab -> Load snapshot
- 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.
- Take snapshot after warmup (baseline).
- Perform the suspected leaky operation 5-10 times.
- Force GC (if
--expose-gcis enabled) and take snapshot. - Compare snapshots 1 and 3 — objects that grew are leak candidates.
- “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?”
79. CPU Profiling (Deep Dive)
79. CPU Profiling (Deep Dive)
- 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).
- Wide
JSON.parsebars: Parse large JSON on a worker thread. - Wide regex bars: Optimize or replace the regex.
- Wide GC bars: Memory pressure — reduce allocations.
- Wide
_reador_writebars in streams: Stream processing bottleneck.
- “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?”
80. HTTP/2 Server
80. HTTP/2 Server
- 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.
- “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?”
81. WebSocket Server (ws library)
81. WebSocket Server (ws library)
- “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?”
82. GraphQL Server
82. GraphQL Server
- “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)?”
83. Message Queues with RabbitMQ
83. Message Queues with RabbitMQ
- “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?”
84. Redis Caching (Advanced Patterns)
84. Redis Caching (Advanced Patterns)
- “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?”
85. Graceful Shutdown
85. Graceful Shutdown
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?”
86. Health Checks (Production Pattern)
86. Health Checks (Production Pattern)
- 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.
- “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?”
87. Request Timeout Handling
87. Request Timeout Handling
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.timeoutandserver.keepAliveTimeout?” - “How do you handle a timeout when a database query is already in progress? Does the query get cancelled?”
88. Memory Leak Detection (Production Workflow)
88. Memory Leak Detection (Production Workflow)
- Take snapshot at baseline (after warmup).
- Reproduce the suspected leak (run load test for 5 minutes).
- Take snapshot after forced GC.
- Compare: sort by “Objects allocated between Snapshot 1 and 2.” Look for unexpected growth in specific constructors.
(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.
- “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?”
89. Native Addons with N-API
89. Native Addons with N-API
- Performance-critical code (100x+ speedup over JS for numerical work).
- Interfacing with system libraries (libcurl, OpenCV, system calls).
- Reusing existing C/C++ code.
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++?”
90. Microservices Communication (Service-to-Service)
90. Microservices Communication (Service-to-Service)
- “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)
91. AsyncLocalStorage (Request Context Propagation)
91. AsyncLocalStorage (Request Context Propagation)
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.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
AsyncLocalStoragediffer from thread-local storage in Java? What’s the Node equivalent?” - “What are
async_hooksand what performance concerns do they have?” - “How would you use
AsyncLocalStorageto implement distributed tracing across microservices?”
92. Node.js Security: Dependency Supply Chain Attacks
92. Node.js Security: Dependency Supply Chain Attacks
- 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.
-
Lock files (
package-lock.json/yarn.lock): Ensures reproducible installs. Always commit lock files. -
npm audit/ Snyk / Socket.dev: Scan dependencies for known vulnerabilities.
- Lockfile-only installs in CI:
- Pin exact versions (debatable but safer):
-
Review new dependencies:
- Check npm download counts, GitHub stars, maintainer history.
- Use
npm packto inspect what’s actually published (can differ from GitHub source). - Check for install scripts:
"preinstall": "node malicious.js"is a red flag.
- Disable install scripts for untrusted packages:
- Use a private registry (Artifactory, Verdaccio) that proxies npm and caches approved packages.
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 installandnpm 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?”
93. Node.js Signals, Process Lifecycle, and Container Integration
93. Node.js Signals, Process Lifecycle, and Container Integration
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 afterterminationGracePeriodSeconds.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.
process.on('SIGTERM', ...). Tini acts as a proper init process that forwards signals correctly.Container lifecycle in Kubernetes:beforeExit vs exit events:- “What is the PID 1 problem in Docker and how does
tinisolve 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?”
94. `AbortController` and Cancellation Patterns
94. `AbortController` and Cancellation Patterns
AbortController provides a standard mechanism to cancel async operations — network requests, database queries, timers, or any long-running work.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?”
95. Node.js Diagnostic Channels and Modern Observability
95. Node.js Diagnostic Channels and Modern Observability
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.- Metrics:
prom-client(Prometheus) or custom metrics -> Grafana dashboards. - Tracing: OpenTelemetry SDK -> trace requests across services.
- Logging: Pino -> stdout -> log collector -> centralized search.
- Profiling:
--inspect+ Chrome DevTools for ad-hoc,clinic.jsfor automated.
- “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?”
96. Event Loop Monitoring and `libuv` Thread Pool Tuning
96. Event Loop Monitoring and `libuv` Thread Pool Tuning
fs.*operations (all file system calls)dns.lookup()(NOTdns.resolve()which uses c-ares and the network directly)crypto.pbkdf2(),crypto.scrypt(),crypto.randomBytes()zlibcompression- Custom C++ addons using
uv_queue_work
- TCP/UDP sockets (
net,http,https) dns.resolve(),dns.resolveAny()(uses c-ares library)- Pipes, signals, child process I/O
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 butdns.resolve()does not?” - “You notice your Node app’s P99 latency increases when you add more concurrent
fs.readFilecalls. Why might that happen?” - “How does
monitorEventLoopDelaydiffer from the simplesetIntervallag detection approach?”
97. Node.js Version Management and LTS Strategy
97. Node.js Version Management and LTS Strategy
- 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.
nvm (Unix) or fnm (cross-platform, faster) to manage multiple Node versions:- Node 18 LTS:
fetchAPI 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,globandmatchesGlobinfs.
- Run tests on new LTS version in CI before upgrading.
- Check for deprecated APIs (
node --pending-deprecation app.js). - Test native addon compatibility (C++ modules may need recompilation).
- Roll out to staging, monitor for 1-2 weeks, then production.
- “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?”