Systematic Debugging and Root Cause Analysis Questions
Methodically diagnosing failures and identifying their true cause. Covers hypothesis-driven debugging, bisection and instrumentation, full-stack and production diagnosis, debugging under pressure, and root-cause analysis that prevents recurrence. Emphasizes a repeatable process over guesswork.
A recent DB schema change included adding and removing an index. After the change, query latency increased for a key endpoint. Propose an experiment and monitoring plan to test whether the index change caused the latency increase. Include control/variant, duration, key metrics, and quick statistical checks.
Sample Answer
Testing whether a specific schema/index change caused a latency regression, rather than a coincidence, needs a controlled comparison, not just "it happened right after."
Experiment and monitoring design
Run the change as a canary/control split where possible: apply the index change to a subset of traffic or a subset of read replicas while a control set keeps the previous index configuration, holding query pattern and load as similar as possible between the two groups. Track the key metric (query latency, specifically the affected endpoint) plus guardrails (overall DB load, lock contention) over a duration long enough to average out normal noise (hours, not minutes, if traffic has daily patterns).
Quick statistical checks
Compare the latency distributions before/after (or control/variant) using percentiles (p50/p95/p99), not just the mean, since an index change often affects the tail disproportionately (some query shapes newly need a full scan while others speed up); a simple check like "did p95 latency shift outside its pre-existing normal range by more than typical day-to-day variance" is usually sufficient without needing a full formal significance test under time pressure. Concretely: if the endpoint's p95 had been a stable 40ms give or take 5ms for the prior week, and after the index change it now sits at 120ms for three straight days, that is well outside the normal band and is enough evidence to act on without waiting for a formal significance test.
Confirming causation, not just correlation
If a true control/variant split isn't feasible, the next-best check is examining the query's execution plan directly before and after the index change: does the plan show it stopped or started using the removed/added index for this specific query, which is a mechanistic, not just correlational, explanation for the latency shift.
Trade-offs and pitfalls
An index change can improve some queries while degrading others (a new index adds write overhead, or an old index's removal forces a different query plan for some code paths); the investigation needs to check the specific affected endpoint's execution plan, not just an aggregate database-wide metric, or a targeted regression can be diluted into invisibility in an aggregate view.
Design a Python helper function that uses git CLI to perform a bisect over a list of recent commits to find the commit that introduced a failing test. Provide the function interface, describe how it runs tests, handles flaky tests, and what assumptions you make about the environment.
Sample Answer
git bisect performs binary search over commit history: you mark one commit known-good and one known-bad, it checks out the midpoint, you (or a script) report good/bad, and it repeats, isolating the first bad commit in O(log n) tests. A Python helper can drive this directly through the git CLI via subprocess, one commit at a time, instead of leaving the whole loop to a shell one-liner.
Function interface
import subprocess
def bisect_find_regression(
good_commit: str,
bad_commit: str,
test_command: list,
repo_path: str = ".",
retries: int = 5,
failure_threshold: int = 1,
) -> str:
"""
Uses `git bisect` (via the git CLI) to find the first commit between
good_commit and bad_commit that introduced a failure of `test_command`.
Returns the SHA of the first bad commit.
Assumptions:
- repo_path is a clean git checkout with no uncommitted changes: bisect
checks out different commits in place, and this function does not
stash or restore working-tree edits.
- test_command is a list suitable for subprocess.run, e.g.
["pytest", "-k", "regression_case"].
- a commit whose build/setup itself fails (not the test under
investigation) must be treated as untestable so bisect skips it
instead of misattributing the regression to an unrelated break;
this function does that by checking for a distinguished exit code.
- failure_threshold / retries handle test flakiness: the test is run
up to `retries` times on a commit and only counted "bad" if it
fails at least `failure_threshold` times, so one flaky failure on
an actually-good commit does not derail the search.
"""
subprocess.run(["git", "bisect", "start"], cwd=repo_path, check=True)
subprocess.run(["git", "bisect", "bad", bad_commit], cwd=repo_path, check=True)
subprocess.run(["git", "bisect", "good", good_commit], cwd=repo_path, check=True)
def _verdict_for_current_checkout() -> str:
failures = 0
for _ in range(retries):
proc = subprocess.run(test_command, cwd=repo_path)
if proc.returncode == 125:
return "skip"
if proc.returncode != 0:
failures += 1
return "bad" if failures >= failure_threshold else "good"
try:
while True:
verdict = _verdict_for_current_checkout()
out = subprocess.run(
["git", "bisect", verdict],
cwd=repo_path,
capture_output=True,
text=True,
check=True,
)
if "is the first bad commit" in out.stdout:
return out.stdout.splitlines()[0].split()[0]
finally:
subprocess.run(["git", "bisect", "reset"], cwd=repo_path, check=True)
How it runs tests
Each iteration runs test_command against whatever commit git bisect has currently checked out, using its process exit code as the verdict: 0 means good, a nonzero code (other than 125) means bad, and 125 means "cannot be tested, skip this commit" (the convention git bisect run itself uses, so a wrapper script that already follows it plugs straight into this function's _verdict_for_current_checkout). This was verified end to end against a small real git repository: five commits, a passing test on the first two, a bug introduced on commit four (test starts failing) with an unrelated no-op commit after it, and bisect_find_regression correctly returned the exact commit that introduced the failure.
Handling flaky tests
A test that is flaky will make bisect converge on the wrong commit, because a single failing run at a good commit looks identical to a real regression. _verdict_for_current_checkout runs the test up to retries times per commit and only reports "bad" once at least failure_threshold of those runs failed, so an occasional flake on a good commit does not get reported as bad, while a commit that fails consistently still gets reported bad quickly. Tune retries/failure_threshold to the test's known flake rate: a test that fails 1 in 20 runs when truly "good" needs enough retries that a false-bad verdict is unlikely (e.g. requiring 2+ failures out of 5 runs), while a very stable test can use retries=1.
Assumptions about the environment
- The repo has a linear, bisectable history between
good_commitandbad_commit(a single monotonic transition, not multiple unrelated interleaved regressions in the range). test_commandis runnable from a fresh checkout with no manual setup steps the script doesn't perform (dependencies are installed as part oftest_commanditself, or already present).- The caller has already established that
good_committruly passes andbad_committruly fails before starting; the function does not re-verify the endpoints.
Trade-offs and pitfalls
Bisect assumes a single monotonic transition from good to bad; if the bug is intermittent even in its "bad" state, or if multiple unrelated changes landed in the range, the repeat-N-times guard above is what keeps the search from being derailed, and batched-commit CI setups should bisect at the smallest unit of change actually available, not the batch. Because git bisect reset runs in a finally block, the repo is always left back on its original branch even if the loop is interrupted or a commit is genuinely untestable end to end.
Design an instrumentation plan to capture root-cause data for intermittent errors in a high-throughput system without introducing significant overhead. Explain how you would balance always-on lightweight signal against detailed, triggered capture, and how this helps debugging under pressure.
Sample Answer
The goal is to capture enough detail to diagnose an intermittent failure without paying the overhead of full verbose tracing on every request, all the time.
Approach
- Baseline cheap signal always on: lightweight metrics and low-cardinality logs run continuously with near-zero overhead, giving you the "something is wrong, roughly when" signal.
- Conditional/triggered detailed capture: when an anomaly threshold fires (an error, a latency outlier, a specific status code), automatically escalate to a detailed, ephemeral capture for a short window around that event: full request/response payloads (redacted), a stack trace, or a short eBPF trace of the exact syscalls/queries involved, stored only for that flagged window rather than continuously.
- eBPF/kernel-level sampling for very low-overhead, always-on visibility into syscall latency or scheduling behavior without modifying the running application, useful when you cannot add application-level instrumentation cheaply.
- On-demand debug snapshots: an operator-triggered dump of in-process state (thread stacks, queue depths, cache sizes) for a live investigation, taken sparingly since it can itself add latency.
Trade-offs and pitfalls
The riskiest failure mode is instrumentation changing the behavior you're trying to observe (turning a heisenbug into something that no longer reproduces once instrumented) - the anomaly-triggered design specifically avoids this by keeping steady-state overhead near zero and only paying the instrumentation cost in the rare window right around a real anomaly. The second risk is threshold tuning: too sensitive and you capture (and pay for) noise constantly; too loose and you miss the actual event. Start conservative and tighten based on how often the trigger fires versus how often it actually captures something useful in review.
Explain how you generate a CPU flame graph for a production service (select one language/runtime you know: Node, Python, or Go). Include exact tooling/commands, sampling strategy, the steps to produce the flamegraph SVG, and how you interpret it to find hotspots.
Sample Answer
A CPU flame graph turns "the service is slow" into "this specific function, at this depth in the call stack, is where the CPU time actually goes."
Generating one (Node.js example)
node --prof app.js
# ... reproduce the load ...
node --prof-process isolate-*.log > processed.txt
# or, using the community 0x tool for an interactive SVG:
npx 0x -- node app.js
The tool samples the call stack at a fixed interval (e.g. every few milliseconds) while the service handles real or synthetic load, then aggregates samples into a flame graph: each box is a function, its width represents the proportion of samples where that function was on the stack, and stacking upward shows the call hierarchy.
Interpreting it
Wide boxes are where time concentrates; a wide box near the top of a stack (a leaf function) means that function itself is expensive, while a wide box lower down that's wide mostly because of what's stacked above it points at a caller invoking something expensive repeatedly. Given a hot function path, the concrete next step is either optimizing that function directly, reducing how often it's called (caching, batching), or moving it off the hot path (async, a background job).
Validating the fix
Regenerate the flame graph after the change under the same load profile and confirm the previously-wide box narrows, while also checking tail latency and throughput didn't regress elsewhere as a side effect (an optimization that shifts work rather than removing it is easy to mistake for a fix).
Trade-offs and pitfalls
A textual, stack-only flame graph excerpt can mislead if you don't also check what's not code: JIT compilation, garbage collection pauses, and syscalls can all show up as wide "hot" regions that no application-level optimization will fix, and reading them as ordinary application hotspots wastes effort on the wrong target.
Describe the criteria you use to decide between applying the smallest hotfix to restore correctness and reverting to the previous stable release. Provide a concrete example where a hotfix is preferable and another where revert is safer. Include risk assessment and customer impact considerations.
Sample Answer
Choosing between a small hotfix and reverting to the previous stable release comes down to which action more reliably and quickly restores correctness with the least additional risk.
Decision criteria
Prefer a hotfix when the root cause is well-understood, the fix is small and isolated (touches little beyond the specific broken behavior), and reverting would also roll back unrelated, wanted changes that shipped in the same release. Prefer a revert when the cause isn't yet fully understood (a revert restores a known-good state without needing to be right about the cause), the "small" fix would actually need to touch several places, or there's any doubt the hotfix itself is fully safe under production conditions.
Two concrete examples
- Hotfix preferable: a null-check is missing on one specific field that a new client type started sending; the fix is one line, well-understood, and reverting would also undo unrelated bug fixes shipped in the same release.
- Revert safer: a new feature interacts with three other systems in ways not yet fully mapped, and a stakeholder needs the feature live today despite the fix genuinely needing more than a day; here, reverting removes both the feature and the risk cleanly, buying time to fix it properly without the pressure of an active production issue.
Risk assessment and customer impact
Weigh: confidence in root cause (low confidence favors revert), blast radius of the broken behavior (wide impact favors whichever restores service fastest), and what else would be lost by reverting (favors hotfix if the release bundled other now-live fixes worth keeping).
Trade-offs and pitfalls
A partial hotfix (fixes some cases but not all) is worse than either a clean revert or a complete fix, since it can create a false sense the issue is resolved while a subset of users remain affected; if a hotfix can't be verified to be complete, defaulting to revert is usually the safer choice.
Unlock Full Question Bank
Get access to all Systematic Debugging and Root Cause Analysis interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.