Your recursive log parser passed every test, then died at 3 a.m. on a million-record chain — why was that inevitable?
answer
- Ask what the input actually controls
- Fixtures were orders of magnitude smaller
- Which resource was never measured?
- No degradation curve, only a cliff
- Depth is a function of input length
basics
~20 sRecursion depth tracked input size, so thousand-record fixtures held a thousand frames while the production chain demanded a thousand times more than the stack region could hold. Stack exhaustion has no warning curve: it works, then it aborts.
solid answer
~50 sThe parser used one frame per record, so its stack cost was a deterministic function of input length: depth equals records, and the ceiling is the stack region divided by frame size. Fixtures three orders of magnitude smaller than production exercised three orders of magnitude less of the resource that eventually ran out, and every other signal — correctness, throughput, allocated memory — looked healthy, because none of them measure depth. Unlike allocation pressure, stack consumption produces no degradation curve to alert on; the run either fits or it terminates. So this was not bad luck, it was a scaling property nobody measured. The fixes are to know the depth function, exercise the documented maximum input rather than convenient fixtures, emit peak depth as a metric with an alarm below the ceiling, and prefer a design where depth does not track input the system does not control.
go deeper
Take away the core fact: recursion depth grows with input, so passing on small fixtures says nothing about large inputs. Ask early how deep a recursion can go on the biggest input the system accepts.
Explain why the failure is discontinuous — pushing the ten-thousandth frame costs the same as the tenth, so there is no rising metric to extrapolate — and why correctness, throughput and allocation graphs all stayed healthy.
Show the diagnosis and the corrective set: name the depth function, exercise the documented maximum input, instrument peak depth with an alarm, and reject oversized input with a clear error rather than letting the execution context die.
Own the standard that prevents the class: unbounded-depth recursion over externally supplied data is a design defect and an availability surface, and every such path needs a stated input bound with an owner. Decide what the organisation tests at scale and what it merely hopes about.
## Why "we got unlucky" is the wrong diagnosis The skeptic's version of this incident is that a rare, oversized input showed up and tipped the parser over. The accurate version is that the parser's stack consumption was a **deterministic function of input length** — one frame per record — and production simply supplied an input on the other side of a fixed threshold. Nothing was random. Depth = number of records; capacity ≈ stack region bytes / frame bytes. Once those two numbers were set, the crashing input size was determined, and everyone was merely waiting for traffic to reach it. That reframing matters, because "unlucky" invites a retry and a bigger region, while "deterministic" invites the question that actually resolves the incident: *what is the largest chain we can accept, and who guarantees it?* ## Why the test suite could not have caught it Three separate reasons compound: 1. **Fixtures were three orders of magnitude too small.** Thousand-record fixtures exercised roughly a thousand frames. Production needed a million. No amount of *repeating* those tests explores the axis that mattered — this is a scale property, and repetition is not scale. 2. **The failure is discontinuous.** Allocation-heavy problems usually announce themselves: memory use climbs, collection work rises, latency drifts up. Stack consumption produces none of that. Pushing the ten-thousandth frame costs the same as pushing the tenth. The system is perfectly healthy right up to the frame that does not fit, and then the execution context is gone. There is no curve to extrapolate from a small run. 3. **Everything the tests measured looked fine.** Correctness was fine — the algorithm is right. Throughput was fine — the work is linear. Allocated memory was fine — the routine allocates nothing. The one resource being consumed proportionally to input was the one nobody instrumented. The general lesson is worth saying out loud in an interview: **a test suite only protects the resources it observes at the sizes it exercises.** ## Why 3 a.m. and not during business hours The distribution of chain lengths usually has a long tail driven by batch or backlog behaviour — an overnight consolidation job, a retry storm, a slow consumer that let a chain accumulate. The largest inputs a system ever sees are typically produced by machines during quiet hours, not by users during peak. So the first crossing of a size threshold characteristically happens when nobody is watching, and that is a reason to bound the input rather than to hope. ## What to change, in the order a reviewer wants to hear it **1. Name the depth function.** For every recursion over externally supplied data, write down depth as a function of input: constant, logarithmic, or linear. Linear depth over unbounded input is a defect at design time, not a surprise at runtime. This single habit prevents most repeats. **2. Test at the documented bound, not at the convenient size.** If the contract says chains may reach one million, the suite must run one million at least once, even if that test is slow and lives outside the fast suite. If nobody can state the bound, that is the real finding of the postmortem. **3. Instrument peak depth.** Depth is cheap to track — a counter incremented on entry and decremented on return, with a high-water mark reported per run. That converts an invisible cliff into a graph you can alarm on well below the ceiling, which is the difference between a ticket next week and a page at 3 a.m. **4. Fail the input, not the process.** Enforce an explicit depth or size limit and reject the oversized chain with a clear diagnostic. A rejected record with a named reason is vastly better operationally than an aborted execution context that takes unrelated in-flight work down with it, and it also closes an availability risk: if an outsider can influence chain length, an unbounded recursion is a denial-of-service surface, not merely a robustness gap. **5. Remove the coupling if the input is genuinely unbounded.** Bounding depth so it no longer tracks input size is the only change that retires the failure class rather than relocating the threshold. Enlarging the region is a stopgap: a constant factor against an unbounded quantity, acceptable overnight with an alarm attached, embarrassing as a permanent answer. ## The postmortem sentence that lands "Our stack usage was linear in an input we do not control, we never measured it, and the failure mode gives no warning — so the crash date was set the day we shipped it." That sentence gets agreement from the skeptic, because it makes no claim about luck and identifies the missing control precisely.
- A colleague proposes enlarging the stack region and rerunning. Do you accept that as the fix?As an overnight stopgap with an alarm on peak depth, yes; as the fix, no. Depth is linear in an input we do not control, so a larger region buys a constant factor and moves the crash to a larger chain. Accepting it as the resolution ends the incident without changing the property that caused it, and the next crossing arrives with less warning because everyone believes it was handled.
- How would you make this failure visible before it happens?Track depth explicitly: increment on entry, decrement on return, report the high-water mark per parse, and alarm at a fraction of the known ceiling. Pair that with a size check at ingest that rejects chains above the documented bound. Together they turn an invisible cliff into a trend line plus a clean rejection, which is what makes the risk manageable rather than merely known.
- Why is an unbounded-depth parser on externally supplied input a security concern, not just a robustness one?If anyone who can influence the data can influence chain length, they can choose an input that terminates the execution context on demand, and repeat it. That is an availability attack requiring no privilege and little bandwidth. Treating input-driven depth as a hardening item — an enforced limit and a rejection path — puts it in the same category as any other unbounded resource consumption driven by untrusted input.
saying these in an interview costs you the question
- It was an unlucky oversized input, not a design issue
- The tests would have caught it if we ran them more often
- Memory graphs would have shown the problem building
- Just enlarge the stack region and move on
- Wrap the entry point in an error handler and retry
- It is linear time, so it scales fine