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 vendor integration suddenly changes its response contract without notice, breaking your clients. Propose an emergency incident response and a longer-term strategy to prevent future vendor-induced breakages, covering contract enforcement, integration testing against the vendor's real API, and the commercial terms you'd push for.
Sample Answer
Direct answer. An unannounced breaking change from a vendor needs an emergency response that treats the vendor as an unreliable dependency for the moment, followed by a longer-term relationship and architecture change that makes the NEXT surprise less damaging.
Structured elaboration.
- Emergency incident response. Confirm the scope of what's actually broken for your clients (which of your features depend on the changed response contract), and apply the fastest safe mitigation: if you can adapt to the new contract quickly (a small parsing or mapping change on your side), that's often faster than waiting for the vendor to revert. If you can't adapt quickly, consider whether you can temporarily fall back to a cached or last-known-good version of whatever data the vendor provides, or gracefully degrade the dependent feature rather than let it fail outright for your own users.
- Communicate with the vendor immediately, both to report the issue (they may not know it's breaking integrators) and to understand their intent (was this deliberate, will they revert, is there a timeline), since that materially changes whether your best move is adapting to their new contract or waiting them out.
- Longer-term: contract enforcement. Push for a formal API contract or SLA with the vendor that includes advance notice for breaking changes, ideally with a defined deprecation window; without this, you're structurally exposed to a repeat of the same surprise.
- Integration testing against the vendor's real API on a schedule, not just at initial integration time: a contract test that runs periodically against the vendor's actual API (not just a mock) would have caught this specific change close to when it happened, rather than only when it caused a live production failure for real users.
- Commercial terms. If this vendor is significant enough to your business, this incident is legitimate leverage to negotiate stronger contractual protections (advance notice requirements, an SLA with remedies for breaking changes without notice) as part of the relationship going forward, not just a technical fix.
Worked example. Suppose the vendor's response contract change turns out to be a field that used to always be present and is now sometimes omitted for a subset of records; if your integration can tolerate treating that field as optional with a sensible default (say, treating a missing status field as 'unknown' rather than crashing), that's a same-day mitigation that doesn't require waiting on the vendor at all. In parallel, adding a scheduled contract test that specifically asserts the shape of the vendor's response (including that this field is present) against their live API on a recurring basis would have caught this exact change within, at most, one test cycle after it shipped, rather than after it caused a customer-facing failure.
Trade-offs and pitfalls. Adapting quickly to a vendor's new contract fixes the immediate problem but can create an awkward situation if the vendor later reverts the change, since your adapted code might then need to handle BOTH the old and new shapes during the transition; keeping the adaptation defensive (tolerant of either shape) rather than assuming the new shape is permanent is a safer default until the vendor confirms their intent. It's also worth being realistic that not every vendor relationship has enough leverage to negotiate a formal advance-notice SLA; where that's not achievable, investing more heavily in your own scheduled contract testing is the fallback that doesn't depend on the vendor's cooperation.
Write a Python script that reads a log file where each line can contain a trace_id like '[trace_id=abc123]' and a log level. The script should print a summary of error counts per trace_id (count lines containing 'ERROR', grouped by trace_id). Example log lines:
2025-01-01T12:00:01Z [trace_id=abc123] ERROR Payment failed user=1234
2025-01-01T12:00:02Z [trace_id=def456] INFO Processed
Provide working Python code.
Sample Answer
Direct answer. The task is a straightforward parse-count-group problem: read each line, pull out the trace_id if present, check whether the line is an ERROR line, and maintain a running count keyed by trace_id.
Structured elaboration.
- Parsing the trace_id. A regular expression matching the literal pattern
[trace_id=...]reliably extracts the ID regardless of what else is on the line, which is more robust than assuming a fixed column position, since log lines can vary in format around the trace_id field. - Detecting an ERROR line. Checking for the literal substring
'ERROR'in the line, as the prompt specifies, is simple and sufficient here; a more defensive version might check it's a distinct token (not part of a longer word) but the given requirement is a straightforward substring/level check. - Aggregating. A dictionary (or
Counter) keyed by trace_id, incremented once per matching ERROR line, naturally produces the required summary; lines without a parseable trace_id should be handled explicitly (skipped, or bucketed under a sentinel key) rather than silently causing an error. - Output. Printing the counts, sorted for readability, gives a clear, deterministic summary regardless of the input's original ordering.
Worked example (executed).
import re
from collections import Counter
def count_errors_by_trace_id(lines):
pattern = re.compile(r"\[trace_id=([^\]]+)\]")
counts = Counter()
for line in lines:
if "ERROR" not in line:
continue
m = pattern.search(line)
if not m:
continue
trace_id = m.group(1)
counts[trace_id] += 1
return counts
sample_lines = [
"2025-01-01T12:00:01Z [trace_id=abc123] ERROR Payment failed user=1234",
"2025-01-01T12:00:02Z [trace_id=def456] INFO Processed",
"2025-01-01T12:00:03Z [trace_id=abc123] ERROR Retry failed user=1234",
"2025-01-01T12:00:04Z [trace_id=ghi789] ERROR Timeout user=5678",
"2025-01-01T12:00:05Z [trace_id=abc123] INFO Retrying",
]
counts = count_errors_by_trace_id(sample_lines)
for trace_id, n in sorted(counts.items()):
print(f"{trace_id}: {n}")
Running this against the sample lines above prints:
abc123: 2
ghi789: 1
def456 correctly never appears in the output, since its only line is INFO, not ERROR, and should not be counted.
Trade-offs and pitfalls. A naive substring check for 'ERROR' would also match a line that happens to contain the word 'ERROR' as part of unrelated text (an error MESSAGE mentioning the word without actually being an error-level log line, in an unusual logging format); if that's a real risk in your actual log format, matching a proper level field (like a leading ERROR token) is more robust, but the given requirement here is the simple substring form. It's also worth deciding explicitly how to handle a line with 'ERROR' but no trace_id (silently skipping it, as this implementation does, versus counting it under an 'unknown' bucket), since in a real incident you'd want to know if a meaningful fraction of your error lines have no trace_id at all, which would itself be a gap worth fixing in your logging.
A production service became partially available after an upstream dependency experienced a network partition: some requests succeed, others hang. Describe a step-by-step investigation and mitigation plan, covering short-term actions to restore consistency and long-term fixes to prevent recurrence, including what telemetry and logs you would examine and what temporary mitigations you might deploy.
Sample Answer
Direct answer. Requests hanging (rather than failing outright) after a network partition is a specific and important clue: it usually means a caller is waiting on a connection or response that will never arrive because the far side is unreachable, not that the far side is actively rejecting requests, so the mitigation has to focus on TIMEOUTS as much as on the partition itself.
Structured elaboration.
- Confirm the partition's scope. Which specific network path or region boundary is affected, and which services or calls cross it? A partition rarely means 'everything is down'; it usually means 'calls crossing this specific boundary are affected', and calls that don't cross it should be unaffected. Concretely, this means checking per-destination error-rate and latency dashboards (segmented by downstream service and region) to see exactly which calls are degraded, connection-pool and thread-pool utilization/saturation metrics to see which resources are being tied up by hung requests, host- and network-level logs (TCP connection resets, SYN timeouts, retransmission counts) to confirm packets genuinely aren't getting through rather than just running slow, and any circuit-breaker or timeout-trip logs that are already firing, since those tell you which calls the system itself has already identified as failing.
- Understand why some requests succeed and others hang. Requests that don't need to cross the partition succeed normally. Requests that DO need to cross it will hang if there's no timeout configured on that call, or fail relatively quickly if there is one; a service with inconsistent timeout configuration across its various outbound calls will show exactly this kind of mixed 'some succeed, some hang' pattern.
- Immediate mitigation. Apply or tighten timeouts on the specific calls that cross the partition, so hanging requests fail fast instead of consuming resources (a connection, a thread, a request slot) indefinitely; a service where every incoming request eventually hangs waiting on an unresponsive downstream can exhaust its own capacity even though ITS code has no bug. If there's a fallback path or cached data available, serving degraded results instead of hanging is often better for users than waiting.
- Longer-term fixes. Ensure every outbound call has an explicit, sane timeout as a standing practice, not just for this partition. Add circuit breakers so that once a downstream is detected as unreachable, subsequent calls fail immediately instead of each one re-attempting the same doomed wait. Consider whether critical paths need a documented fallback behavior for partition scenarios specifically (serve stale data, degrade a feature, queue the write for later) rather than leaving the behavior undefined and discovering it live during an incident.
- Restore consistency once the partition heals. Any writes that were queued, retried, or that partially succeeded during the partition need reconciliation: check for duplicate side effects (from client-side retries during the hang), and confirm read paths are serving current data again, not still routed to a stale fallback.
Worked example. Suppose the partition separates region A from region B, and a payment service in region A calls a fraud-check service in region B with no configured timeout on that specific call. Requests through the normal path (fraud-check reachable) take their usual 30 to 50ms; requests during the partition hang until the underlying TCP connection itself times out, which at the OS level might take 60 to 130 seconds by default, far longer than any reasonable user-facing request should ever wait, and each hung request holds a thread or connection slot the whole time. Adding an explicit 2-second application-level timeout on that specific call (well above its normal 30 to 50ms but far below the OS default) means a partitioned call fails fast, freeing the resource, rather than hanging for over a minute; combined with a circuit breaker, subsequent calls during the same partition would fail immediately without even attempting the call, once the breaker trips.
Trade-offs and pitfalls. Setting timeouts too aggressively can cause false failures during normal, brief latency blips, so the value needs to be based on the call's actual normal latency distribution, not an arbitrary round number. It's also worth checking whether the SAME issue (missing or overly long timeouts on cross-partition calls) exists on other calls beyond the one that caused this specific incident, since a partition is exactly the kind of event that exposes every under-configured timeout across a system at once.
An incompatible change to a widely used API you owned caused client failures in production. As the responsible architect, outline your immediate steps for incident triage: how you'd assess blast radius, communicate with affected clients, and choose a remediation path (rollback, a compatibility shim, or helping clients patch quickly).
Sample Answer
Direct answer. The first priority is scoping and stopping client-facing damage, since every additional minute the incompatible change stays live means more clients are failing in production, and only once that's contained does the choice between rollback, a shim, or helping clients patch actually matter.
Structured elaboration.
- Assess blast radius immediately. Identify which clients are actually calling the changed endpoint or field, and how many are failing versus succeeding; API access logs, error-rate dashboards segmented by client or API key, and any client-reported errors together give you this picture quickly. This also tells you whether the failure is universal (every caller of this field breaks) or partial (only callers using it a specific way).
- Choose a remediation path based on what step 1 shows. A full rollback is fastest and safest when feasible and undoes the incompatibility for everyone at once, but isn't always possible if other changes have already shipped on top of it. A compatibility shim (temporarily supporting both the old and new behavior, detecting which one a given client expects) buys time without a full rollback, at the cost of extra complexity you'll need to remove later. Directly helping specific clients patch is appropriate when the affected set is small and known, and a shim or rollback would be disproportionate effort for the actual scope.
- Communicate with affected clients concretely, not generically: which specific behavior changed, what error they're likely seeing, and what you're doing about it and on what rough timeline. Specific, honest communication reduces the number of support escalations and duplicate investigation on the client side.
- Confirm the chosen remediation actually resolves it, the same way you'd confirm any incident mitigation: watch the client-facing error rate for the affected callers specifically, not just the aggregate API error rate, since aggregate metrics can mask a fix that helped some clients but not others.
Worked example. Suppose access logs show the field's old shape is still being requested by roughly 40 client integrations, of which about 15 are actively failing (the other 25 apparently don't touch that specific field despite calling the endpoint). A full rollback isn't available because a dependent internal change already shipped on top of it. A compatibility shim that detects a version header (or, absent that, infers intent from another field in the request) and serves the OLD shape to clients still expecting it is feasible given the moderate, identifiable scope. Rolling out the shim and watching error rates for those specific 15 previously-failing integrations confirms whether it actually worked, rather than assuming from the aggregate API error rate alone, which might look fine even if a handful of the 15 are still broken in some other way.
Trade-offs and pitfalls. A compatibility shim is a genuinely useful stopgap but has a real ongoing cost (more code paths to maintain and test) if it's left in place indefinitely; treating it explicitly as temporary, with an owner and a removal plan once clients have migrated, prevents it from becoming permanent, invisible debt. It's also worth being honest that 'the aggregate error rate looks fine now' is not the same as 'every affected client is actually fixed'; checking the SPECIFIC previously-affected callers, not just the overall number, is what actually confirms the incident is resolved for everyone who was hit by it.
Users report inconsistent account balances across regions. The system uses eventually consistent replication with conflict-resolution rules. Describe how you'd determine whether this is a bug, replication lag, or correct-but-surprising eventual-consistency behavior. Propose fixes (strong consistency for the critical path, causal guarantees, compensating transactions) and discuss the trade-offs of each.
Sample Answer
Direct answer. Inconsistent balances need a definitive test, not a guess: compare the SAME account's state across regions using the system's own conflict-resolution rules to determine whether the current values are what those rules would actually produce, since only that tells you whether you're looking at a bug or working-as-designed eventual consistency.
Structured elaboration.
- Reconstruct the write history for an affected account. Pull every write (with timestamps and originating region) to that account's balance across all regions during the relevant window. This is the ground truth you'll check the current state against.
- Apply the documented conflict-resolution rule by hand to that history. If the rule is, say, 'last-write-wins by timestamp', manually determine what the final balance SHOULD be according to that rule, and compare it against what each region actually shows. If they match, the system is behaving as designed (which may still be a product or business problem, just not a bug). If they DON'T match, the conflict-resolution logic itself has a bug, which is a much more serious finding since it means the intended consistency model isn't even being honored.
- If it's working as designed, quantify how OFTEN and how LONG the inconsistency window actually is, since 'eventually consistent' as a design still needs bounded windows for something as sensitive as balances; a discrepancy that resolves in under a second is very different from one that persists for minutes.
- If it's a genuine bug in conflict resolution, this needs to be treated with more urgency given the financial stakes: contain further divergence (potentially by pausing writes to the affected accounts or region while you fix the logic), then work out how to reconcile the accounts that already diverged incorrectly.
- Propose fixes matched to the finding. If the system is working as designed but the inconsistency window is unacceptable for balance data specifically, strong consistency for that critical path (even if the rest of the system stays eventually consistent) is a reasonable, scoped fix: not every piece of data needs the same consistency guarantee, and balances are a natural candidate for stronger guarantees than, say, a view count. Causal consistency (ensuring that writes which are actually causally related, such as a user's own prior writes, or a write made after reading another value, are seen in that same order by every reader and every region, while writes that are genuinely unrelated and concurrent can still be observed in different orders in different places) is a middle ground. Compensating transactions (detecting a conflict after the fact and issuing a correcting transaction, with an audit trail) are appropriate when strong consistency isn't practical for the whole system but correctness still needs to be guaranteed eventually.
Worked example. Say the write history shows two writes to the same account from two regions within 150ms of each other (a legitimate near-simultaneous transaction, not a bug in how the writes were made), and the documented rule is last-write-wins by timestamp. Applying that rule by hand to the two timestamps says the balance should now reflect the LATER of the two writes; but Region A's replica still shows the value from the EARLIER write, three minutes after both writes completed. That's a genuine conflict-resolution bug: the rule isn't being applied correctly, or Region A never received or processed the later write's conflict-resolution signal at all. That's a materially different, more urgent finding than 'it's just eventual consistency doing its job', and changes the response from 'explain the trade-off to the team' to 'stop write acceptance on affected accounts and fix the propagation bug before more accounts diverge'.
Trade-offs and pitfalls. For financial data specifically, 'eventually consistent, usually fast' is often not an acceptable design even when it's working exactly as intended, because the business cost of a customer briefly seeing a wrong balance can be real (support tickets, trust, in some jurisdictions regulatory exposure); this is why strong consistency for the critical balance-mutation path, even inside an otherwise eventually-consistent system, is a common and justified exception rather than an architectural inconsistency. Compensating transactions need a genuinely reliable detection mechanism to be trustworthy: if conflicts can go undetected, 'we compensate for conflicts' is not actually a guarantee, just a hope. Causal consistency sits between the two on cost as well as guarantee: it's cheaper to provide globally than full strong consistency, since truly unrelated writes across different users or accounts still don't need to be globally ordered, but implementing it correctly means tracking causal dependencies through the system (commonly via version vectors or similar per-write metadata), which is real engineering complexity a naive eventually-consistent design doesn't carry, and it still leaves an inconsistency window open for writes a given reader has no causal relationship to.
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.