Node.js Backend Developer Top 100 Interview Questions
100 Node.js backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers the event loop, async patterns, streams, HTTP servers, databases, caching, testing, security, performance and system design, from the basics to production scenarios.
Questions 81-100 of 100
Q81.File reads and password hashing in a Node.js service suddenly slow down together. Why?
They share the thread pool. File system operations,
dns.lookup, asynchronous crypto and compression all queue for the same small set of threads, four by default, so a burst of one kind of work delays all the others.How to confirm it:
- Latency rises for operations that use the pool, while pure network calls stay fast.
- Event loop delay may be low, because the main thread is idle while it waits for pool results.
How to fix it:
- Raise the pool size with
UV_THREADPOOL_SIZE, set before startup, typically up to the number of cores. - Reduce heavy work: cache results, lower compression levels, or move password hashing to a dedicated authentication service or worker pool.
- Rate-limit expensive endpoints such as login, which also protects against brute-force attempts.
- Avoid host name lookups per request: reuse connections, or cache DNS results.
A bigger pool helps only when cores are available; if all cores are already busy, more threads just compete for them.
Example
Four pool threads handle password hashes that each take 100 ms.
- Pool capacity is
4 / 0.1 = 40hashes per second. - A login burst brings 60 logins per second, so the queue grows by about 20 per second.
- A file read that needs a pool thread now waits behind the queued hashes, and after 10 seconds it waits roughly
200 / 40 = 5seconds. - With 8 threads on an 8-core machine, capacity becomes 80 hashes per second, the queue drains, and file reads return to normal.
Q82.How would you design a real-time chat backend in Node.js?
Clients keep WebSocket connections to stateless Node.js gateway processes, messages are stored durably first and then fanned out through a broker to every gateway holding a recipient's connection.
flowchart LR A["Client"] -->|WebSocket| B["Gateway process"] B --> C["Message service"] C --> D["Durable message store"] C --> E["Pub-sub broker"] E --> F["Other gateway processes"] F --> G["Recipients' connections"] C --> H["Offline notification queue"]Key decisions:
- Authenticate on connect, and check membership for every room or conversation a client joins.
- Persist before delivering, so a crash never loses a message that was acknowledged to the sender.
- Message IDs and ordering: assign a per-conversation sequence number, so clients can detect gaps, reorder and fetch missed messages after reconnecting.
- Fan-out: the broker routes each message to the gateways where recipients are connected, keyed by conversation.
- Presence and typing indicators: keep them in a fast, expiring store and accept that they are approximate.
- Offline users: queue push notifications.
- Back-pressure: disconnect clients whose send buffers grow too large.
Scale gateways horizontally, and reconnect clients gradually during deploys to avoid a reconnection storm.
Q83.How would you design a Node.js service that combines data from several slower APIs?
This is an aggregation layer, sometimes called a backend for frontend. Node.js fits it well, because the work is mostly waiting on other services. The design goals are calling dependencies in parallel, bounding the total time and returning useful partial results.
flowchart TD A["Client request"] --> B["Aggregation service"] B --> C["Profile service"] B --> D["Orders service"] B --> E["Recommendations service"] C --> F["Combine results within deadline"] D --> F E --> F F --> G["Response with optional sections"]Design points:
- Parallel calls: start independent requests together, so total time is roughly the slowest call rather than the sum.
- Overall deadline: give the request a time budget and pass each call an abort signal derived from it.
- Required versus optional: fail the request if a required source fails, but omit optional sections, such as recommendations, and mark them as unavailable.
- Caching: cache slow-changing data, such as profiles, with short expiry, and coalesce concurrent loads.
- Circuit breakers: stop calling a failing dependency for a while, returning the optional fallback immediately.
- Connection reuse: keep-alive agents per dependency.
- Shape for the client: return exactly what the screen needs, which reduces payload size and client logic.
Q84.How would you migrate a large callback-based Node.js codebase to async/await safely?
Migrate gradually from the edges inward, keeping behavior identical at each step and relying on tests, rather than rewriting everything at once.
- Add tests around critical flows first, especially error handling, since that is where behavior most often changes.
- Wrap callback functions with
util.promisify, or use the promise versions of core modules, so new code canawaitthem immediately. - Convert leaf functions first, those that call nothing else in the codebase, then move up the call tree.
- Keep both interfaces temporarily where many callers exist, so callers can migrate one at a time.
- Watch for behavior changes:
- Errors thrown in async functions become rejections, which need
awaitor.catchin callers. - Callbacks that were called twice by mistake now silently ignore the second call.
- Sequential
awaits can make independent operations slower than parallel callbacks were.
- Errors thrown in async functions become rejections, which need
- Lint for floating promises, so a missing
awaitis caught in review. - Remove the old interfaces once nothing uses them.
Ship each step separately and watch error rates, so a regression is easy to trace to one small change.
Q85.How do you calculate the maximum throughput of a single Node.js process?
A process can run JavaScript on only one core, so its ceiling is set by the CPU time each request spends on the main thread, not by how long the request waits on I/O.
max_requests_per_second = 1 / cpu_seconds_per_requestprocesses_needed = ceil(target_rate / (max_requests_per_second × target_utilization))
What each term means:
- cpu_seconds_per_request: main-thread time per request, such as parsing, validation, serialization and business logic. Time spent waiting on the database does not count.
- target_utilization: how busy you let each process get, usually 60% to 70%, because event loop delay rises sharply near 100%.
When to use it:
- To estimate how many processes or containers a target load needs.
- To see the impact of optimizing hot code: halving CPU time per request doubles the ceiling.
Measure CPU time per request under load with profiling or event loop utilization, and confirm the estimate with a load test, since garbage collection and thread-pool work add overhead.
Example
Each request uses 4 ms of main-thread CPU time, and the target is 1,000 requests per second at 70% utilization.
- Ceiling per process:
1 / 0.004 = 250requests per second. - At 70% utilization, each process should handle
250 × 0.7 = 175requests per second. - Processes needed:
ceil(1,000 / 175) = ceil(5.71) = 6. - If optimizing serialization cuts CPU time to 2 ms, the ceiling doubles to 500 per process, and 3 processes are enough.
Q86.How do you handle very large JSON payloads without blocking the event loop?
Avoid parsing or serializing huge documents in one synchronous call on the main thread. Limit sizes, stream the data in smaller pieces, or move the work to a worker thread.
- Limit request bodies: set a body size limit in the parser and reject larger payloads with status 413, so one request cannot freeze the process.
- Paginate responses: return pages instead of entire collections.
- Stream formats: newline-delimited JSON, one object per line, can be produced and consumed incrementally with streams.
- Streaming parsers: for huge inbound documents, a streaming JSON parser processes values as they arrive.
- Worker threads: when a large document must be handled whole, parse or build it in a worker; transfer the raw bytes to the worker rather than copying a big object.
- Batch imports: accept large uploads as files, process them in a background job, and report progress.
Measure first: time
JSON.parseandJSON.stringifyon realistic payloads. A few hundred kilobytes is usually fine; tens of megabytes on the request path is not.Interview tip: mention that the cost is not only the slow request itself, but every other request that waits behind it on the same event loop.
Q87.How would you implement an LRU cache in JavaScript using `Map`?
A JavaScript
Mapremembers insertion order, so it can act as both the lookup table and the recency list. Deleting and re-inserting a key on each access moves it to the newest end, and the first key in iteration order is always the least recently used.- get: if the key exists, delete it and set it again, then return the value.
- set: delete any existing entry, set the new value, and if the size exceeds the capacity, delete the first key from
map.keys(). - Validation: reject a capacity that is not a positive integer.
Production considerations:
- Add expiry times per entry when cached data can change.
- Size by memory, not just count, if values vary greatly in size.
- Coalesce concurrent loads for the same key, so a miss does not trigger duplicate work.
- Each process holds its own cache, so expect different contents across processes.
All operations are constant time on average, which makes this a clean answer to a classic interview exercise.
Example
After reading
a, insertingcevictsb, the least recently used key.class LRUCache { constructor(capacity) { if (!Number.isInteger(capacity) || capacity <= 0) { throw new RangeError('capacity must be a positive integer'); } this.capacity = capacity; this.map = new Map(); } get(key) { if (!this.map.has(key)) return undefined; const value = this.map.get(key); this.map.delete(key); this.map.set(key, value); return value; } set(key, value) { this.map.delete(key); this.map.set(key, value); if (this.map.size > this.capacity) { this.map.delete(this.map.keys().next().value); } } } const cache = new LRUCache(2); cache.set('a', 1); cache.set('b', 2); cache.get('a'); cache.set('c', 3); console.log([...cache.map.keys()].join(',')); // a,c console.log(cache.get('b')); // undefinedQ88.How would you process a large log file line by line in Node.js?
Create a readable stream for the file, wrap it with
readline.createInterface, and loop over lines withfor await. Memory stays small however large the file is, and line breaks split across chunks are handled for you.- Stream the input:
fs.createReadStream(path)reads the file in chunks; in tests,Readable.fromturns strings into a stream. - Read lines:
readlineemits complete lines; setcrlfDelay: Infinityso Windows-style line endings count as one break. - Parse strictly: match each line against a pattern and count malformed lines instead of crashing.
- Aggregate in a
Map: accumulate counts, sums and errors per key as each line arrives. - Report once the loop finishes.
Scaling further:
- Compressed logs can be streamed through a decompression stage in the same pipeline.
- Several files can be processed in parallel with worker threads, then merged.
- For continuous logs, the same code can read from a socket or queue instead of a file.
Example
Four valid log lines are aggregated per route, and one malformed line is counted and skipped.
const readline = require('node:readline'); const { Readable } = require('node:stream'); const input = Readable.from([ 'GET /jobs 200 120\nGET /jobs 500 900\n', 'POST /apply 201 300\nGET /jobs 200 80\n', 'bad line\n', ]); async function summarize(stream) { const lines = readline.createInterface({ input: stream, crlfDelay: Infinity }); const stats = new Map(); let skipped = 0; for await (const line of lines) { const match = /^(\w+) (\S+) (\d{3}) (\d+)$/.exec(line); if (!match) { skipped++; continue; } const [, method, path, status, ms] = match; const key = `${method} ${path}`; const entry = stats.get(key) ?? { count: 0, errors: 0, totalMs: 0 }; entry.count++; entry.totalMs += Number(ms); if (status.startsWith('5')) entry.errors++; stats.set(key, entry); } return { stats, skipped }; } summarize(input).then(({ stats, skipped }) => { for (const [key, s] of stats) { console.log(`${key} count=${s.count} errors=${s.errors} avg=${Math.round(s.totalMs / s.count)}`); } console.log(`skipped=${skipped}`); }); // Output: // GET /jobs count=3 errors=1 avg=367 // POST /apply count=1 errors=0 avg=300 // skipped=1- Stream the input:
Q89.How do you copy objects safely in JavaScript?
Choose the copy method by what the object contains. Spread syntax copies only the top level; JSON round-tripping loses types;
structuredClonemakes a true deep copy of most data types.- Spread or
Object.assign: a shallow copy. Nested objects are still shared, so changing one through the copy changes the original. JSON.parse(JSON.stringify(x)): a deep copy of plain data only. Dates become strings,MapandSetbecome empty objects,undefineddisappears andBigIntthrows.structuredClone(x): built in, it deep-copies dates, maps, sets, typed arrays and circular references. It throws on functions and does not keep class instances' prototypes.
When to copy at all:
- Copy request or configuration objects before modifying them, so other code is not affected.
- In hot paths, avoid deep copies of large objects; build a new object with only the fields you need.
- Freezing shared configuration with
Object.freeze, recursively if needed, prevents accidental changes without copying.
Example
The shallow copy shares nested data, JSON loses types, and
structuredClonekeeps them.const original = { id: 7, createdAt: new Date('2026-03-01T09:30:00Z'), tags: new Set(['remote']), address: { city: 'Pune' }, }; const deep = structuredClone(original); const viaJson = JSON.parse(JSON.stringify(original)); const shallow = { ...original }; shallow.address.city = 'Delhi'; console.log(original.address.city); // Delhi console.log(typeof viaJson.createdAt, JSON.stringify(viaJson.tags)); // string {} console.log(deep.createdAt instanceof Date, deep.tags.has('remote'), deep.address.city); // true true Pune try { structuredClone({ format() {} }); } catch (error) { console.log(error.name); // DataCloneError }- Spread or
Q90.How would you flatten a nested object into dot-separated keys in JavaScript?
Walk the object recursively, building a path from each key. When a value is a non-empty plain object, recurse with the longer path; otherwise record the value under the full path.
Decisions to make explicit:
- Arrays: keep them as values, or expand them with index keys such as
features.0; keeping them is simpler and usually what configuration and logging need. - Empty objects: keep them as values, so no key silently disappears.
- Special values: treat
null, dates and other non-plain objects as leaf values. - Dangerous keys: when flattening or unflattening user input, skip
__proto__andconstructorto avoid prototype pollution.
Where it is useful:
- Turning nested configuration into environment-style keys.
- Building log fields or search index fields from nested documents.
- Comparing two configurations key by key.
Recursion depth is bounded by the object's nesting, which is small for real data, but untrusted input should have a depth limit.
Example
A nested configuration flattens into dot-separated keys, keeping arrays and empty objects as values.
function flatten(value, prefix = '', out = {}) { for (const [key, child] of Object.entries(value)) { const path = prefix ? `${prefix}.${key}` : key; const isObject = child !== null && typeof child === 'object' && !Array.isArray(child); if (isObject && Object.keys(child).length > 0) { flatten(child, path, out); } else { out[path] = child; } } return out; } const config = { db: { host: 'db.internal', pool: { min: 2, max: 10 } }, features: ['search'], debug: false, empty: {}, }; console.log(JSON.stringify(flatten(config))); // Output: // {"db.host":"db.internal","db.pool.min":2,"db.pool.max":10,"features":["search"],"debug":false,"empty":{}}- Arrays: keep them as values, or expand them with index keys such as
Q91.How would you parse and validate pagination query parameters in Node.js?
Read the raw strings with
URLSearchParams, convert each one explicitly, apply defaults for missing values, enforce bounds, and allow only known values for options such as the sort order. Return all problems together, so the client can fix them in one go.- Strict numbers: check the raw string against a digits-only pattern before converting, because
Number('')is 0 andparseInt('20abc')is 20. - Bounds: cap the page size, for example at 100, so clients cannot request a million rows.
- Allowlists: accept only defined sort fields, which also prevents injecting column names into SQL.
- Defaults: apply them only when a parameter is absent, not when it is invalid.
- Errors: respond with status 400 and the list of problems.
Schema validation libraries do this declaratively, but writing it once by hand shows exactly what they must handle.
Example
Valid, invalid and empty query strings produce typed values, a list of errors, or defaults.
function parsePaging(query) { const params = new URLSearchParams(query); const errors = []; const toInt = (name, fallback, min, max) => { const raw = params.get(name); if (raw === null) return fallback; if (!/^\d+$/.test(raw)) { errors.push(`${name} must be a whole number`); return fallback; } const value = Number(raw); if (value < min || value > max) errors.push(`${name} must be between ${min} and ${max}`); return value; }; const limit = toInt('limit', 20, 1, 100); const page = toInt('page', 1, 1, 10000); const sort = params.get('sort') ?? 'newest'; if (!['newest', 'salary'].includes(sort)) errors.push('sort must be newest or salary'); return errors.length ? { errors } : { limit, page, sort }; } console.log(JSON.stringify(parsePaging('limit=50&sort=salary'))); // {"limit":50,"page":1,"sort":"salary"} console.log(JSON.stringify(parsePaging('limit=500&page=-1&sort=name'))); // {"errors":["limit must be between 1 and 100","page must be a whole number","sort must be newest or salary"]} console.log(JSON.stringify(parsePaging(''))); // {"limit":20,"page":1,"sort":"newest"}- Strict numbers: check the raw string against a digits-only pattern before converting, because
Q92.How would you implement a simple in-process event bus with unsubscribe in JavaScript?
Keep a
Mapfrom topic to aSetof handler functions.subscribeadds a handler and returns a function that removes it;publishcalls every handler for the topic and isolates failures, so one broken subscriber cannot stop the others.Design points:
- Return an unsubscribe function, which makes cleanup easy and avoids listener leaks.
- Copy the handler set before iterating, so handlers can unsubscribe themselves during delivery.
- Catch errors per handler and log them, then continue with the remaining handlers.
- Synchronous or asynchronous: this version delivers synchronously; for slow handlers, queue the work or await handlers with a concurrency limit.
Limits of an in-process bus:
- Events exist only in memory, so they are lost if the process crashes, and other processes never see them.
- For events that must survive restarts or reach other services, use a durable message broker.
It is still useful for decoupling modules inside one service, such as sending an email and updating stock when an order is created.
Example
Three handlers subscribe; one keeps failing without stopping the others, and unsubscribing removes the email handler.
function createBus() { const handlers = new Map(); return { subscribe(topic, handler) { if (!handlers.has(topic)) handlers.set(topic, new Set()); handlers.get(topic).add(handler); return () => handlers.get(topic)?.delete(handler); }, publish(topic, message) { let delivered = 0; for (const handler of [...(handlers.get(topic) ?? [])]) { try { handler(message); delivered++; } catch (error) { console.log(`handler failed: ${error.message}`); } } return delivered; }, }; } const bus = createBus(); const received = []; const stopEmail = bus.subscribe('order.created', (order) => received.push(`email ${order.id}`)); bus.subscribe('order.created', (order) => received.push(`stock ${order.id}`)); bus.subscribe('order.created', () => { throw new Error('analytics down'); }); console.log(bus.publish('order.created', { id: 1 })); stopEmail(); console.log(bus.publish('order.created', { id: 2 })); console.log(received.join(',')); // Output: // handler failed: analytics down // 2 // handler failed: analytics down // 1 // email 1,stock 1,stock 2Q93.When is Node.js a good choice for a backend service, and when is it not?
Node.js is a strong choice when a service spends most of its time waiting on networks and databases, and when sharing JavaScript between frontend and backend helps the team. It is a weaker choice when most work is heavy computation on the request path.
Good fit Weaker fit APIs and aggregation layers that mostly call databases and services CPU-heavy work on the request path, such as video encoding or model inference Real-time features with many open connections, such as chat and live dashboards Workloads needing precise decimal arithmetic everywhere Proxies, gateways and integration services Systems where garbage collection pauses must be tightly controlled Teams sharing one language and validation across the stack Teams with deep experience and tooling built around another runtime CPU-heavy work can still fit when it is offloaded to worker threads or a separate service, so the event loop stays free for requests.
Other factors: team experience, existing libraries, the operational tooling already in place, and hiring all matter as much as runtime traits. A well-built Node.js service and a well-built service in another runtime are both usually limited by their database first.
Q94.How do you organize several Node.js packages in one repository?
Use the package manager's workspaces feature: one repository holds several packages, such as services and shared libraries, with a single lock file and one install for everything. Shared code is linked locally, so changes are visible immediately without publishing.
Benefits:
- One change can update a shared library and every service that uses it, reviewed together.
- One lock file keeps dependency versions consistent across packages.
- Shared tooling, such as lint rules, formatting and test configuration, lives in one place.
Challenges and how to handle them:
- Build and test only what changed: compute which packages a change affects, including dependents, so the pipeline stays fast.
- Clear boundaries: packages should import each other only through their declared entry points, never through relative paths into internals.
- Versioning: decide whether internal packages are versioned independently or always move together.
- Deployment: each service's image should include only that service and the packages it depends on.
- Dependency hoisting: packages may accidentally use dependencies they never declared; strict modes in package managers catch this.
Q95.How do you secure a Node.js API end to end?
Combine secure defaults at every layer: the network edge, the HTTP layer, the application code, the dependencies and the runtime. No single control is enough.
HTTP layer:
- HTTPS only, security headers, strict CORS allowlists, body size limits and rate limits on sensitive endpoints such as login.
Authentication and authorization:
- Slow password hashing, short-lived tokens or secure session cookies, and authorization checks on every request, including object ownership.
Input and output:
- Schema validation with unknown keys rejected, parameterized queries, protection against prototype pollution, and encoding of any data rendered as HTML.
Dependencies:
- Lock files, clean installs, vulnerability scanning, minimal dependencies and install scripts disabled in builds.
Runtime:
- Non-root user, no inspector port exposed, secrets injected from a secret store, and errors that never reveal stack traces to clients.
Operations:
- Logs without secrets, alerts on authentication failures and unusual traffic, and fast patching of both Node.js and dependencies.
Review this list in threat modeling for each new feature, and test it with automated security scans in the pipeline.
Q96.What should you know about timers, drift and `unref` in Node.js services?
Timers in Node.js are minimum delays, not precise schedules. They also keep the process alive, which matters for clean shutdown and for command-line tools.
- Drift: a callback runs only when the event loop is free, so a busy loop delays it.
setIntervaldoes not catch up on missed time, and long-running intervals drift away from wall-clock times. - Overlapping work: if an interval's callback starts async work that takes longer than the interval, runs pile up. Prefer scheduling the next run with
setTimeoutafter the current one finishes. - Keeping the process alive: an active timer prevents the process from exiting.
timer.unref()lets the process exit if that timer is the only thing left, which suits background refreshers. - Cleanup: clear timers on shutdown, and in tests, so nothing runs after resources are closed.
- Wall-clock schedules: for "every day at 9", compute the delay from the current time each run, or use a scheduler, because intervals drift and time zones change.
Timeouts on promises:
timers/promisesprovides awaitable timers that accept an abort signal, which makes cancellation clean.- Drift: a callback runs only when the event loop is free, so a busy loop delays it.
Q97.How do you handle backpressure between a fast producer and a slow database writer in Node.js?
Never let incoming data pile up in memory faster than it can be written. Pause or slow the producer while the writer catches up, using streams with backpressure or a bounded queue.
flowchart LR A["Fast producer"] --> B["Bounded queue"] B --> C["Batch writer"] C --> D["Database"] B -->|Full| E["Pause the producer"] E -->|"Queue drained"| A- Streams: if the source is a stream, such as a file or socket, process it with
for awaitorpipeline; reading the next chunk only after the current batch is written applies backpressure automatically. - Batch writes: collect rows into batches of a few hundred and write each batch in one statement, which raises the writer's throughput.
- Bounded queue: for push-based sources, such as events from a message broker, keep an in-memory queue with a maximum size and stop consuming, or pause the subscription, when it is full.
- Limit concurrency: allow only a few batches in flight, matched to the database pool size.
- Observe: track queue length and write latency, and alert when the queue stays near its limit.
Never respond to slow writes by raising memory limits or buffering without bounds; that only delays the crash and loses more data when it happens. If the database is permanently slower than the input, the real fixes are bulk loading, partitioning or more capacity.
- Streams: if the source is a stream, such as a file or socket, process it with
Q98.How do you make a Node.js service scale horizontally?
Make every process interchangeable: keep no essential state in memory, put shared state in external stores, and make background work safe to run anywhere. Then adding processes or containers adds capacity linearly.
flowchart TD A["Load balancer"] --> B["Node.js process 1"] A --> C["Node.js process 2"] A --> D["Node.js process 3"] B --> E["Shared session and cache store"] C --> E D --> E B --> F["Database and object storage"] C --> F D --> F- Sessions: store them in a shared store, or use signed tokens, so any process can serve any user.
- Uploads and files: write them to shared object storage, not local disk.
- Caches: in-process caches are fine for performance, but correctness must not depend on them; use a shared cache when every instance must agree.
- Scheduled jobs: run them once across the fleet through a queue or a leader lock, not once per process.
- Rate limits and counters: keep them in a shared store.
- WebSockets: use a broker for fan-out, and sticky routing only where a protocol truly needs it.
- Configuration: inject it from the environment, identically for every instance.
- Database: the shared database becomes the bottleneck, so pool connections carefully and add read replicas or caching as load grows.
Interview tip: a quick test is to ask what breaks if a request's next call lands on a different process; every answer to that question is a scaling problem to fix.
Q99.How should you explain a Node.js backend project you personally worked on?
Choose one real project and walk through it as a story: the problem, your role, the key decisions, a hard problem you solved and the measured outcome. Interviewers want evidence of judgment, not a list of packages.
- Context: what the service did, its users and its scale, such as requests per second or open connections.
- Your role: what you personally designed or built, separate from the team's work.
- Decisions: how you structured the code, handled async errors, chose the database and caching approach, and why.
- A hard problem: an event loop blockage, a memory leak, a race condition or a performance bottleneck, with the evidence that led you to the cause.
- Quality and operations: tests, deployment, monitoring and how you knew it was healthy.
- Results: concrete numbers, such as "p99 latency fell from 1.2 seconds to 250 ms" or "memory stayed flat after the leak fix".
- Reflection: what you would do differently now.
What the interviewer is listening for:
- Understanding of the Node.js runtime model, not just framework usage.
- Trade-off reasoning and honest ownership of mistakes.
- Clear, simple explanations of technical choices.
Q100.How do you discuss a production incident you handled in a Node.js service?
Tell the incident in order: impact, detection, mitigation, diagnosis, root cause and prevention. Show that you restored service first and investigated with evidence, not guesses.
- Impact: what users experienced and for how long, such as error rates or failed checkouts.
- Detection: which alert or report surfaced it, and how quickly.
- Mitigation: what you did to stop the impact before knowing the cause, such as rolling back, disabling a feature or adding capacity.
- Diagnosis: the signals you used, such as event loop delay, heap growth, CPU profiles, logs and traces, and the hypotheses you ruled out.
- Root cause: stated plainly, with contributing factors such as a missing timeout or test.
- Prevention: the code fixes, alerts, tests and process changes that followed, and how you verified they worked.
- Lessons: what you learned about the system or about responding under pressure.
What the interviewer is listening for:
- Calm prioritization of recovery over root cause in the first minutes.
- Blameless language that focuses on how the system allowed the failure.
- Follow-through, such as an alert that now catches the same problem early.