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.
You have an intermittent race condition in production producing duplicate orders, and staging cannot reproduce it at any realistic scale. Design a safe reproduction approach using traffic mirroring, controlled chaos, synthetic requests, and feature flags. Explain how you'd collect deterministic traces of the bug, how you'd disable write-through side effects while testing, and how you'd protect user privacy when mirroring production traffic.
Sample Answer
Direct answer. Reproducing a rare race condition safely in production traffic (since staging can't trigger it at scale) means observing real traffic patterns without letting your reproduction attempt cause the very duplicate-order side effect you're investigating, which is the central design tension of this whole approach.
Structured elaboration.
- Traffic mirroring, done safely. Mirror a copy of real production traffic to an isolated environment where you can instrument heavily, but the mirrored copy's WRITES must not actually reach production systems (the real order-processing backend, payment systems, inventory), since the goal is deterministic observation, not adding more real, uncontrolled write traffic to the very system you're debugging.
- Controlled chaos to increase the odds of triggering it. Since the bug requires a specific timing collision to manifest, deliberately introducing controlled variability (slight, bounded delays or reordering in a test harness) into the mirrored environment can increase how OFTEN the race condition's triggering conditions occur, without touching real production behavior at all, making the rare bug reproducible on a much shorter timescale than waiting for it to happen naturally.
- Synthetic requests as a complementary technique. Once you have a hypothesis about the specific timing window involved, purpose-built synthetic requests that deliberately try to land within that window (sent to the same isolated, non-production-writing environment) can confirm the hypothesis far more directly than waiting for mirrored real traffic to happen to collide the right way.
- Feature flags as the control and safety layer. Feature flags are what let you turn the other three techniques on and off without a deploy: gate the heavier instrumentation and controlled-chaos injection so they only apply to mirrored or synthetic traffic, never to real production requests, and give yourself an instant kill switch that can disable the whole reproduction harness in seconds if anything about it starts behaving unexpectedly. A flag is also a second, independent layer on top of the write-stubbing itself: structure it so a request can only reach the write-stubbed path if it's carrying a flag that marks it as part of this investigation, so a bug in the stubbing logic alone isn't the only thing standing between the reproduction attempt and a real write.
- Deterministic trace collection. Ensure the isolated environment captures full, high-fidelity traces (not sampled) for every request during the investigation, since you specifically need to see the exact interleaving of events that led to a duplicate, and a sampled trace might simply miss the one request pair that mattered.
- Disabling write-through effects during testing. This is the mechanism that makes the whole approach safe: the mirrored/synthetic environment needs its outbound calls to real downstream systems (anything that would actually create an order, charge a card, or update inventory) intercepted and stubbed, so the investigation can observe the RACE CONDITION's logic playing out without any of its side effects actually happening.
- Privacy when mirroring production traffic. Real production traffic contains real customer data; mirroring it for debugging needs the same data-handling discipline as production itself (access controls, retention limits, ideally scrubbing or tokenizing sensitive fields that aren't needed for this specific investigation), not a looser standard just because it's 'for debugging'.
Worked example. Suppose the hypothesis is that the race occurs when two requests for the same order ID arrive within a roughly 5 to 10 millisecond window (perhaps from a client that mistakenly double-submits, or a retry that fires just as the original request is completing). A synthetic test harness that intentionally sends two identical order-creation requests with a controlled, sweeping delay between them (testing 1ms, 5ms, 10ms, 20ms gaps against the isolated, write-stubbed environment) can map out the exact window where the duplicate actually occurs, turning a rare, unpredictable production event into a reliably reproducible one within the test harness, letting you confirm both the root cause and, later, that a fix actually closes the window across the whole range you tested.
Trade-offs and pitfalls. The single most important discipline in this whole approach is making absolutely certain the mirrored or synthetic environment's write-through paths are genuinely stubbed, not just believed to be; a mistake here would mean 'safe reproduction testing' actually creates real duplicate orders in production, which is the exact harm you're trying to investigate, now caused by the investigation itself. It's also worth budgeting real time for the privacy work (scrubbing or tokenizing mirrored customer data) rather than treating it as an afterthought, since skipping it to move faster creates a real compliance and trust risk for the sake of debugging convenience.
You observe high tail latency (p99) for a microservice under load, even though the median latency remains acceptable. Outline a step-by-step troubleshooting plan, including instrumentation, reproducing the load pattern, targeted mitigations, and how you'd validate the fixes in production without risking further user impact.
Sample Answer
Direct answer. When p99 is bad but the median is fine, the problem is affecting a MINORITY of requests in a specific way, so averaging or median-based dashboards will actively hide it; the investigation has to specifically pull out the slow tail and ask what's different about those requests.
Structured elaboration.
- Isolate the slow requests specifically, don't look at aggregate dashboards. Filter your tracing or logging to just the requests above, say, the 95th or 99th percentile latency threshold, and look for what they have in common: a specific endpoint, a specific customer or tenant, a specific payload size or shape, a specific instance or availability zone, or a specific time-of-day pattern.
- Check for resource contention that only bites intermittently. A thread pool or connection pool that's usually adequate can occasionally saturate under a burst, causing a small fraction of requests to queue while most sail through; this produces exactly a good-median-bad-tail signature. Garbage-collection pauses have the same shape: most requests are unaffected, a small fraction land during a pause and get delayed.
- Check for a specific slow dependency call that only some requests make. If a feature flag, a conditional code path, or a specific customer's data shape triggers an extra downstream call (a cache miss requiring a database lookup, for example) for only a subset of requests, the median (mostly fast, cache-hit requests) looks fine while the tail (the subset needing the extra call) is slow.
- Reproduce under load, targeting the specific pattern found in step 1. Once you have a hypothesis (say, large payloads are slow), a load test that specifically varies that dimension can confirm it without waiting for it to reoccur naturally in production.
- Validate the fix without risking more user impact. Roll a fix out to a small percentage of traffic first and specifically watch the SAME tail-latency metric you used to find the problem, not just the aggregate, since a fix could improve the median while barely touching the tail if you mis-diagnosed the cause.
Worked example. Say filtering to the slowest 1% of requests shows they're disproportionately concentrated on one specific API key, and further digging shows that customer's requests average 50 times larger payloads than typical traffic (large batch uploads versus the typical small requests). The service's payload-parsing step is O(n) in a way that's invisible at typical sizes but adds real milliseconds at 50x the size, and if that customer represents roughly 1 to 2% of total request volume, that lines up closely with a p99 (but not median) impact. The mitigation is either optimizing the parsing step's complexity or, as a faster stopgap, routing especially large payloads to a separate processing path so they don't compete for the same resources as typical small requests.
Trade-offs and pitfalls. The trap here is trusting a healthy-looking median or average as evidence nothing is wrong; those aggregates are exactly the metric that HIDES this class of problem by construction, since a small fraction of slow requests barely moves the median at all. It's also worth being skeptical of 'it's probably just noise' when the tail is consistently bad rather than randomly bad: consistent tail badness usually has a specific, findable cause, while genuinely random noise (network jitter, for example) tends to look different in the data.
Your telemetry shows a sharp increase in intermittent 429 rate-limit responses right after a partner integration launched. As the engineer owning the API, describe how you'd diagnose the problem, including how you'd coordinate with the partner to isolate whether the cause is on your side or theirs, and the short-term mitigations you'd apply while a fix is worked out.
Sample Answer
Direct answer. A rate-limit spike right after a partner integration launch is a strong, specific correlation, but confirming it (rather than just assuming it) and figuring out which side needs to change is the actual diagnostic work, since 'their new integration' could mean several different things technically.
Structured elaboration.
- Confirm the correlation precisely. Check whether the 429 spike is genuinely concentrated on requests from the partner's integration specifically (by API key, client ID, or whatever identifies their traffic) versus a broader, coincidental increase across all callers; this rules out a red herring where the timing matches but the actual traffic source doesn't.
- Rule out a cause on your own side, not just theirs. Before treating the partner's traffic as the mechanism, check whether anything changed on YOUR side around the same time: a recent deploy that altered a rate limiter's default thresholds or a shared config value, a pooled or shared quota bucket that unrelated traffic is also drawing from (so the partner's launch is coincidental timing, not the actual cause), or a bug in how the rate limiter attributes or counts requests that's inflating the count for otherwise-normal traffic. This is what the coordination step below is actually for: if the partner's measured rate turns out to be well within your own documented limits once you look closely, that combination, correlated timing but no real limit violation, is itself the strongest signal the cause is on your side, not theirs.
- Understand the partner's actual request pattern. Is their traffic volume simply higher than expected (a legitimate but larger-than-anticipated load), or is their pattern inefficient in a specific way (polling far more frequently than necessary, not respecting a documented rate limit, retrying aggressively on their own 429 responses in a way that compounds the problem, or making redundant calls that could be batched or cached on their end)? Reviewing their actual call patterns (timing, frequency, whether calls are cacheable but aren't being cached) answers this.
- Coordinate with the partner using specific evidence. Share concrete data: their actual request rate against your documented limits, the specific pattern (if any) that's inefficient, and what a compliant integration would look like; specific, evidence-backed feedback is far more actionable for them than a general 'you're causing errors' report.
- Short-term mitigations while a proper fix is worked out. If their volume is legitimate but exceeds current limits, a temporary limit increase (if your infrastructure can absorb it safely) avoids blocking their real business need while a more durable fix is designed. If their pattern is inefficient, helping them add basic client-side caching or reduce polling frequency, even as a stopgap before a deeper fix, reduces load quickly without your side needing to change anything.
Worked example. Suppose reviewing the partner's call pattern shows they're polling a specific endpoint every 2 seconds per user session to check for status updates, when the underlying data they're polling for typically only changes every few minutes; that's a classic inefficient-polling pattern, not a legitimate high-volume need. Recommending they either reduce polling frequency to something more proportional to how often the data actually changes, or better yet, move to a webhook or push-notification mechanism if your API offers one, would cut their request volume dramatically (potentially by 90% or more, if polling every 2 seconds versus needing an update roughly every few minutes) without requiring any change to your own rate limits or infrastructure at all.
Trade-offs and pitfalls. Simply raising rate limits for a partner without first understanding WHY they're hitting them treats the symptom, not the cause, and can mean you're now absorbing genuinely wasteful traffic indefinitely, which costs you infrastructure capacity for no real business benefit on either side. It's also worth building this kind of check (comparing new-integration traffic patterns against expected, efficient usage) into your partner-onboarding process going forward, so an inefficient pattern like aggressive polling is caught and corrected before launch rather than discovered from a live 429 spike after the fact.
You suspect an intermittent memory leak in a distributed service, manifesting as a slow increase in restarts over several weeks. Outline an incident-investigation plan: the immediate mitigations, the data you'd collect (metrics, traces, heap dumps), the instrumentation you'd add, and how you would perform the root-cause analysis without disrupting production.
Sample Answer
Direct answer. A memory leak that shows up as restarts creeping up over weeks needs an investigation on that same timescale: the goal is to find what's accumulating and never getting released, using data collected over the leak's actual lifetime, not a single snapshot.
Structured elaboration.
- Immediate mitigations. Scheduled or threshold-triggered restarts (restarting an instance once its memory crosses a safe ceiling, before it OOMs (runs out of memory) and restarts uncontrolled) buy time without fixing anything, and are a reasonable stopgap while you investigate, since an OOM kill mid-request is worse for users than a controlled, load-balanced restart.
- Data to collect. A memory-usage-over-time metric per instance, ideally with enough resolution and history to see the actual growth RATE and whether it's linear (suggesting something growing proportional to time or request count) or has a different shape. Periodic heap dumps or heap-profiling snapshots (not just at the moment of failure, but at intervals across the leak's timeline) let you compare what's IN memory at, say, hour 1 versus hour 100 and see what grew. Traces or logs correlating memory growth against specific request types or code paths, if the leak is triggered by a subset of traffic rather than uniformly.
- Instrumentation to add if not already present. Object-count or memory-usage metrics broken down by type or subsystem (many runtimes support this, at some overhead), so you're not limited to a single aggregate number when trying to localize where growth is happening. A way to trigger an on-demand heap snapshot without a full restart, so you can capture state right when memory looks unusually high rather than waiting for a scheduled dump.
- Root-cause analysis without disrupting production. Compare heap snapshots from two points in time (early versus late in an instance's uptime) and look specifically for object types or data structures whose COUNT grew disproportionately relative to request volume over the same window; this is usually the fastest way to localize a leak to a specific subsystem or cache. Once localized, review that code path specifically for an unbounded collection (a cache with no eviction, a list that's appended to but never trimmed, a subscription or listener that's registered but never unregistered) since these are the most common leak patterns in long-running services.
- Validate the fix on the same timescale as the leak. Because this leak takes weeks to become visible, confirming a fix worked needs a similarly long observation window (watching the memory-over-time trend stay flat for a comparable period), not just a quick sanity check right after deploying the fix.
Worked example. Suppose comparing heap snapshots from early and late in an instance's lifetime shows one specific object type, a per-request context object, growing in count roughly linearly with total requests served, while other object types stay roughly constant. Tracing that object type's lifecycle in the code reveals it's stored in a dictionary keyed by request ID for the duration of the request, intended to be removed once the request completes, but a specific error path (triggered by a small fraction of requests) returns early without reaching the cleanup code, leaving that entry in the dictionary forever. If that error path fires on, say, 0.1% of requests, and the service handles a few million requests per week, that's still thousands of leaked entries per week, small enough per-request to be invisible immediately but large enough in aggregate to explain a multi-week creep toward OOM. The fix is moving the cleanup into a finally-equivalent block (or whatever the language's guaranteed-cleanup construct is) so it runs regardless of which path the request takes, and validating means confirming the dictionary's size metric (once added) stays flat under normal error-path traffic instead of growing without bound.
Trade-offs and pitfalls. A single heap snapshot at the moment of failure is much less useful than a COMPARISON between two points in time, because a snapshot alone shows you what's in memory but not what's actually GROWING; always try to capture at least two, meaningfully separated in time. It's also worth being suspicious of any code path that stores something keyed by a per-request identifier without an equally certain, guaranteed removal path, since that pattern is one of the most common sources of exactly this class of leak.
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.
Unlock Full Question Bank
Get access to all 32 Production Incident Diagnosis and Distributed Systems Troubleshooting interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.