We shipped a job that sent order confirmation emails. It deduped by keeping message IDs in an in-memory Set. Clean code. An agent wrote it, I skimmed it, tests passed, staging was happy for two weeks.
Then traffic spiked, autoscaling added a second instance, and a few hundred customers got the same email twice. Two Sets. Zero shared state. Obvious in hindsight, invisible in review.
Here's the part that actually cost me: finding it took nine hours, and roughly seven of those were spent watching an AI agent confidently fix things that were not the bug. Vibe coding didn't hurt me. Vibe debugging did.
TL;DR
- Vibe coding works because generation has a cheap oracle: you run it, you see if it does the thing. Wrong guesses die instantly.
- Vibe debugging breaks because debugging is a search over runtime state, and your agent has never observed your runtime. It only sees text.
- A wrong guess while coding costs you a rejected diff. A wrong guess while debugging costs you a plausible diff that changes behavior without fixing the cause. Now you have two bugs.
- The fix is not a better prompt. It's a deterministic repro. A failing test is the best prompt you will ever write.
- After ~3 failed attempts, the agent's own rejected theories are polluting its context. Reset the session instead of pushing harder.
What is vibe coding, and why does it usually work?
Vibe coding is letting a model write code you accept without fully reading it. I do it every day and I'm not going to pretend otherwise.
It works because the feedback loop is short and the correctness check lives outside the model. Does the endpoint return 200? Does the page render? Does the CLI print the right thing? You don't need to understand the diff to evaluate it, because reality evaluates it for you in about four seconds.
Generation is also the thing language models are genuinely best at. Millions of examples of "function that takes X and returns Y" exist in training data. Your CRUD handler is not a novel artifact.
Why is vibe debugging so much worse than vibe coding?
Because a bug is a claim about something that happened inside a process the model never watched. It gets a stack trace, a log line, and your grumpy one-sentence description. From that it does the only thing available: pattern-match your symptom against the most statistically common cause of that symptom.
That's right often enough to be dangerous. When it's wrong, it doesn't return an error. It returns a fix.
My duplicate-email bug looked exactly like a race condition, because it was a concurrency problem. So the agent proposed a mutex around the Set. Textbook answer for a single process. Completely useless across two of them. The tests still passed. The duplicates kept going out. And now the code had a lock in it that would live there forever, quietly making every future reader assume the concurrency question had been handled.
What does an AI agent actually do when it can't find the bug?
Four moves, in this order, every time. You'll recognize all of them:
-
The null guard.
if (!x) return;The crash disappears. The bug relocates downstream, where it now looks like a different bug. - The try/except sponge. Wrap it, log it, call it "handled." Your error rate drops. Your correctness doesn't change.
- The rewrite. Narrow fixes failed, so it widens the scope and reimplements the whole function. Now the diff is 200 lines and you cannot tell which line mattered, or whether anything got fixed versus reshuffled.
-
The confident close. "Fixed! The issue was that the
Setwasn't thread-safe." Stated as fact. Nothing was run.
None of this is the model lying. It's what you get from a system optimized to produce a plausible patch, with no ground truth to check itself against. Give it no oracle and it will invent the feeling of one.
Why does the agent get worse the longer you debug with it?
Because its own wrong theories become context. Turn 1 it says the issue is the lock. Turn 4 it's reading "the issue is the lock" as evidence in its own transcript. You've built a machine that cites itself.
I've watched an agent spend twelve turns defending a diagnosis I had already disproven in turn three, because my disproof was one line and its theory was six paragraphs.
Three strikes and you reset. Open a clean session and paste only facts: the repro steps, the actual observed values, what you ruled out and how. Not the conversation. The evidence.
How do I stop the vibe debugging death spiral?
Five rules. They cost real time up front and they've saved me entire days.
1. No repro, no agent. Spend the first hour making it fail on command. This is not preparation for the work, it is the work. Once you have a deterministic failing test, the agent's search has a terminal condition it can check by itself, and its hit rate goes up enormously. Before that, you're asking it to guess.
2. Make it explain before it edits. Ask for the causal chain with file:line citations. Which line writes the bad value, which line reads it, what's between them. If it can't cite, it's guessing, and you just saved yourself a patch. One extra turn, kills most bad fixes.
3. Cap the diff. "Fix this in 10 lines or tell me you can't." Big diffs are where unfixed bugs hide. The constraint also forces a real diagnosis, because you can't shotgun a whole module in 10 lines.
4. Delete the fix and re-derive it. Once it's green, revert the patch and confirm the test goes red again. Embarrassing but true: a solid share of "fixes" I've accepted were placebo. The real change was a restart, a cleared cache, or an unrelated edit made three turns earlier in the same session.
5. Feed it observations, not adjectives. Don't say "it's flaky." Have the agent add instrumentation, run it yourself, and paste the actual values back in. It is an inference engine running on a bad prior until you hand it data. Accuracy is a function of what's in the context window, not how sternly you phrase the request.
What finally fixed the duplicate emails?
A unique constraint on (order_id, template) and an insert that catches the conflict. Three lines. The dedupe belonged in the database, which is the only thing in that system both instances agreed on.
The agent could have gotten there in one turn if I'd given it the right input. Instead of "emails are duplicating, here's the handler," the prompt that would have worked was: "this dedupe uses process-local memory; we run 2+ instances." That's not a prompting trick. That's me having done the diagnosis. The model was never going to discover the deploy topology by staring at the file.
Is vibe coding still worth it?
Yes, and I'd argue it gets more worth it once you stop vibe debugging. Let the agent generate freely in places where reality checks the work fast: UI, scaffolding, glue, one-off scripts, anything with a visible output. Then move your attention from reading diffs to owning the feedback loop. Tests, repros, instrumentation, and a clear picture of what your system does at runtime.
The skill that appreciates here isn't prompt writing. It's being the person who can make a bug happen on demand. That's the one thing an agent still can't do for you, and it's the input that makes everything else it does actually work.
So: is vibe coding dangerous? Vibe coding is fine. It fails cheaply and visibly, because running the code tells you immediately whether it worked. Vibe debugging is what kills you, because an AI agent can't observe your runtime state and will answer a bug report with the most statistically plausible fix instead of the correct one. That fix often passes your tests, changes behavior, and hides the real cause under a lock or a null guard. Fix the loop, not the prompt: build a deterministic repro first, demand a cited causal chain before any edit, cap the diff size, revert-and-re-verify every fix, and start a fresh session after three failed attempts.
Top comments (2)
The three failed attempts cutoff is the bit I would steal. Once the agent has written two plausible wrong fixes, the transcript starts anchoring it to the wrong theory. I usually want a fresh session plus one failing test at that point, even if it feels slower.
The mutex around the in-memory Set is a perfect example of a patch that looks responsible while preserving the actual failure mode across two autoscaled instances. Moving deduplication to a unique constraint on
(order_id, template)gives the invariant to the one layer every worker shares, and reverting the three-line fix to prove the test turns red again is a strong placebo check. For founders, I'd make that ownership question part of design review: if correctness must survive retries, restarts, or horizontal scaling, process memory can optimize the path, but it cannot be the authority.