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 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.
After an emergency multi-region failover, a streaming pipeline is emitting duplicate messages to downstream systems. Design an investigation plan to determine whether the duplicates are caused by producer retries, cross-region replication semantics, or consumer reprocessing, and propose concrete remediation steps to restore correctness while avoiding further data loss.
Sample Answer
Direct answer. Duplicates after a failover have three distinct plausible sources (producer retries, cross-region replication semantics, consumer reprocessing), and each leaves a different fingerprint in the message metadata, so the investigation should read that metadata carefully before assuming which one it is.
Structured elaboration.
- Check for a duplicate-detection key in the messages themselves. If messages carry a unique ID (an idempotency key, a UUID assigned at creation), compare the duplicated messages' IDs: if they're EXACT duplicates (same ID appearing twice), that points at either producer retry (the same logical message sent twice) or consumer reprocessing (the same message read and processed twice); if they're DIFFERENT IDs but represent the same underlying event, that points more at the producer generating a genuinely new message for what should have been a single event, possibly because it lost track of what it already sent during the failover.
- Check producer-side retry behavior around the failover window specifically. A producer that doesn't get a clear acknowledgment before a failover (because the acknowledgment itself was lost in the transition) may reasonably retry, not knowing whether its original send succeeded; this is a common and often unavoidable source of duplicates unless the messaging system provides exactly-once semantics or the consumer is built to dedupe.
- Check cross-region replication configuration. If messages are replicated between regions as part of the failover mechanism, verify whether that replication has its own at-least-once semantics that could reprocess or re-deliver messages already delivered before the failover; some replication mechanisms replay from an earlier offset or checkpoint during a failover specifically to avoid losing anything, which trades a small risk of duplication for a guarantee against loss.
- Check consumer offset/checkpoint behavior around the failover. If the consumer's own tracked position (its checkpoint of what it's already processed) wasn't itself failed-over correctly, or reverted to an earlier point during the transition, the consumer could genuinely reprocess a range of messages it already handled once.
- Remediation. If the mechanism is upstream retry or replication behavior that's difficult to eliminate entirely, idempotent processing on the consumer side (using the message's unique ID to detect and skip a message it's already processed) is usually the more robust fix than trying to guarantee exactly-once delivery end to end, since delivery guarantees are hard to make airtight across a failover specifically.
Worked example. Suppose duplicated messages carry an identical message ID both times, and checking the consumer's checkpoint history shows its tracked offset reverted to a point about 90 seconds earlier than where it had actually gotten to, right at the moment of failover. That points squarely at consumer-side checkpoint reversion, not producer retries or replication: the consumer's failover mechanism restored an earlier, stale checkpoint rather than its true last-processed position, causing it to legitimately reprocess everything from that earlier point forward, which is why the IDs match exactly. The fix is ensuring the checkpoint itself is failed-over correctly (replicated synchronously enough, or recovered from a source that reflects the true last-processed position, not a periodic but stale snapshot), and adding idempotent processing as a second line of defense so even a future checkpoint-reversion event doesn't produce user-visible duplicate side effects.
Trade-offs and pitfalls. It's easy to assume 'duplicates after a failover' automatically means the messaging layer failed at exactly-once delivery, but as this example shows, the actual cause is often specific to how failover itself handles state (like a checkpoint) rather than the steady-state delivery mechanism; check the failover-specific behavior, not just the ordinary-operation delivery guarantees. Idempotent consumer processing is a robust general defense, but it does add real cost (storing and checking processed-ID state) and complexity, so it's worth reserving the strictest, most expensive form of it for operations where a duplicate side effect would actually be harmful (a payment, for example) versus operations where a duplicate is merely wasteful but harmless.
An hourly ETL job failed mid-run, leaving partially written partitions for several recent hours, and downstream consumers expect stable reads. Describe a safe recovery sequence: how you'd identify the affected partitions, snapshot the current state, safely delete or mark the partial partitions, re-run ingestion, and validate the result. Mention the SQL patterns or transactions you'd use to preserve atomicity where the storage layer supports it.
Sample Answer
Direct answer. A mid-run ETL failure leaving partial partitions is a data-integrity problem before it's anything else, so the recovery sequence has to be careful about ORDER: identify the exact damage, make it safe (don't let readers see it, don't lose the ability to undo), then repair, in that order.
Structured elaboration.
- Identify the affected partitions precisely. Compare the job's expected output (which partitions it was supposed to write, based on its schedule and input range) against what actually exists and its row counts or checksums; a partition with a suspiciously low row count, or one entirely missing, is your candidate set. Check the job's own logs for the exact point of failure to corroborate which partitions were mid-write versus genuinely untouched.
- Snapshot current state before touching anything, so you have a rollback point if your recovery sequence itself goes wrong; this is cheap insurance relative to the cost of compounding a partial-write incident with a botched recovery.
- Make the partial partitions safe for downstream consumers immediately. If downstream reads happen from the same partitions the failed job wrote to, either mark those specific partitions as unavailable/invalid (so readers get a clear 'not ready' signal rather than silently reading incomplete data) or, if the storage layer supports it, use an atomic swap pattern (write to a staging location, then atomically publish only once complete) so partial writes are simply never visible to readers in the first place, which is the more robust long-term pattern.
- Safely delete or mark the partial partitions, informed by step 1's precise identification, not a broad guess at which partitions might be affected.
- Re-run ingestion for exactly the affected range, not the whole job, to avoid unnecessary reprocessing and unnecessary risk to partitions that were never touched.
- Validate before declaring done. Compare row counts or checksums for the re-run partitions against an independent source of truth (an authoritative count or checksum comparison against a trusted source, the same kind of evidence used in any cross-system consistency check) to confirm the recovery is actually complete, not just that the job exited without an error this time.
Worked example. Say the job failed while writing partition 2026-07-20T14, and comparing that partition's row count (12,000) against the typical count for that hour (roughly 85,000, based on the surrounding hours) confirms it's a partial write, not a genuinely low-volume hour. Where the storage layer supports transactional writes at the partition level, the safest sequence is: write the re-run's output to a new, separate location, run the count/checksum validation against that new output BEFORE swapping it in, and only then atomically replace the partial partition with the validated, complete one, which never exposes readers to an intermediate, still-partial state during the fix itself, illustrated here for a Postgres-style target table using a swap pattern:
-- illustrative safe-swap pattern for a partitioned table
BEGIN;
-- new, validated data already loaded into staging_2026_07_20_14
ALTER TABLE events DETACH PARTITION events_2026_07_20_14;
-- DETACH does not drop the old partition, it stays present as a standalone table under its
-- original name, so that name must be freed up before staging can take it
ALTER TABLE events_2026_07_20_14 RENAME TO events_2026_07_20_14_old;
ALTER TABLE staging_2026_07_20_14 RENAME TO events_2026_07_20_14;
ALTER TABLE events ATTACH PARTITION events_2026_07_20_14
FOR VALUES FROM ('2026-07-20 14:00') TO ('2026-07-20 15:00');
COMMIT;
-- once the swap is confirmed correct (row counts/checksums match), drop the retained old partial data:
-- DROP TABLE events_2026_07_20_14_old;
The whole swap happens inside one transaction, so readers see either the old (partial) partition or the new (complete) one, never a state where the partition is half-replaced; the detached old partition is renamed rather than dropped immediately, so it stays available as a rollback/audit artifact until the swap is independently confirmed correct.
Trade-offs and pitfalls. Deleting the partial partition BEFORE the re-run has successfully produced and validated its replacement is a common and risky shortcut: if the re-run also fails, you've now made things worse (no data at all, rather than partial data) instead of better; always have the new, complete data validated and ready before removing the old, partial version. It's also worth checking WHY the job failed mid-run in the first place (a transient issue that a retry alone fixes, versus a systematic problem that will cause the exact same partial-write failure again) before considering the incident closed.
A distributed job-processing system suffers cascading failures under peak load. Create a diagnostic approach to find the bottleneck (queuing, thread pools, garbage collection, or downstream latency), then propose immediate mitigations (rate-limiting, load shedding, backpressure) and longer-term architectural fixes (partitioning, horizontal scaling, async decoupling). Discuss the trade-offs of each.
Sample Answer
Direct answer. In a distributed job-processing system under peak load, cascading failure usually starts at ONE bottleneck (queuing, thread pools, GC, or a slow downstream) and spreads because upstream stages keep pushing work at a rate the bottlenecked stage can no longer absorb, so the diagnostic job is finding that first bottleneck before the whole system look uniformly unhealthy.
Structured elaboration.
- Find where work is actually piling up, not just where things look broken. Check queue depths (or backlog size) at each stage of the pipeline; the stage with a rapidly growing queue, while stages before it look normal, is usually the actual bottleneck, and stages AFTER it may look artificially healthy simply because they're starved of work, not because they're fine.
- For the identified bottleneck stage, check the specific resource. Thread-pool saturation shows as a growing count of queued tasks with active threads pinned at the pool's max; GC pressure shows as pause time and frequency correlating with the slowdown; downstream latency shows as the stage's own outbound calls taking longer than normal.
- Distinguish 'genuinely more work than capacity' from 'a specific slow operation'. If EVERY unit of work through the bottleneck stage got proportionally slower, suspect a shared resource under contention (GC, a lock, a saturated connection pool). If most work is fine but a subset is disproportionately slow, suspect a specific bad input or a specific downstream call that only some jobs make.
- Immediate mitigations, matched to what you find. Rate-limiting or shedding load at the entry point buys time without fixing the root cause but prevents things from getting worse while you investigate. Backpressure (having upstream stages slow down when downstream stages are behind, rather than blindly pushing more work at them) is a more targeted immediate response that specifically prevents the pileup from cascading further upstream.
- Long-term architectural fixes. Partitioning (splitting work into independent shards so one hot partition's overload doesn't affect others), horizontal scaling of the specific bottlenecked stage, and async decoupling (using a durable queue between stages so a slow downstream stage doesn't block the upstream stage's own throughput) each address a different aspect: partitioning limits blast radius, scaling adds raw capacity, decoupling prevents a slow stage from propagating its slowness backward through the pipeline.
Worked example. Suppose queue-depth metrics show stage 3 of a 5-stage pipeline growing from a typical few hundred queued jobs to over 50,000 during peak load, while stages 1 and 2 look normal and stages 4 and 5 are nearly idle (starved, waiting on stage 3's output). Checking stage 3's thread pool shows it's pinned at its configured maximum of 20 threads, each one busy, with no GC or lock-contention anomaly. That points at simple capacity: stage 3's throughput ceiling (roughly bounded by 20 concurrent workers times their average per-job time) is below the rate work arrives at during peak load. If each job in stage 3 takes about 2 seconds, 20 threads give a ceiling of roughly 10 jobs per second; if peak load is pushing closer to 25 jobs per second into stage 3, that 2.5x gap explains the queue growing without bound during the peak window. The mitigation is either raising stage 3's concurrency (if the underlying resource, likely a downstream call or database, can handle more concurrent load) or partitioning stage 3's work so multiple independent worker pools each handle a slice, rather than one pool being the single ceiling for all traffic.
Trade-offs and pitfalls. Backpressure and rate-limiting both trade a clean failure (rejecting or delaying some work explicitly) for the alternative of an uncontrolled pileup that eventually causes a messier failure (memory exhaustion from unbounded queues, or cascading timeouts); that trade is usually worth it, but stakeholders need to understand that 'load shedding' means some work is deliberately deferred or dropped, not silently absorbed. Partitioning also has a real cost: if partitioning is done on a key that's unevenly distributed (a few very active tenants, for example), you can end up with the exact same bottleneck concentrated on one partition instead of solved. Horizontal scaling has its own trade-off: it's usually the fastest lever when the bottlenecked stage is stateless and genuinely just short on capacity, but it doesn't help, and can even mask, a real algorithmic or lock-contention problem, and it costs real money to run extra capacity that's only needed at peak. Async decoupling trades a simpler, synchronous call chain for a durable queue between stages, which absorbs bursts and stops a slow downstream from blocking upstream throughput, but it adds real operational complexity (the queue itself now needs its own monitoring and capacity planning) and can turn a request that used to fail fast into one that succeeds eventually with a much longer and less predictable end-to-end latency, which is not free for every kind of job.
You detect inconsistent aggregates between an OLAP analytics system and the OLTP transactional system it's derived from, suggesting missing events. Provide a methodical approach to identify whether the problem originated at the source (the event producer), in streaming ingestion, in batch processing, or in the query and reporting layer. Include the kinds of SQL queries, checksum strategies, and log or offset checks you would use.
Sample Answer
Direct answer. Missing events between an OLTP system and its OLAP analytics need a layer-by-layer walk from source to report, because the same symptom (an aggregate mismatch) can originate at any of four different layers, each needing a different kind of check to confirm or rule out.
Structured elaboration.
- Establish the actual gap precisely. Compute an authoritative count (or a checksum over key fields) directly against the OLTP source for a specific time window, and compare it against the same window's count in the OLAP system; this tells you the true size of the discrepancy and the window it's concentrated in, which narrows everything that follows.
- Check the source (event producer) layer. Does the producer actually emit an event for every relevant OLTP write? A producer that's conditionally triggered (only fires on certain code paths, or misses events during a specific kind of transaction like an update-without-a-corresponding-insert) would show a genuine gap between OLTP writes and events emitted, checkable by comparing OLTP row counts against emitted-event counts directly.
- Check the streaming-ingestion layer. If events are emitted correctly but some don't make it through ingestion, check for consumer lag, dropped messages, or a partition/routing issue that could silently drop events, the same categories of check as an ordinary streaming-lag investigation.
- Check the batch-processing layer. If this pipeline includes a batch step (deduplication, transformation, aggregation), check whether that step's input count matches its output count for the affected window; an over-aggressive dedup step, in particular, is a common source of events that were successfully ingested but never reached the final table.
- Check the query and reporting layer last. If data all the way through the pipeline is actually complete, the mismatch could be in how the OLAP report itself is queried (a filter that unintentionally excludes some valid rows, a join that drops rows, a materialized view that hasn't refreshed).
- Use checksums and offset checks as the connective tissue between layers. A checksum over a stable key field, computed independently at each layer's input and output, localizes exactly where the count changes; Kafka or similar offset tracking, if used, tells you whether the consumer has actually processed everything that was produced up to a given point, or is behind or has skipped something.
Worked example. Suppose the authoritative comparison finds OLAP is short by roughly 3% of events for the affected window, and comparing OLTP write counts against the producer's emitted-event counts for the same window shows those two numbers MATCH exactly, ruling out the source layer as I demonstrate below with a runnable check. Comparing emitted-event counts against what the streaming consumer actually processed (via consumer-group offset metrics) also matches. That pushes the investigation to batch processing, where comparing the dedup step's input row count against its output row count for the window shows exactly the missing 3%, consistent with the source layer and ingestion layer both being clean and the dedup step being where records disappear, which is a well-known failure shape for dedup logic in general: a dedup step that keys on a composite field which is not actually unique across every real event of a given type will treat some genuinely distinct events as duplicates and silently drop them, exactly like a hash-based cache key collision drops one of two different values. Confirming it here means checking whether the dedup key used by this specific step is genuinely unique for this event type, not assuming the general failure shape automatically applies.
-- illustrative dedup-mismatch check: input rows vs. output rows for one window,
-- run against the same window used for the earlier authoritative count comparison
SELECT
(SELECT COUNT(*) FROM dedup_step_input WHERE event_time BETWEEN :window_start AND :window_end) AS input_rows,
(SELECT COUNT(*) FROM dedup_step_output WHERE event_time BETWEEN :window_start AND :window_end) AS output_rows;
Running this comparison across several recent windows and confirming the gap consistently matches the dedup step, not the earlier or later layers, is what turns 'a plausible hypothesis' into a confirmed root cause.
Trade-offs and pitfalls. It's tempting to start investigating at whichever layer you're most familiar with, but a systematic layer-by-layer elimination (using count or checksum comparisons at each boundary) is what actually localizes the problem efficiently across four very different technical domains; guessing based on familiarity risks spending a long time investigating the wrong layer. It's also worth checking more than one time window before concluding you've found the cause, since a coincidental one-window match to a hypothesis can be misleading if the real cause is intermittent rather than a stable, per-window percentage.
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.