DEV Community

Sam Yang
Sam Yang

Posted on

The Patch That Passed Review and Broke the Build: A Debugging Retrospective

At 09:14 on a Tuesday, the alert page lit up with a spike of HTTP 500s on the catalog endpoint, and the patch responsible had merged the previous Friday with a green build and an approving review. The uncomfortable detail was that the patch had been generated by a coding agent, and its diff looked smaller and cleaner than most human pull requests on that service. This is the story of how that patch passed every gate and still broke production, and the debugging loop that found the real cause in under an hour.

The symptom was a KeyError: 'etag' raised inside the cache-hit path, and it only appeared after a specific sequence of writes and reads under load. Requests that missed the cache worked without issue, while requests that hit it after a particular write pattern crashed with a 500. The first hypothesis was cache invalidation, the second was a race between two worker processes, and both were wrong. What made the incident hard to read was that the traceback pointed at a line that had not changed in the patch, which is a classic sign that the real change lives somewhere else.

Instead of reading the entire service, I wrote a minimal reproducer that simulated the exact request sequence from the alert timeline: write, write, read, read. The second read crashed on the same KeyError, which narrowed the problem to the cache layer in about forty lines of Python. That is the first reusable technique from this incident: reproduce first, read code second, because a failing script is worth more than a thousand lines of inspection. The reproducer also gave me a way to verify the fix, which is something a traceback alone never provides.

import requests

BASE = "http://localhost:8000"

def write(key, value):
    r = requests.put(f"{BASE}/cache/{key}", json={"value": value})
    r.raise_for_status()

def read(key):
    r = requests.get(f"{BASE}/cache/{key}")
    return r.status_code, r.headers.get("etag"), r.json()

write("item:42", {"name": "first"})
write("item:42", {"name": "second"})
print("first read:", read("item:42"))
print("second read:", read("item:42"))  # cache-hit path crashes here
Enter fullscreen mode Exit fullscreen mode

The root cause turned out to be a constraint that vanished during generation, not a bug in the cache library. The agent had been given a token budget for the task, and its own plan showed the original requirement: write-through caching on updates, so the etag is populated at write time. By step five of that plan, the constraint was gone, and the delivered patch implemented read-through with lazy population instead. On a miss, the cache stored the response body but never the etag header, so the next hit crashed when the handler tried to compare validators. The agent did not crash or misbehave; it produced a coherent, well-typed patch that simply omitted one requirement, which is the hardest kind of failure to catch in review.

The second reusable technique is diff archaeology: when an agent wrote the patch, the final diff is not the whole story, because the intermediate plan contains the decisions that were traded away. I compared the agent's step-by-step plan against the merged diff, and the missing write-through was visible as a silent deletion between step two and step five. That comparison took five minutes and turned a confusing runtime error into a clear design regression.

To run the reproducer without touching a shared environment, I used a disposable server from MonkeyCode, an open source project whose free tier currently includes 10 million tokens and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The free model access was sufficient to analyze the traceback and the git history and produce a ranked hypothesis ledger, and the free server gave me a throwaway environment where I could corrupt cache state as much as I wanted. The hypothesis ledger is the third reusable technique: instead of chasing the first idea, list every plausible cause with its evidence and test them in order.

H1: stale cache entry missing etag     evidence: KeyError only on hit path
H2: race between writer and reader     evidence: intermittent under load
H3: header stripped by reverse proxy   evidence: works locally, fails behind LB
Enter fullscreen mode Exit fullscreen mode

Each hypothesis had a cheap test, and the reproducer made testing them a matter of minutes rather than meetings. H1 failed first because the reproducer crashed deterministically, which eliminated the race and the proxy in one step. That ordering mattered, because the cheapest test was also the most informative one, and it saved me from instrumenting a load balancer I did not control.

The fix itself was three lines: populate the etag during write-through, plus a regression test that exercised the exact sequence from the reproducer. I also added a constraint checklist to the agent prompt for future tasks, and a CI rule that fails when a diff touches the cache layer without a corresponding test. The regression test is the artifact that matters most, because it converts a one-time incident into a permanent guardrail.

This workflow has real limits, and it is not for every team. A free server is not a production environment, and my reproducer was synthetic, so it did not reproduce the load profile that triggered the original spike. Free model tiers carry rate limits, and the model's analysis was a starting point for the ledger, not a verdict on the root cause. Teams with strict data residency rules should not paste production logs into any hosted model, and teams mid-outage may be better served by a direct rollback than by an hour of reproduction.

MonkeyCode's free tier makes this loop cheap enough to practice before the next incident, which is exactly when you want to discover that your reproducer works. The next time a patch passes review and breaks the build, the question is not whether the agent was wrong; it is whether your debugging loop can find out why before the alert page does.

Top comments (0)