Debugging and Systematic Troubleshooting Questions
Diagnosing defects methodically: reproducing failures, forming and testing hypotheses, reading stack traces and logs, bisecting changes, and reasoning about error handling and edge cases. Covers a disciplined root-cause approach that applies from local bugs to production issues, distinct from embedded hardware-level debugging. A universally probed engineering-craft skill.
Provide a succinct example of a recent small bug you fixed. Include the minimal code before and after in a language of your choice, explain the root cause, why your fix works, how you tested it, and what you learned that will help you avoid similar bugs in the future.
Sample Answer
Direct answer
A recent example: a function computing a running total returned a subtly wrong result specifically when passed an empty input list, because it assumed at least one element would always be present; the root cause was an unchecked assumption about the input's shape, not a logic error in the calculation itself, and the fix was a one-line explicit guard plus a test covering the previously-unconsidered case.
Structured elaboration and worked example
Before:
def average_response_time(durations):
total = sum(durations)
return total / len(durations)
This looks correct and passes any test using a non-empty list. The bug: called with an empty list (a genuinely possible input, e.g., a time window with zero recorded requests), it raises ZeroDivisionError, which in this specific case was crashing a reporting job whenever a low-traffic service had a quiet hour with literally zero requests, a real, recurring production scenario the original code never considered.
After:
def average_response_time(durations):
if not durations:
return None # no data for this window; caller decides how to display that
total = sum(durations)
return total / len(durations)
Root cause: the function was written and tested against realistic-looking sample data, which always happened to be non-empty, so the implicit assumption "there's always at least one duration" was never challenged during development; the bug surfaced only once real production traffic included a genuinely empty window, an edge case that's easy to overlook precisely because it's uncommon rather than because it's hard to reason about once you think to check for it.
How it was tested: added an explicit unit test calling the function with an empty list and asserting it returns None rather than raising, alongside the existing non-empty-input tests, so the specific edge case that caused the production issue is now permanently covered and can't silently regress.
What was learned to avoid similar bugs: for any function that aggregates over a collection, explicitly consider and test the empty-collection case as a matter of habit, not just when a bug report forces the question; more broadly, the pattern generalizes to any input assumption implicit in code (non-null, non-negative, within some expected range) that reads as "obviously always true" during development but isn't guaranteed by the function's actual contract, and is worth deliberately challenging each such assumption with a test rather than trusting that realistic-looking sample data will happen to cover it.
Trade-offs and pitfalls
The fix here (returning None for an empty input) is a specific design choice, not the only valid one; an alternative would be raising a more informative, explicit exception (ValueError("cannot average an empty list")) if silently returning None risks a caller mishandling it further downstream without noticing. The right choice depends on what callers actually need to do with a "no data" result, and is worth deciding deliberately rather than defaulting to whichever felt fastest to write.
Your CI build fails only on the pipeline and never locally. Outline a methodical plan to find the difference: comparing environment variables, container images, OS versions, dependency versions, and filesystem semantics, and describe how you would produce a minimal reproducible example inside CI itself.
Sample Answer
Direct answer
When a CI build fails only in the pipeline, the fastest path is to systematically diff the CI environment against your local one along every axis that could plausibly differ, rather than guessing: environment variables, container/base image, OS and library versions, filesystem case-sensitivity and permissions, and the exact command CI runs versus the one you run locally. The end goal is a minimal reproduction that runs inside CI itself, because "works when I add print statements" isn't a fix, it's a workaround.
Structured elaboration
- Compare environment variables. Dump the full environment CI uses (most CI systems let you print it, or add a debug step that runs
env) and diff it against your local shell. Missing or unexpectedly-set variables (aNODE_ENV, a locale, a timezone, a feature flag) are a common silent cause. - Compare the base image or container. If CI runs in a container and you develop on bare metal (or a different container), pin down exact OS version, installed system libraries, and default locale/timezone, since these can silently change behavior (date parsing, sort order, floating-point rounding modes).
- Compare dependency versions exactly, not just "the same major version." A CI pipeline that does a clean install from a lockfile can resolve a transitive dependency (a dependency of one of your dependencies: something your package manager pulled in automatically, not something you installed directly) differently than a long-lived local
node_modulesorvenvthat never got a clean reinstall. - Compare filesystem semantics. CI runners are frequently Linux (case-sensitive filesystem) while local development happens on macOS (case-insensitive by default); an import or file reference with inconsistent casing works locally and fails only in CI.
- Reproduce inside CI, not around it. Add a debug step to the CI job itself, before the failing step, that dumps the environment, tool versions, and installed package list, so you're comparing CI's actual state rather than guessing from documentation. If possible, get an interactive shell into the exact CI container (many CI providers support this) and run the failing command by hand.
- Build a minimal reproducible example inside CI: strip the pipeline down to the smallest set of steps that still reproduces the failure, in a scratch branch if needed, so you have a fast iteration loop instead of waiting on the full pipeline each time.
Worked example
A test suite that passes locally on macOS but fails in CI (Linux) with a file-not-found error: dumping the CI environment shows the import path from Utils import helper, while the actual file on disk is utils.py. macOS's case-insensitive filesystem silently accepted the mismatch locally; Linux's case-sensitive filesystem in CI did not. The minimal repro: a two-line script with that exact import, run in a Linux container locally, reproduces it in seconds without needing the full CI pipeline.
Trade-offs and pitfalls
The trap is iterating by pushing small changes and waiting for the full CI pipeline to re-run, which can cost many minutes per guess. Getting a fast, local reproduction of the CI environment (even a bare Docker container matching CI's base image) pays for itself after two or three iterations. A second common trap: fixing the symptom by disabling the failing step (skip, retry, quarantine) without ever finding the actual environment difference, which resurfaces the same class of bug on the next similarly-shaped test.
A production API sometimes returns elements in an inconsistent order across clients because sets are used internally. You are responsible for triage: how do you investigate, explain the nondeterminism to stakeholders, and implement a stable ordering for the API output while keeping acceptable performance?
Sample Answer
Direct answer
Sets in most languages make no guarantee about iteration order, so building API output directly from set iteration produces order that can legitimately differ across processes, language/runtime versions, or even between runs of the SAME process, depending on internal hash-table implementation details; the investigation confirms this mechanism directly, then implements a stable, explicit ordering rather than relying on incidental set-iteration behavior.
Structured elaboration
How to investigate: confirm the specific code path building the API response iterates over a set (or a dict/map in a language where iteration order isn't guaranteed) rather than a list or an explicitly sorted structure; reproduce by calling the endpoint multiple times, or across multiple server instances/processes, and diffing the returned element order directly, which should show inconsistency if a set is indeed the cause, versus consistent-but-simply-unexpected order from some other source (like a database query with no explicit ORDER BY, a related but distinct cause worth ruling out with the same investigative approach).
Explaining the nondeterminism to stakeholders: frame it precisely: this isn't a random or buggy failure, it's the EXPECTED behavior of an unordered collection, and the API was implicitly promising an ordering guarantee it was never actually designed to provide; different clients (or the same client at different times) can legitimately see different orderings today, which may have gone unnoticed as long as most callers didn't depend on order, until a specific consumer's logic (or a stricter test) started depending on stability that was never actually guaranteed.
Implementing stable ordering while maintaining acceptable performance:
- Sort explicitly at the point of serialization, using whatever ordering makes sense for the API's actual semantics (alphabetical, insertion order if that's meaningful and trackable, or a natural key like an ID or timestamp); for most APIs, the cost of sorting a response-sized collection (typically not enormous) is negligible compared to the request's other costs (network, serialization itself).
- If insertion order specifically needs to be preserved (and the language's default set doesn't track it), switch to an ordered-set-like structure if the language provides one (some languages/standard libraries offer collections that combine set semantics with insertion-order iteration), avoiding a separate sort step while still gaining determinism.
- For very large collections where sorting cost genuinely matters, consider whether the ordering can be established earlier in the pipeline (e.g., if the data already comes from a sorted source like a database query with an explicit
ORDER BY, preserving that order through to the response rather than passing it through an unordered set at any intermediate step) rather than re-sorting a large collection at serialization time on every request.
Worked example
Confirming the mechanism: the endpoint's handler collects results into a set (used originally just to deduplicate, with no awareness that its iteration order would become externally visible), then serializes that set directly to the response. Diffing repeated calls to the same endpoint shows genuinely different orderings across calls, confirming set-iteration nondeterminism as the mechanism (as opposed to, for example, a database query lacking an explicit sort, which would tend to be consistent WITHIN one server/database session but could still differ across sessions or after a schema change, a related but mechanistically distinct possibility worth ruling out explicitly rather than assuming). Fix: after deduplicating via the set (keeping that step, since dedup itself is still correct and desired), explicitly convert to a list and sort it by a natural, stable key (the item's own ID) before serializing, at negligible added cost relative to the rest of the request, resolving the nondeterminism while preserving the original deduplication behavior.
Trade-offs and pitfalls
The temptation to "fix" this by simply switching the internal data structure to something that happens to iterate in insertion order today, without an EXPLICIT sort, risks re-introducing the same class of bug if the underlying collection or its implementation ever changes in a future language/runtime version; an explicit, intentional sort at the serialization boundary is more robust than relying on an implementation detail of whatever collection happens to be used internally, even if that detail is currently observed to be stable.
What techniques do you use to prioritize multiple concurrent bugs or incidents affecting ML systems? Describe a decision rubric considering severity, user impact, reproducibility, rollback cost, and business KPIs, and explain how you would apply it during a busy incident window.
Sample Answer
Direct answer
Prioritize concurrent bugs and incidents using an explicit rubric weighing severity, user impact, reproducibility, rollback cost, and business KPIs together, not any single factor alone, since a high-severity-sounding bug with low actual user impact and an expensive rollback can rank BELOW a "smaller" bug that's actively costing revenue and has a trivial fix available.
Structured elaboration
The rubric, and how each factor is weighed:
- Severity: how bad is the failure mode itself (data loss/corruption ranks above a cosmetic issue, a security-relevant bug ranks above a performance blip), independent of how many users are currently affected.
- User impact: how many users/requests are affected right now, and is that number growing, stable, or shrinking; a severe bug affecting a handful of users may rank below a moderate bug affecting a large fraction of traffic.
- Reproducibility: a reliably reproducible bug can be diagnosed and fixed faster (lower time-to-resolution for the same engineering effort) than an intermittent one, which affects how quickly EACH candidate bug can actually be resolved if picked next, not just how bad it is.
- Rollback cost: if a bug traces to a specific recent change, how cheap and safe is reverting that change right now; a bug with a trivial, low-risk rollback available should often be handled immediately regardless of its rank on other factors, since the fix is nearly free.
- Business KPIs: which specific business metric is being affected (revenue, a contractual SLA, a compliance requirement) and how directly; a bug affecting a metric with hard, immediate business consequences (a broken payment flow) generally outranks one affecting a softer, longer-horizon metric even at similar technical severity.
Applying the rubric during a busy incident window: first, quickly triage EVERY open issue against the rubric (a few minutes per issue, not a deep investigation), to get a relative ranking rather than working issues in the order they arrived; second, look specifically for any issue with BOTH meaningful impact AND a cheap available mitigation (like a trivial rollback), since these should jump the queue regardless of their raw severity ranking, because the cost of addressing them is so low relative to the benefit; third, re-triage periodically as the window continues, since impact and severity can both change (a bug's blast radius growing, or a rollback becoming available partway through investigation of one issue), rather than treating the initial ranking as fixed for the whole incident window.
Worked example
Three concurrent issues during a busy window: (A) a high-severity-sounding data-consistency bug affecting a small, specific edge case (low current user impact, no clear quick fix, moderate rollback risk since the change is tangled with other recent work); (B) a moderate-severity bug causing a checkout-flow error for roughly 5% of transactions (clear, growing user impact, directly hitting a revenue KPI, and traced quickly to a specific recent config change with a trivial, low-risk rollback available); (C) a low-severity cosmetic UI bug reported by a few users (minimal impact, no urgency). Applying the rubric: (B) is prioritized FIRST despite being technically "less severe" than (A) in the abstract, specifically because it combines real, growing, revenue-affecting impact with an almost-free rollback fix, making it both the highest-leverage and fastest issue to resolve. (A) is prioritized second, staffed for a proper investigation given its rollback isn't cheap and its actual mechanism needs to be understood before a safe fix can be applied. (C) is deprioritized entirely for the duration of the busy window, revisited once the window calms down.
Trade-offs and pitfalls
A common mistake is prioritizing purely by SEVERITY LABEL (treating "critical" as automatically first regardless of current impact or fix cost), which can leave a rapidly-growing, revenue-affecting, cheaply-fixable issue waiting behind a technically-severe-but-currently-narrow, expensive-to-fix one; the rubric's explicit multi-factor weighing exists specifically to avoid that trap, and re-triaging periodically (rather than committing to the initial ranking for the whole window) accounts for the fact that these factors genuinely change as an incident window progresses.
Find and explain the bug in this Python function, then provide a corrected implementation:
def append_item(item, lst=[]):
lst.append(item)
return lst
# append_item(1) -> [1]
# append_item(2) -> [1, 2] # unexpected: shared across calls
Explain why the sharing happens and show the safe fix for the default argument.
Sample Answer
Direct answer
The bug is that lst=[] creates the default list once, at function-definition time, not once per call. Every call that omits the lst argument reuses the exact same list object, so items accumulate across calls that were never meant to share state. The fix is to use None as the sentinel default and create a fresh list inside the function body when no list was passed.
Structured elaboration
In Python, default argument values are evaluated exactly once, when the def statement executes, and the resulting object is bound to the function itself. A mutable default (a list, dict, or set) is therefore the SAME object across every call that relies on the default, so mutating it in one call is visible in the next. This is a well-known Python pitfall precisely because the code reads as if a fresh list is created per call, which is the intuitive (and correct) behavior for immutable defaults like None, 0, or "", but not for mutable ones.
Worked example
def append_item_buggy(item, lst=[]):
lst.append(item)
return lst
def append_item_fixed(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
print("BUGGY:")
print(append_item_buggy(1))
print(append_item_buggy(2))
print("FIXED:")
print(append_item_fixed(1))
print(append_item_fixed(2))
Executed output:
BUGGY:
[1]
[1, 2]
FIXED:
[1]
[2]
The buggy version's second call returns [1, 2] instead of the expected [2], because it's still appending to the exact same list object created back when the function was defined. The fixed version creates a brand-new list inside the function body on every call where the caller didn't explicitly pass one, so each call starts clean.
Trade-offs and pitfalls
This bug is especially dangerous because it's silent and cumulative: a function like this can appear to work correctly in isolated tests (each test creates its own scenario and might not notice the shared state) and only manifest as data mysteriously growing or leaking between unrelated calls once the function is used repeatedly in a long-running process, which is exactly the kind of "works in tests, fails in production" gap this class of bug produces. The general rule: never use a mutable object (list, dict, set, or a custom mutable class instance) as a default argument value; use None and construct the mutable object inside the function body instead.
Unlock Full Question Bank
Get access to all 24 Debugging and Systematic Troubleshooting interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.