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.
During a migration, an analytics service starts receiving inconsistent data because two different teams are using slightly different versions of an event schema. Outline a concrete plan to detect this kind of schema divergence in production, enforce schema compatibility going forward, and backfill or reconcile the historical inconsistencies it already caused.
Sample Answer
Direct answer. Schema divergence between teams is fundamentally a coordination failure showing up as a data problem, so the fix needs both a technical detection mechanism and an actual compatibility CONTRACT that both teams agree to and can't silently drift away from again.
Structured elaboration.
- Detect the divergence precisely first. Compare the two teams' actual event schemas field by field: which fields exist in one but not the other, which fields exist in both but with different types or semantics (the more dangerous case, since it doesn't fail loudly, it just silently produces wrong values), and since when the two schemas diverged, by checking version history or deploy timestamps for each team's event-producing code.
- Enforce schema compatibility going forward. A schema registry (a central, versioned definition of the event shape that both teams' producers validate against before publishing) is the standard mechanism: it rejects or flags an incompatible change at publish time rather than letting it silently ship and diverge. Compatibility rules (for example, requiring new fields to be optional with sane defaults, and disallowing changing an existing field's type) need to be explicit and enforced automatically, not just documented and hoped for.
- Backfill or reconcile historical inconsistencies. For data already affected by the divergence, decide per-field whether it can be safely reconciled (if one team's version is clearly authoritative or a mapping between the two versions is well-defined) or whether the affected historical range needs to be flagged as lower-confidence rather than silently 'fixed' with a guess. A full backfill re-deriving the field from a still-available raw source is preferable to guessing when it's available.
- Establish ongoing ownership and process, since a schema registry alone doesn't prevent a NEW kind of divergence if there's no clear owner for the shared schema: designate who approves changes to the shared event schema, and require any change proposal to go through that owner rather than either team unilaterally deciding its version is correct.
Worked example. Suppose comparing the two teams' schemas shows both have a field called status, but one team's producer emits it as one of three string values (active, inactive, pending) while the other's emits it as a boolean (true/false), a genuine type-level divergence that likely happened when one team's field was originally boolean and the other team, building a similar-but-separate producer later, modeled the same concept differently without realizing a shared consumer expected consistency. Since the underlying business concept genuinely differs in expressiveness (three states versus two), simply mapping boolean-to-string automatically for the historical boolean data would lose information (true could map to active OR pending); the honest fix for historical data is flagging the boolean-sourced rows as only reliably distinguishing active-vs-not, without pretending to recover the active/pending distinction retroactively, while going forward, a schema-registry rule requiring both producers to emit the three-value string form closes the gap.
Trade-offs and pitfalls. It's tempting to solve type-level divergence with an automatic mapping to make the data LOOK consistent quickly, but as the boolean-versus-three-state example shows, an automatic mapping can silently manufacture false precision (pretending you know 'pending' when you only ever had 'true'); being honest about what can and can't be recovered is more valuable than a superficially clean-looking dataset that's actually wrong. A schema registry also only helps if BOTH teams' pipelines actually validate against it before publishing; adding the registry without also gating publishing on it leaves the same silent-drift risk in place.
A schema migration is suspected to have caused silent data regressions. Design an investigative approach to prove causality: automated sampling strategies, aggregate- and row-level diffs using checksums, a deploy-bisection process to identify which deploy actually introduced the regression, and safeguards to prevent this class of regression in the future.
Sample Answer
Direct answer. Proving that a specific migration caused a data regression, rather than merely suspecting it, means finding the exact point where the data diverges from correct and confirming that point lines up with the migration's deployment, not just noticing that both happened around the same general time.
Structured elaboration.
- Define 'correct' precisely before comparing anything. Establish an authoritative source or a derivable expected value for the affected data (a recomputation from raw inputs, a comparison against a system that wasn't touched by the migration, or a snapshot from before the migration ran) so you have something concrete to diff against, not just a vague sense that something looks off.
- Run aggregate- and row-level diffs using checksums. An aggregate checksum (a hash or sum over a whole table or partition) tells you quickly whether TWO points in time or two sources agree at all; if they don't, row-level checksums (hashing each row's relevant fields) let you isolate exactly which rows differ, which is far more efficient than comparing every field of every row directly at scale.
- Use automated sampling to find the boundary, not just confirm a suspicion. Rather than comparing the entire dataset, sample rows across the time range spanning before and after the suspected migration, checking each sample's correctness against your reference from step 1; this narrows down not just WHETHER there's a regression but WHEN it starts, precisely.
- Deploy-binary-search to confirm causality specifically. If several deploys happened in the suspected window, checking correctness immediately before and after EACH deploy (not just before and after the whole window) isolates which specific deploy is responsible, the same logic as a code bisection but applied to data correctness checkpoints instead of a test suite.
- Safeguards to prevent recurrence. Adding an automated, ongoing checksum or row-count comparison specifically around future schema migrations (comparing a sample of migrated rows against their pre-migration values via a documented, checkable transformation rule) would catch this class of regression at migration time rather than requiring a manual investigation days or weeks later.
Worked example. Suppose you're checking whether a schema migration that changed how a total_amount field is computed introduced a systematic error. You compute an independent, from-raw-inputs recomputation of total_amount for a sample of orders and compare it against the stored value; below is an illustrative version of that comparison, checked for logical correctness (not run against real production data, since none is available here, but confirmed to execute and produce the expected result against a small synthetic case with a known correct answer):
# illustrative row-level regression check: recomputed vs stored value
def recompute_total(line_items, tax_rate, discount):
subtotal = sum(item['price'] * item['qty'] for item in line_items)
return round(subtotal * (1 + tax_rate) - discount, 2)
# a synthetic order with a KNOWN correct total, computed by hand:
# subtotal = 2*10.00 + 1*5.00 = 25.00; with 8% tax = 27.00; minus 2.00 discount = 25.00
order = {
'line_items': [{'price': 10.00, 'qty': 2}, {'price': 5.00, 'qty': 1}],
'tax_rate': 0.08,
'discount': 2.00,
'stored_total': 27.00, # what the migrated system actually stored
}
expected = recompute_total(order['line_items'], order['tax_rate'], order['discount'])
print(f"recomputed={expected}, stored={order['stored_total']}, match={expected == order['stored_total']}")
Running this: recomputed=25.0, stored=27.0, match=False. In this illustrative case, the migration's stored value (27.00) matches the PRE-discount total, meaning the migrated computation is applying the discount incorrectly (or not at all) somewhere in the pipeline; comparing this same check across a real sample of orders straddling the migration's deploy timestamp, and finding the mismatch rate jumps from 0% before the deploy to consistently nonzero after it, is what would confirm the migration as the cause with actual evidence rather than a hunch.
Trade-offs and pitfalls. A checksum comparing whole-row hashes is fast but only tells you THAT something differs, not WHAT or WHY; row-level, field-level comparison (like the recompute check above) is more expensive but is what actually reveals the mechanism, so budget for both: cheap aggregate checks to find the affected range fast, more expensive row-level checks to understand the mechanism once you've narrowed the range. It's also worth being careful that your 'authoritative' recomputation is itself genuinely correct and independent of the migrated logic, since a recomputation that shares a bug with the migration would falsely confirm the migration's (wrong) output as correct.
A critical production pipeline shows silent data loss between stages: some events go missing downstream with no obvious errors, and in one recent case the affected job had actually failed silently and kept running for two days before anyone noticed. As the on-call data engineer, provide a step-by-step incident response: your immediate mitigations, how you'd collect evidence to determine the true extent of the loss or corruption, your root-cause-analysis approach, and the long-term prevention you'd put in place (instrumentation, data contracts, reconciliation jobs).
Sample Answer
Direct answer. When data goes missing silently, with no errors and no alerts firing, the first job is not root-causing yet: it's establishing exactly how much data is affected and for how long, because that scope determines both your mitigation urgency and what a correct fix even needs to repair.
Structured elaboration.
- Establish scope before cause. Compare an authoritative count or checksum at the start of the pipeline against the same count or checksum at the end, for a range of recent time windows, to find exactly when the loss started and how much has been lost. Without this, you can't tell stakeholders how bad it is, and you risk fixing the code but missing that a specific batch or partition also needs to be reprocessed.
- Contain first, then investigate. If the pipeline is still running and still losing data, the immediate priority is stopping further loss: this might mean pausing the pipeline stage that's dropping events, or routing new events to a location where they're safe (a raw, unprocessed store) even if you haven't fixed the processing yet.
- Walk the pipeline stage by stage. With no obvious error, the loss is likely happening in a place that fails silently: a filter or transform step that drops records that don't match an expected shape without logging them, a deduplication step that's over-aggressive and treats distinct events as duplicates, an at-most-once delivery mechanism (the message is sent once and never retried, so if it's lost in transit it's simply gone, unlike at-least-once delivery which keeps retrying until acknowledged) that occasionally drops a message under backpressure (a signal from an overwhelmed downstream stage telling upstream to slow down or drop work rather than queue it indefinitely), or a downstream write that silently no-ops on a conflict instead of erroring. Check each stage's input count against its output count to localize which stage is where records disappear.
- Reconstruct the two-day case specifically. For an incident that ran silently for an extended period, check whether any monitoring existed at all for input-versus-output counts at each stage; if not, that absence of monitoring is itself part of the root cause, since a real bug that WOULD have been caught in minutes with the right check instead ran undetected for days.
- Recover and prevent recurrence. Once the losing stage is found, recovery usually means reprocessing the affected window from a raw or replayable source if one exists; if there is no way to recover the missing data, that gap needs to be communicated honestly to whoever consumes this data downstream. Prevention has three complementary parts: instrumentation (emitting an explicit count or checksum metric at every stage boundary, not just logging on error, so a silent drop shows up on a dashboard instead of requiring someone to notice); data contracts (an explicit, enforced agreement on what shape and volume of data each stage should produce, so a stage that silently changes behavior violates a checkable contract rather than drifting unnoticed); and scheduled reconciliation jobs that periodically re-verify end-to-end counts independently of the pipeline's own reporting, catching a class of bug where the pipeline's own instrumentation is itself the thing that's wrong.
Worked example. Say a checksum comparison finds the pipeline's ingestion stage received 100,000 events per hour throughout the affected window, but a stage-by-stage count shows only 94,000 events per hour reaching the final sink, a stable roughly 6% loss rather than a spike. Checking each intermediate stage's input-versus-output count in turn shows the drop happens entirely at a deduplication step, whose input and output counts differ by exactly the missing 6%. Looking at that step's logic reveals it dedupes on a composite key that, for a specific event type, isn't actually unique across different real events, so it's discarding legitimate events it mistakes for duplicates. Because the loss was steady rather than a sudden failure, it evaded any anomaly-style alerting that watches for sudden drops, which is exactly why an ongoing count-reconciliation check (rather than only alerting on sudden changes) is the prevention that would have caught it on day one instead of day two.
Trade-offs and pitfalls. The biggest pitfall is jumping straight to a fix before establishing scope: without the before/after counts, you don't know if you're looking at a total outage of one stage or a steady small leak, and those need very different urgency and different recovery plans. The other common mistake is fixing the code bug and declaring victory without checking whether the ALREADY-LOST data during the incident window can be recovered or backfilled from a raw source; a correct fix going forward doesn't repair the historical gap on its own.
A streaming consumer began lagging during bursts of traffic. Walk through your diagnostic process to determine whether the bottleneck was network I/O, CPU, garbage collection, serialization, disk, or downstream backpressure. Describe the specific tools and metrics you'd use and the mitigations that would reduce lag under peak load.
Sample Answer
Direct answer. With six plausible layers (network I/O, CPU, GC, serialization, disk, downstream backpressure) to check, the efficient approach is to look at where the consumer is actually SPENDING its time first, rather than checking each layer in an arbitrary order.
Structured elaboration.
- Start with the consumer's own resource metrics, since they're usually already collected: CPU utilization (pegged CPU points toward compute-bound work like deserialization or business logic; low CPU with high lag points elsewhere), and whether garbage-collection pause time and frequency correlate with the lag increase.
- If CPU and GC look normal, check I/O next. Disk I/O wait time (relevant if the consumer writes to local disk or a local database as part of processing) and network I/O (relevant if the consumer makes outbound calls) both show up as the consumer's threads being blocked waiting, rather than actively computing, which CPU metrics alone won't clearly show; thread-state sampling helps here.
- Check serialization/deserialization cost specifically, since it's an easy layer to overlook: if message size or shape changed recently (a schema change, a new field, larger payloads), deserialization cost per message can increase even though throughput in messages-per-second looks unchanged, which would show up as rising CPU time per message rather than a change in message volume.
- Check downstream backpressure, meaning whether the consumer's OWN calls to something further downstream (a database write, another service) are slow, causing the consumer to spend most of its time waiting on that downstream rather than actually consuming new messages; this asks 'what is the consumer doing with each message once it has it' rather than 'is the broker delivering messages fast enough', so it complements a broker-throughput-focused Kafka investigation with a resource-layer one.
- Correlate against the traffic burst itself. Since this happens specifically during bursts, check whether the bottleneck resource is one that scales with MESSAGE VOLUME (CPU, serialization, downstream calls) versus one that's roughly constant regardless of volume (a fixed disk write latency, for example); a volume-scaling bottleneck explains why it only shows up during bursts, while a constant one would need a different explanation for why it only bites during bursts specifically (perhaps concurrency-related contention that only appears at higher parallelism).
Worked example. Suppose CPU utilization during a burst climbs from a typical 30% to 95%, and profiling shows the majority of that CPU time is inside the message deserialization step. Checking message size shows average payload size grew from about 2KB to roughly 8KB after a recent schema change that added several new fields, a 4x increase; if deserialization cost scales roughly linearly with payload size, a 4x larger payload plausibly explains close to a 4x increase in per-message CPU cost, which would explain why the consumer, previously comfortably keeping up, now saturates CPU and falls behind specifically once burst volume pushes total processing demand past its now-lower effective throughput ceiling. The fix path is either optimizing the deserialization step for the new, larger payload shape, or scaling out consumer parallelism to compensate for the higher per-message cost.
Trade-offs and pitfalls. It's tempting to jump straight to 'add more consumers' whenever lag appears, but if the bottleneck is genuinely CPU-per-message (as in this example), adding consumers does help by adding more CPU in aggregate, yet it doesn't address the underlying inefficiency, and the same problem will resurface at a higher volume threshold later; fixing the deserialization cost directly is the more durable answer even if scaling out is the faster immediate mitigation. Also be careful not to conflate 'consumer CPU is high' with 'consumer is the bottleneck' without checking: high CPU during high THROUGHPUT can simply mean the consumer is working hard and keeping up just fine, so always tie the resource metric back to whether lag is ACTUALLY growing, not just whether a resource number looks high.
A customer reports periodic data corruption in a distributed SQL database used by multiple teams. During your investigation you have access to the schema, the replication topology, and recent query patterns. Explain a structured root-cause-analysis approach: which metrics, tests, and experiments you would run, and how you would isolate the contributing factors from each other.
Sample Answer
Direct answer. Periodic, hard-to-pin-down corruption in a multi-team shared database calls for isolating contributing factors systematically, since 'periodic' suggests a trigger condition rather than a constant bug, and 'multiple teams' means the cause could originate from any of several independent write paths.
Structured elaboration.
- Characterize the corruption precisely first. What exactly is wrong: are values incorrect, are rows missing or duplicated, are relationships between tables violated? The SHAPE of the corruption narrows the search dramatically; a duplicated-row pattern points toward replication or retry logic, while an incorrect-value pattern points more toward application logic or a race condition in a read-modify-write sequence.
- Look for periodicity in the corruption itself, not just assume it's random: does it correlate with a specific time of day, a specific batch job, a specific team's deploy schedule, or a specific load pattern? A pattern that recurs at the same time daily strongly suggests a scheduled job as the trigger, versus a pattern tied to load suggesting a race condition that only manifests under concurrency.
- Check the replication topology for a role in the mechanism, since it was explicitly available: if corruption appears differently on different replicas, or specifically on read replicas but not the primary (or vice versa), that points at a replication-specific bug (a replication lag interacting with an application's read-then-write logic, for example) rather than a bug that would corrupt the primary directly.
- Correlate against recent query patterns from EACH team, since multiple teams write to this database: look for any team's queries that involve a read-modify-write sequence without proper locking or optimistic-concurrency checks, since that's a classic source of intermittent corruption that only manifests when two writes race closely enough in time, which explains both the periodicity (needs a specific timing collision) and the multi-team angle (any team's racy write pattern is a candidate).
- Design experiments to confirm, not just infer. Once you have a specific hypothesis (say, team B's batch job races with team A's real-time writes during a specific window), a targeted test that deliberately reproduces that timing collision in a non-production environment can confirm the mechanism definitively before you commit to a fix.
Worked example. Suppose the corruption pattern is specifically duplicate rows with slightly different values in a subset of fields, and it correlates closely with a nightly batch job's run window. Investigating that batch job's logic shows it reads a row, computes an update based on current state, and writes it back, all without an optimistic-concurrency check (no version column comparison) or a row-level lock; if a real-time write from the application happens to land in the narrow window between the batch job's read and its write, the batch job's write can silently overwrite the real-time write's change based on now-stale data it read moments earlier, and depending on the exact field overlap, this can look like a duplicate or a partially-reverted row. The fix is adding an optimistic-concurrency check (reject the batch job's write if the row has changed since it was read, and retry the read-modify-write cycle) or moving the batch update to use an atomic, single-statement update rather than a separate read-then-write, either of which removes the race window entirely.
Trade-offs and pitfalls. It's easy to blame 'the database' or 'replication' generically when the actual root cause is an application-level race condition in how one specific job reads and writes data; walking through each team's actual query PATTERNS, not just infrastructure-level metrics, is often what actually finds this class of bug. It's also worth checking whether the SAME read-modify-write-without-locking pattern exists in other jobs or services touching this database, since a fix that only patches the one job found here leaves the same latent bug available to reappear from a different write path.
Unlock Full Question Bank
Get access to all 10 Production Incident Diagnosis and Distributed Systems Troubleshooting interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.