Production Incident Diagnosis and Distributed Systems Troubleshooting Questions
Debugging distributed systems under fire: diagnosing latency and reliability regressions, root-causing across service boundaries, reading traces and metrics during an incident, and reasoning about complex production failures. Covers the investigative method for hard-to-reproduce, multi-service problems. The operational counterpart to resilient design.
A downstream third-party API starts rate-limiting you, and your worker processes retry aggressively, causing a queue buildup that exhausts memory and leads to out-of-memory errors and service restarts. Describe the immediate containment actions to stop the cascading failures, and propose a longer-term architecture change to prevent similar incidents.
Sample Answer
Direct answer. The chain here is straightforward once you see it: rate-limiting from the third party is the trigger, but aggressive retries turned a manageable slowdown into a memory-exhausting queue buildup, so the fix needs to address both the trigger and the amplification, not just one.
Structured elaboration.
- Immediate containment: stop the retry amplification first. Since the retries are what's consuming memory (queued work waiting to be retried), the fastest lever is reducing or pausing retry volume, either by lowering the retry count/rate at the application level or, if that's not quickly deployable, shedding load at the point where work enters the queue in the first place.
- Confirm the mechanism with data before assuming. Check queue depth or memory growth against the timing of the rate-limit responses starting; check the retry configuration (count, delay, whether it backs off) to confirm it's actually as aggressive as suspected, rather than assuming.
- Address the root trigger. Since the third party's rate limit is presumably a fixed constraint you don't control directly, the sustainable fix is respecting it: implement proper backoff (ideally reading and honoring any rate-limit-related response headers or codes the third party provides, like a retry-after value, rather than retrying blindly) and cap total outstanding retries so the system can't queue unboundedly regardless of how long the rate-limiting lasts.
- Longer-term architecture change. A bounded queue with backpressure (rejecting or shedding new work once the queue is full, rather than growing it unboundedly) prevents this specific failure mode (OOM from unbounded queuing) structurally, so a future rate-limiting event from ANY dependency degrades gracefully instead of taking down the service. A circuit breaker around the third-party call, tripping after a run of rate-limit responses and giving both this service and the third party a cooldown, is a complementary pattern that reduces load on the third party too, which can help it recover faster.
- Recovery once contained. Once memory pressure is relieved and the retry storm is under control, drain the backlog of legitimately-still-needed work at a controlled rate (respecting the third party's limit) rather than releasing it all at once, which would just recreate the same rate-limiting collision immediately.
Worked example. Suppose the retry configuration shows up to 5 retries with a fixed 100ms delay and no cap on total concurrent outstanding retries; under normal conditions with no rate-limiting, this rarely matters since most calls succeed on the first try. Once the third party starts rate-limiting (say, returning 429s for roughly 30% of calls during a traffic spike on their end), each of those failed calls generates up to 5 retry attempts spaced only 100ms apart, and if the retries ALSO get rate-limited (likely, since the underlying constraint hasn't gone away in 100ms), work piles up in the retry queue far faster than it drains. If the service normally processes, say, 1,000 jobs per minute and 30% now need up to 5 retries each at essentially no effective backoff, the queue can grow by hundreds of pending retries per minute with no ceiling, which is what exhausts memory. Adding exponential backoff (say starting at 500ms and doubling, capped at a few seconds) combined with a hard cap of, say, 3 total attempts and a bounded queue size would keep the same rate-limiting event from ever reaching an OOM condition, even if the third party's rate-limiting itself lasts much longer.
Trade-offs and pitfalls. A bounded queue with backpressure means SOME work gets explicitly rejected or delayed rather than silently queued forever, which is a real behavior change stakeholders need to understand (the alternative, unbounded queuing, isn't actually safer, it just fails later and worse). It's also worth checking whether this same unbounded-retry pattern exists on OTHER outbound calls this service makes, not just the one that caused this specific incident, since the underlying retry configuration is likely shared code.
Monitoring shows that adding more instances of a microservice increased average and p95 latency instead of reducing it. Walk through a debugging checklist explaining possible causes (for example shared-resource contention, DNS or iptables issues, connection-pool exhaustion, or leader-election thrashing), how you'd gather evidence for each, and the remedial action for each cause.
Sample Answer
Direct answer. This is a genuinely counter-intuitive result, since adding instances should spread load and reduce latency, so the debugging checklist has to specifically look for mechanisms where MORE instances create MORE overhead or contention rather than simply assuming the scaling itself is broken.
Structured elaboration.
- Shared-resource contention. If the new instances all compete for the same downstream resource (a database, a cache, a shared connection pool with a fixed total size), adding instances doesn't add capacity to that shared resource, it just adds more competitors for the same fixed pie; check whether a shared downstream's own load or connection count grew proportionally to the new instance count, and whether ITS latency (not just this service's) got worse at the same time.
- DNS or iptables-level issues. A load balancer or service-discovery mechanism that takes non-trivial time to register or fully propagate new instances can cause uneven traffic distribution during scale-up (some instances overloaded while others are still ramping up); at the OS level, an inefficient iptables ruleset (iptables is the Linux kernel's packet-filtering and routing system, commonly used under the hood to implement service load-balancing rules in container networking) can scale poorly with a growing number of backend targets, adding real per-packet overhead as the instance count grows.
- Connection-pool exhaustion, from a different angle than shared-resource contention: if EACH instance opens its own pool of connections to a downstream, more instances can mean MORE total connections than the downstream can handle, even if each instance's own pool looks reasonably sized in isolation.
- Leader-election thrashing, if this service participates in any kind of coordination or leader election: more instances competing for leadership, or more instances triggering more frequent rebalancing in whatever coordination mechanism is in use, can itself consume real resources and add latency, especially if the coordination overhead scales poorly with instance count.
- Gather evidence for each candidate rather than guessing. For each of the above, there's a specific, checkable signal: shared-resource metrics correlated with instance count and latency; load-balancer target-registration timing and per-instance traffic distribution; per-instance versus aggregate connection counts against the shared downstream's own limits; and coordination-service metrics (rebalance frequency, election frequency) if relevant.
- Remedial actions per cause. Shared-resource contention needs the shared resource itself scaled or partitioned, not just the calling service. Load-balancer or iptables issues need investigation of the specific networking layer causing the overhead, which may mean a different load-balancing algorithm or container-networking mode. Connection-pool exhaustion needs a TOTAL connection budget considered across all instances, not just per-instance. Leader-election thrashing needs either fewer participants in the coordination (a subset act as candidates, not every instance) or a coordination mechanism that scales better.
Worked example. Suppose per-instance connection-pool size is a fixed 20 connections to a shared database, and the database's own max-connections limit is 200; at 8 instances, that's 160 total possible connections, comfortably under the limit. Scaling to 15 instances pushes the theoretical maximum to 300, which exceeds the database's 200-connection limit; once actual usage approaches that ceiling, connections start queueing or getting rejected, and EVERY instance (not just the newest ones) experiences worse latency waiting for a connection slot, which is exactly the paradox described: more instances made the shared resource, not any individual instance, the bottleneck. The fix is either reducing per-instance pool size as instance count grows (keeping the total bounded) or increasing the database's connection limit and capacity to match, with the total connection budget explicitly tracked as a fleet-wide constraint rather than a per-instance setting nobody re-evaluates as the fleet grows.
Trade-offs and pitfalls. The core lesson worth internalizing here is that per-instance settings (pool sizes, timeouts, cache sizes) that look reasonable in isolation can become a fleet-wide problem purely from being multiplied across a growing instance count; any config that's 'per instance' should be evaluated against what happens at your MAXIMUM realistic instance count, not just today's count. It's also worth being skeptical of your own instinct here: 'add more instances' is such a common, usually-correct scaling response that it's easy to reach for it again as the FIX when it was actually the trigger.
Given this simplified trace for a single request, identify where the latency spike originates and why:
TraceID: abc123
Spans:
- gateway (api-gw): duration 50ms
- auth (service-b): duration 5ms
- payments (service-c): duration 400ms
- db-proxy (service-d): duration 380ms
- db-query: duration 370ms
- db-proxy (service-d): duration 380ms
Explain the steps you would take to confirm the database is the true root cause and what further data you'd collect before concluding.
Sample Answer
Direct answer. Reading the spans, the request makes three sequential hops (gateway, then auth, then payments), and within the payments hop the 400ms is almost entirely accounted for by the 380ms db-proxy span beneath it, which is itself almost entirely the 370ms db-query span, so the database query is the dominant contributor to this request's latency; the gateway and auth hops (50ms and 5ms) are not meaningfully part of the problem.
Structured elaboration.
- First, read the SHAPE of the tree correctly, before computing anything.
gateway,auth, andpaymentsare three separate, sibling spans in this trace: one request flowing sequentially through a gateway hop, an auth check, and a payments call, one after another, not one nested inside another. Onlypaymentshas a child of its own (db-proxy, which in turn has its own child,db-query). That distinction matters because it decides which spans you even need to subtract anything from. - Compute each span's OWN time (its duration minus its children's durations), only where a child exists to subtract.
gatewayandauthhave no children shown in this trace, so their own time is simply their full duration: 50ms and 5ms.paymentshas one child (db-proxyat 380ms), so its own time is 400 minus 380 = 20ms: the payments SERVICE's own code cost only 20ms, the rest of its 400ms is time spent waiting on its call todb-proxy.db-proxyhas one child (db-queryat 370ms), so its own time is 380 minus 370 = 10ms.db-queryhas no child shown, so its own time is its full 370ms.span total duration child duration own time gateway 50ms none 50ms auth 5ms none 5ms payments 400ms 380ms (db-proxy) 20ms db-proxy 380ms 370ms (db-query) 10ms db-query 370ms none 370ms - Add up the own-times and see where the total request time actually went. 50 + 5 + 20 + 10 + 370 = 455ms, which is the full sequential request time (50 + 5 + 400). db-query's 370ms alone is roughly 370/455 ≈ 81% of that total, by a wide margin the single largest contributor; gateway and auth together are only about 12%.
- Confirm it's the query itself, not something around it. Before concluding 'the database is slow', check three distinct possibilities that all show up as a slow
db-queryspan: the query itself is slow (missing index, bad query plan, lock contention), the connection pool made the caller wait before the query even started (which would usually show as extra time BEFORE the query span begins, not inside it), or the database host itself is resource-constrained (high CPU, disk I/O, or replication lag if this is a replica). - Pull further data to distinguish those. Check the database's own slow-query log or
EXPLAINplan for this query around the incident window; check whether the query's duration is consistently ~370ms or spiking intermittently (consistent points to a plan or index problem, spiky points to contention or a noisy neighbor on the host); check the database host's own CPU, I/O, and lock-wait metrics for the same window. - Confirm before you commit to a fix. If
EXPLAINshows a sequential scan where an index should be used, or the slow-query log shows this exact query is consistently the slowest one running, that confirms the query itself and points to an index or query-shape fix. If instead the database host's CPU or I/O is saturated across many queries at once, the fix is capacity or isolating this workload, not this one query.
Worked example. Suppose the slow-query log shows this exact query pattern (a lookup by a non-indexed customer_email column) running consistently between 350 and 390ms throughout the incident window, while other indexed queries against the same table stay under 10ms. That consistent, query-specific slowness (not database-wide) points squarely at a missing index on customer_email: adding it would be expected to bring this query down to roughly the same low-single-digit-millisecond range as the other indexed lookups on that table. Since db-query is nested three levels deep (inside db-proxy, inside payments), fixing it collapses the whole chain: db-proxy's own 10ms plus a near-zero query time would bring payments down from 400ms to roughly its own 20ms plus a few milliseconds of query time, and the full request (gateway + auth + payments) would drop from about 455ms to well under 100ms.
Trade-offs and pitfalls. The most common mistake reading a trace like this is to see 'payments took 400ms' and start investigating the payments service's own code, when the trace is telling you the payments service's code only cost about 20ms and the other 380ms is entirely its call to the database, three levels down. The second most common mistake is the one this answer corrects itself on above: subtracting a span's duration from a SIBLING's duration because they happen to be listed near each other, rather than checking which spans are actually nested inside which. Always attribute time to the span whose OWN duration (after subtracting only its ACTUAL children) is largest, and follow the tree down until a span has no slower child left. It's also worth checking for cache misses: if this query is normally served from a cache and the cache was cold or evicted, the fix might be restoring the cache rather than touching the database at all.
Users report high p99 latency on a critical API. Propose a comprehensive instrumentation and analysis plan to determine whether the cause is GC pauses, lock contention, network wait, or downstream calls. Specify which metrics, profilers, and span attributes you would collect, and the order in which you would investigate them.
Sample Answer
Direct answer. p99 latency has four common suspects (GC, or garbage collection, pauses; lock contention; network wait; downstream calls), and the fastest way to distinguish them is to collect data that isolates TIME SPENT rather than guess from the symptom alone, starting with whatever's cheapest to check and narrowing from there.
Structured elaboration.
- Start with what you already have: request-level tracing. If spans show most of the request's time inside a single downstream call, that points at downstream calls directly and you can stop chasing GC or locks; if the time is spent inside your own service's code with no obvious downstream span accounting for it, that points at GC, lock contention, or CPU-bound work inside the process itself.
- If it's inside the process, check GC metrics next, since they're usually the cheapest to pull: GC pause time and frequency, correlated against the same time window as the latency spike. A JVM (Java Virtual Machine) or similar managed runtime, such as the .NET CLR or the Go runtime, typically exposes this directly. If GC pauses spike in the same window as p99 latency, that's a strong lead.
- If GC looks clean, check for lock contention. Thread-dump sampling (or a profiler's lock-contention view) during the bad window shows whether threads are spending time BLOCKED waiting for a lock rather than doing work. This is a common cause of tail latency specifically (not average latency) because it only bites when enough concurrent requests collide on the same lock at once.
- Check network wait separately from downstream-call latency itself, since they're different things: network wait is time lost to the network layer (DNS resolution, TCP handshake, TLS negotiation, or packet loss causing retransmits) as distinct from the downstream service's own processing time. Span attributes that separate 'time to first byte' from 'connection setup time' help distinguish these.
- Order of investigation, in practice: traces first (cheap, and often points straight at the answer), then GC (cheap to check, common cause), then lock contention (needs a profiler or thread dump, more involved), then network-level (needs packet-level tools, most involved), unless traces already pointed you at one of these directly.
Worked example. Say traces show no single downstream call accounts for the tail latency, but GC metrics show pause times jumping from a typical 10 to 15ms to occasional 400 to 600ms pauses during the bad window, and those pauses' frequency correlates with a recent increase in heap allocation rate (visible as a steeper slope on the allocated-bytes metric). That's consistent with a garbage collector doing more frequent full collections because allocation pressure increased, likely from a recent code change that allocates more per request than before. The fix path is either reducing allocation in the hot path or tuning the collector's heap sizing and generation thresholds (the size limits that decide when the collector promotes an object from the fast-collected 'young generation' to the less-frequently-collected 'old generation') to handle the new allocation rate without such long pauses, and you'd validate by checking that p99 latency and max GC pause time both drop after the change, in the same dashboard you used to spot the correlation.
Trade-offs and pitfalls. The temptation is to check the cheapest signal (metrics) and stop as soon as you see ANY anomaly, but a GC metric that looks slightly elevated might be a red herring if it doesn't actually correlate with the specific timestamps of the latency spikes; always confirm the timing lines up, not just that both look 'bad'. It's also easy to conflate lock contention and GC pauses since both show up as 'CPU-bound work stalling', but they need very different fixes (reducing critical-section size or lock granularity versus tuning collector behavior or reducing allocations), so profiler-level evidence matters more than a guess based on which one you've seen before.
Your team gets reports of increased error-budget consumption for an API during a marketing campaign surge. How would you triage whether the consumption is due to legitimate increased load, a bug, or malicious traffic? Describe the telemetry and controls you'd use to distinguish these three causes and the immediate mitigation you'd apply for each.
Sample Answer
Direct answer. These three causes (legitimate load, a bug, malicious traffic) leave different fingerprints in your telemetry, so the triage is really about which specific signals distinguish them, not treating error-budget consumption as one undifferentiated problem. An error budget is the amount of acceptable errors or downtime you're allowed before breaching your service's reliability target, tracked so you know how much room you have left before you need to act.
Structured elaboration.
- Check traffic composition against the marketing campaign's expected shape. If the campaign is expected to drive traffic to specific pages, endpoints, or a specific user segment, does the traffic increase match that shape? Traffic that matches the expected campaign pattern (right endpoints, right general geography or channel, reasonable per-user request rates) looks like legitimate load. Traffic that doesn't match the expected shape (unexpected endpoints, unusual request patterns, disproportionate volume from a narrow set of IPs) is a flag for something else.
- Check for bug signatures independent of traffic volume. If the error rate is rising DISPROPORTIONATELY to the traffic increase (say traffic is up 3x but errors are up 20x), that points more toward a bug or a capacity limit being crossed than toward pure organic load, since a well-behaved system's error rate under legitimate load shouldn't spike that much faster than the load itself. Correlate against any recent deploy or config change too, the same way you would for any deploy-correlated regression.
- Check for malicious-traffic signatures. Request patterns typical of abuse: very high request rates from individual IPs or accounts, requests that don't follow normal user navigation patterns (hitting endpoints in an order or at a rate no real user session would), or a spike concentrated in a way that doesn't match the campaign's actual targeting (traffic from regions or channels the campaign never touched).
- Distinguish causes are not mutually exclusive: a real campaign-driven traffic increase can simultaneously expose a latent bug or a capacity limit that wouldn't have been reached at normal traffic; the point of triage is characterizing the MIX, not forcing a single explanation.
- Immediate mitigation matched to each cause. For legitimate load exceeding capacity: scale out, or shed lower-priority requests to protect capacity for the campaign traffic itself. For a bug: the same deploy-correlated-regression response as any other incident (roll back or fix forward). For malicious traffic: rate-limit or block the specific offending pattern (by IP, by account, by request signature) without throttling legitimate campaign traffic in the process.
Worked example. Suppose traffic to the campaign's specific landing page and signup flow is up roughly 4x, matching the campaign's expected reach reasonably well, but error-budget consumption is up nearly 15x relative to baseline error rate, concentrated specifically on the signup endpoint. Digging into those specific errors shows they're mostly timeouts, and checking the signup service's own dashboards shows its database connection pool, sized for normal traffic, is now saturated at 4x load, a straightforward capacity gap rather than a code bug or an attack, since the traffic composition genuinely matches the campaign's expected shape. The immediate mitigation is scaling the connection pool (or the underlying database capacity) to handle the now-higher legitimate load, not blocking traffic or rolling back a deploy.
Trade-offs and pitfalls. It's tempting to default to 'it's probably the campaign, that's expected' and under-investigate, which risks missing a genuine bug or an attack riding alongside the legitimate spike; always check whether the error INCREASE is proportional to the traffic increase, since disproportionate error growth is the strongest single signal that something beyond pure volume is happening. The opposite mistake, assuming malicious traffic and aggressively rate-limiting during a real marketing push, risks throttling the very campaign traffic the business is trying to capture, which is its own costly error.
Unlock Full Question Bank
Get access to all Production Incident Diagnosis and Distributed Systems Troubleshooting interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.