Start now →

From Hallucination to Production Bug: A Post-Mortem on AI-Generated Code

By Rajan Patekar · Published April 25, 2026 · 7 min read · Source: Level Up Coding
EthereumAI & Crypto
From Hallucination to Production Bug: A Post-Mortem on AI-Generated Code

It didn’t fool the developer. It fooled the reviewer. Here’s how a Copilot suggestion — made with good intentions — became a race condition in QA.

I want to tell you about a bug I helped create.

Not the developer. Me — the reviewer. The person whose job is to be the safety net.

I’ve been thinking about this incident for a while because it exposes something about AI coding assistants that almost nobody talks about. Everyone warns developers about blindly accepting Copilot suggestions. Nobody warns reviewers about confidently spreading them.

Here’s what happened.

The Fragment Problem: Copilot saw a query. It couldn’t see the TransactionScope, the row lock, or the concurrent load around it.
The Fragment Problem: Copilot saw a query. It couldn’t see the TransactionScope, the row lock, or the concurrent load around it.

The Setup

A developer on my team had written a function that fetched and updated a record. It was wrapped in a TransactionScope — a deliberate choice. The transaction was acquiring a row-level lock, ensuring that no other process could touch that record while the operation was in flight. The logic was correct and intentional.

During my review, I selected a chunk of that function — the query portion — and pasted it into Copilot Chat with a simple ask: “Can you optimize this?”

Copilot came back almost immediately with a clean, confident suggestion: add AsNoTracking() to the LINQ query.

// Copilot's suggestion
var record = _context.Records
.AsNoTracking()
.FirstOrDefault(r => r.Id == recordId);

On the surface, this looked great. AsNoTracking() is a well-known Entity Framework Core optimization. When you don't need to track changes to an entity — when you're just reading data — it skips the overhead of the EF change tracker. Faster query. Less memory. Totally valid in the right context.

I recognized it immediately. I felt good about it. I posted it as a review comment.

The developer trusted my review. They incorporated the change. It passed CI. It went to QA.

The Bug

In QA, under concurrent load, the system started producing race conditions.

Here’s what was actually happening. The TransactionScope around that function existed specifically because the code was designed to read, then update a record in a way that needed to be atomic. The row lock was the mechanism that made it safe — it prevented another request from reading and modifying the same record between our read and our write.

AsNoTracking() bypassed the EF change tracker, but that wasn't the core issue. The deeper problem was that by changing how the query interacted with the transaction context, the locking behavior changed in a way that wasn't immediately visible in the code. Under concurrent requests, two processes could now read the same record, both believing they had an exclusive lock, and both proceed to update it.

The race condition wasn’t consistent. It only showed up under load. Which is exactly the kind of bug that’s easy to miss in unit tests and terrifying to diagnose in production.

Why This Is Worse Than a Developer Accepting a Bad Suggestion

The usual AI code review story goes like this: developer gets a Copilot suggestion, accepts it without thinking, reviewer catches it or doesn’t, bug escapes.

This story is different. And I think it’s more important to talk about.

I didn’t passively accept a suggestion. I actively sought one out. I copied code, asked a question, received an answer, evaluated it with my own knowledge, agreed with it, and then used my authority as a reviewer to tell the author to make the change.

The AI didn’t bypass the safety net. The AI became the safety net — and I handed it that role without realizing it.

The reason it worked so convincingly is this: Copilot saw a fragment of code, not the whole function. It had no idea about the TransactionScope. It had no idea that a row lock was in play. It had no idea that the query's interaction with the transaction context was load-bearing. It saw a LINQ query without AsNoTracking() and correctly identified that — in isolation — adding it would be an optimization.

It was right about the fragment. It was blind to the system.

This is what I mean by the Hallucination Gap in practice. It’s not the AI inventing fake methods or writing obviously broken code. It’s the AI being confidently, partially correct — and partial correctness, in a system under concurrent load, is enough to break things.

Where the Review Process Failed

Looking back honestly, there were two moments where this could have been caught.

When I asked Copilot for the optimization. I gave it a fragment. That was the mistake. I selected the query lines because that’s what I wanted to optimize. But context isn’t optional when you’re dealing with concurrency, transactions, or locking. If I’d shared the entire function — the TransactionScope, the update logic, the intent — the suggestion might have been different. Or I might have read the suggestion differently knowing the full picture was visible.

The mental model I was using. I evaluated Copilot’s suggestion using my knowledge of AsNoTracking() in isolation. I didn't ask myself: "Does this function have concurrency concerns? Is this query inside a transaction that depends on locking behavior?" I answered the wrong question — "Is this a valid optimization?" — instead of the right one: "Is this a safe optimization given everything this function is doing?"

Those are different questions. The AI can only answer the first one. The second one was my job.

What We Changed

Four concrete things came out of this incident.

1. We made a rule: never ask Copilot to review or optimise a fragment. If you’re going to use Copilot Chat during a review, the entire function goes in — not just the lines you’re curious about. Copilot optimises what it can see. If it can’t see the TransactionScope, it doesn't know the TransactionScope exists.

2. We added concurrency to our review checklist. New prompt for reviewers: “Does this code touch shared state, run inside a transaction, or acquire a lock? If yes, would any suggested optimisation affect that behaviour?” One question. It takes ten seconds to ask. We weren’t asking it before.

3. We created a team norm around flagging transactional intent in code. Code that uses TransactionScope or explicit locking now gets a short comment explaining why — not just what.

// TransactionScope used intentionally to acquire a row-level lock.
// Do not add AsNoTracking() — it affects locking behaviour under load.
using (var scope = new TransactionScope())
{
var record = _context.Records
.FirstOrDefault(r => r.Id == recordId);
// ...
}

This sounds like over-commenting. But it’s insurance. A future developer — or a future reviewer with a Copilot Chat window open — now has explicit context they can’t miss.

4. We had a team conversation about the psychology of AI-assisted review. The instinct when you get a clean, confident Copilot suggestion is to feel like it’s been validated. It hasn’t. Copilot is not a second reviewer. It’s a tool that is very good at pattern matching within a limited context window. Using it during a review is fine. Treating its output as reviewed is not.

The Bigger Point: AI-Assisted, Not AI-Unsupervised

The future of software development is AI-Assisted. Not AI-Unsupervised. The difference between those two words was, in our case, a race condition in QA.

AI assistants are genuinely powerful. They make developers faster, they reduce boilerplate, and they surface optimizations that a tired reviewer might miss. But they are optimizing for what looks correct based on patterns — not for what is correct given your system’s specific behavior under load, concurrency, or failure conditions.

That gap — between looking correct and being correct — is still human territory.

The teams that will use these tools well aren’t the ones prompting the hardest. They’re the ones who’ve been honest with themselves about what the AI can see and what it can’t — and who’ve built their review culture around that distinction.

In my case, Copilot could see a query. It couldn’t see the lock. That was my job. I outsourced my job to a tool that wasn’t equipped to do it, and a race condition was the result.

I won’t make that mistake the same way again. But I’ll probably make a different version of it — because these tools are new, the failure modes are new, and we’re all still learning where the edges are.

The least I can do is write it down when I find one.


From Hallucination to Production Bug: A Post-Mortem on AI-Generated Code was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

This article was originally published on Level Up Coding and is republished here under RSS syndication for informational purposes. All rights and intellectual property remain with the original author. If you are the author and wish to have this article removed, please contact us at [email protected].

NexaPay — Accept Card Payments, Receive Crypto

No KYC · Instant Settlement · Visa, Mastercard, Apple Pay, Google Pay

Get Started →