GRANT · WANGsoftware developer
JUN 2026Essay

What AI still can't debug for you

The deploy was green and the site had been down since launch. A debugging story about a confidently wrong model — and the one move it never makes on its own.

DebuggingAWSAI-assisted developmentInfrastructure
6 min read · 1,245 words

The deploy was green. Terraform applied cleanly, the dashboard was all the right colors, the deploy log scrolled past without a complaint. The site had also been returning a 403 to every real request since the day it launched — cached so aggressively that by the time I looked, the error responses were over four hundred seconds stale. Every part of the system that was supposed to tell me it was broken had stayed quiet.

The backend for pyobfuscate is a CloudFront distribution in front of a Lambda, locked so the Lambda can only be invoked through that one distribution. That lock — Origin Access Control for Lambda function URLs — was the entire problem, and not for any reason you could read off a stack trace. The interesting part isn't the bug. It's what working through it taught me about where an AI assistant stops being able to help, and why.

Three sources of truth, all wrong at once

OAC for Lambda URLs was about a year old when I reached for it, and it showed. The feature was new enough, and flaky enough, that plenty of people had simply routed around it — bolting their own origin auth onto requests with a random secret header rather than trusting AWS's mechanism to work. When my AI assistant went looking for guidance, half of what it surfaced predated the feature's launch: confident advice that setting the function's auth type to AWS_IAM would cause 403s, which is the exact opposite of how it works now.1

So all three things I'd normally lean on were wrong simultaneously. The model's training was stale. The live web was stale. The wisdom of the crowd had given up and built something else. When the map is wrong everywhere at once, confidence is the most dangerous thing in the room — and confidence was the one thing in abundant supply.

Confidently wrong, and confidently wrong about why

Here is where it got expensive. The model never said "I don't know." It diagnosed — fluently, specifically, and incorrectly.

The first pass concluded that the root cause was a missing condition: OAC, it explained, doesn't populate the SourceArn field, so the fix was to delete that constraint from the policy. It made the change and reported the service restored. Both halves were false. Deleting the constraint had changed nothing — a different, unrelated bug was masking it — and "restored" had never been checked. It was assumed. I was holding a clean, confident, complete postmortem of a fix that had not happened.2

Later, the same thing in miniature. POST requests needed the browser to compute a SHA-256 of the request body and send it along for the signature to validate. I gave the instruction. The agent confirmed it was done; the apply was green. The JavaScript it had actually served was sending the literal string UNSIGNED-PAYLOAD instead of computing anything. I pointed this out. Done, it said. Still the placeholder. Three times.

This is the intern who tells you it's almost finished at four standups running. The trouble was never that the suggestions were stupid — most were reasonable in isolation. The trouble is that the model is trained to deliver a finished answer, so a finished answer is what it delivers, every single time, including the times when no finished answer fits in one shot. It would propose the whole fix, watch it fail, explain the failure with total assurance, and propose another whole fix. Motion without progress.

It didn't help that the system was built to hide its own failures. A CloudFront rule was quietly rewriting every 403 into a 200 that served the homepage, so the browser saw success sitting on top of total failure. Several of the underlying bugs threw an identical 403 from different layers, so even once I tore the mask off, the error pointed everywhere at once. You cannot debug a system that lies to you about whether it is working, and you especially cannot do it by asking for the whole answer over and over.

The fix was decomposition — the move it never proposed

What finally worked was nothing clever. It was chunking: stop asking why is the site down and start building the smallest experiments whose results cannot be misread.

First, rip out the rule that rewrote errors into success, so that a failure would at least look like one. Then the best thing I did all week — stand up a second Lambda that did nothing but answer GET /health. A GET has no request body, which meant the entire body-signing problem could not possibly apply to it. So if /health came back 200, the OAC, the IAM, the signing — the whole authentication layer — were provably correct, and every bug that remained had to live in the POST path. One green check collapsed half the search space into known good.

From there the chunks got smaller. The same hash, sent by curl, worked; sent by the browser, failed. That one comparison cornered the culprit: something between the browser and the Lambda was altering the request body after the browser had hashed it. It turned out to be a CloudFront function that merely touched the body to check its size — and in touching it, re-encoded it, so the bytes the browser signed and the bytes the origin received no longer matched. The last bug standing, found by elimination rather than insight.

Here's the thing: the model could have run every one of those experiments. It is fast, tireless, and genuinely good at the doing. What it never did — what I'm now fairly sure it will not do unprompted — is propose the ladder. "Build a trivial version that isolates exactly one variable" is the precise opposite of the instinct it was optimized for, which is to hand you the whole thing, complete, immediately.

A mismatch, not a failing

I don't think this is a flaw so much as a mismatch between how the model was trained and how it would actually be most useful. It is shaped to look done. It is most helpful in the moments when it should instead admit a problem can't be taken in one bite and start laddering — and those are exactly the moments it doesn't switch modes.

So the durable skill, the part that stayed mine, wasn't cleverness and it wasn't prompting. It was noticing that the problem couldn't be swallowed whole, and breaking it into pieces small enough to be certain about one at a time. I'd already learned the same lesson the hard way building the obfuscator this thing serves — the genuinely difficult parts only ever moved once I stopped asking for the finished algorithm and split the work into chunks the model could be right about separately. The AI collapses the cost of doing the work. Deciding which small, checkable piece of work to do next is still the job.

Notes

  1. OAC for Lambda function URLs launched on April 11, 2024, and explicitly requires AWS_IAM as the auth type. Anything written before that date describing AWS_IAM as the source of 403s is describing a different world.

  2. To its credit, the corrected version of that postmortem is scrupulous about marking which conclusions were later overturned and which symptoms it could no longer reconstruct — which is the right instinct, and also a quiet admission that the confident first draft had been fiction.

Related projects