← AI-built apps
Guide

Why AI-generated code breaks under real traffic (and how to fix it)

By Alastair Murdoch Last reviewed Tested on .NET, Entity Framework and SQL Server, on our own AI-assisted builds
In short

AI-generated code usually works for one user and fails when two arrive at once. It checks, then acts, with nothing stopping a second user between, trusts a quick look-up to prevent duplicates, and queries the database inside loops. The fix: re-check under a lock, claim each job in one step, lock in a consistent order and batch queries.

2
Orders created for one payment
9 → 4
Places in the code that changed ticket status
~60 → ~28
Database queries on one page
~15×
Throughput of a payment-collection job

Before you start

  • ✓A staging copy of the database you can put under heavy load
  • ✓Query logging or tracing switched on for the busiest pages
  • ✓A list of every action that touches money, stock or bookings
  • ✓A way to make two requests at exactly the same time (a test tool, or two people)

1. "Check, then act": lock what you're about to change

The most common pattern in AI-generated code is to check that a slot is free, then book it. Two customers clicking together both pass the check and both get the slot. The fix is to repeat the check inside the database transaction, with the records locked in a consistent order. A check on the form still gives a friendly early message, but only the locked check is a guarantee.

2. Payment confirmations arriving twice: claim the work in one step

After a card payment, the provider's notification and the customer's returning browser often arrive together. If each creates the order when it finds none, both get through: on one of our platforms, that meant two orders for one payment. Instead, each tries to claim the payment in one database step and only the winner builds the order, backed by a rule allowing one order per payment. A claim abandoned by a crash can be retaken after a couple of minutes, and losing the race is treated as normal, not an error.

3. Deadlocks: always lock in the same order

Two baskets adding the same two items in opposite orders can block each other until the database gives up on one of them (a deadlock). When we removed a lock to cure one deadlock, it simply moved to other records: the answer was a consistent order, not more or fewer locks.

4. Lost updates: make "someone else changed this" work

When two people edit a record, the second save can silently overwrite the first. A version stamp catches this only if it travels with the form and the database is told which version the person started from. A common AI mistake passes it back in a way the code quietly ignores, so the protection never fires.

5. Too many queries: fetch once, pass the results down

A lookup per item in a list means a database call per item (the "N+1" problem), and helpers calling other helpers multiply it. One grouped query, with the results passed down, fixes it. On one page, about 60 database calls became about 28; for ten ticket types the estimate was around 150 down to about a dozen.

6. Batch jobs: don't lose your place as the list shrinks

A job processing "expired" records 100 at a time, a page at a time, skips work: each processed batch leaves the list, so the next page starts 100 records too far in. Ours stepped over 100 still-expired records per batch. Take the first 100 each time, or have parallel workers each claim their own records.

7. Prove each race with a test that fails every time

Races that only reproduce by luck don't get fixed. Our test tool pauses one simulated user at a chosen point in its database work, so two users can be interleaved exactly. Each known race became a test kept switched off until fixed; passing when switched on is what "fixed" means. It caught a plausible fix in an AI-drafted plan that would have left a basket reporting success while holding one ticket instead of four.

What usually goes wrong

From our own projects, not the manual.

Concurrency protection was set up but never used

Three kinds of record had a version stamp to catch clashing edits, but no form carried it and nothing handled the clash, so two people editing the same record silently overwrote each other. When you wire it up, the database has to be told which version the person started from: simply setting the value does nothing.

A payment retry reused the same reference

After a card was declined, the retry sent the same duplicate-prevention key, so the payment provider rejected every attempt and the customer couldn't pay. Use a new key only after a definite decline. Keep the same one after a timeout, or you risk charging twice.

A failed tracking write cancelled a paid order

A non-essential marketing record was saved in the same transaction as the order. When it failed, the whole order rolled back after the card had been charged. Keep side-effects out of the order's transaction, or make sure their failure is harmless.

The concurrency test passed while the bug was live

A test booking 7 and 5 tickets against 10 stayed within stock, so it passed, while two baskets were actually taking the same tickets. Test the thing you care about (who holds which ticket), not just the totals.

The first fix over-reported success

Our first fix trusted the error details after a clash and counted tickets as won that had actually been lost. After a clash, re-read the result from the database.

Do it yourself, or get help?

If your app has one of these patterns and your developer is comfortable with database transactions and locking, the fixes are well trodden. Not every app has every pattern, and the right fix depends on how it's built, so start with anything that touches money or stock. If you're seeing duplicate orders, overselling or deadlocks in production, or you're not sure where the races are, get help: these bugs rarely show up in testing and cost real money when they happen.

Get a developer on it
FAQ

Common questions

Why does my AI-built app create duplicate orders?

Usually because the payment provider's confirmation and the customer's browser both try to create the order at the same moment, and the duplicate check looks before it writes. The fix is to claim the payment in a single database step, backed by a rule that allows only one order per payment.

How do I stop overselling or double-booking?

Re-check availability inside the database transaction with the records locked, always in the same order, rather than relying on a check made beforehand. A check on the form is still useful for a friendly message, but it isn't a guarantee.

What is an N+1 query?

It's a database query run once per item in a list instead of once for the whole list, so a page with 50 items makes 50 or more database calls. The fix is to fetch everything in one grouped query and pass the results down.

Why don't these bugs show up in testing?

Because tests usually run one request at a time. Races need two requests interleaved at exactly the wrong moment, so you need tests that force that interleaving.

Seeing duplicates, oversells or deadlocks?

Tell us what's happening. We'll find the race, fix it properly and add a test that proves it stays fixed.