← Todas las noticias
security5 min de lectura

Race conditions in AI-generated API code

Por Seaworthy · 22 de junio de 2026

A graphical user interface displaying concurrent processes and timing indicators

Photo by RKTW extend on Unsplash

Race conditions are the kind of bug that doesn't show up in unit tests, passes code review, and then causes a production incident at 2am on a Saturday. AI-generated API code is especially prone to them, not because the AI is careless, but because the patterns it learns from are written for clarity, not concurrency.

Why AI code misses this

LLMs are trained on code examples, documentation, and tutorials. That material is almost universally written to show one thing: how the happy path works. A tutorial teaching you how to deduct credits from a user account doesn't simulate two simultaneous requests hitting the same endpoint.

So the model produces code that looks correct. It reads the current balance, checks if it's sufficient, then deducts the amount. Sequential, readable, easy to follow. The problem is that "easy to follow" and "safe under concurrent load" are not the same thing.

The check-then-act problem

Here's a pattern that appears constantly in AI-generated payment and credits endpoints:

// Typical AI-generated credits deduction
async function deductCredits(userId: string, amount: number) {
  const user = await db.users.findUnique({ where: { id: userId } });

  if (user.credits < amount) {
    throw new Error("Insufficient credits");
  }

  await db.users.update({
    where: { id: userId },
    data: { credits: user.credits - amount },
  });
}

This reads fine. But two requests arriving within milliseconds of each other will both read the same credits value, both pass the check, and both perform the deduction. A user with 10 credits can spend 20.

This is a time-of-check to time-of-use (TOCTOU) vulnerability. It has real financial consequences, and it's completely invisible until production load creates the right timing or an attacker deliberately engineers it.

The fix is to push the conditional logic into the database query itself, so the check and the update happen atomically:

// Atomic update: the WHERE clause does the guard
const result = await db.users.updateMany({
  where: {
    id: userId,
    credits: { gte: amount },
  },
  data: {
    credits: { decrement: amount },
  },
});

if (result.count === 0) {
  throw new Error("Insufficient credits");
}

Now if two requests race, only one will match the WHERE clause. The other gets zero rows updated and throws. No double-spend.

Token invalidation

Another common failure: one-time tokens. AI-generated password reset flows frequently look like this:

async function consumeResetToken(token: string) {
  const record = await db.resetTokens.findFirst({
    where: { token, used: false },
  });

  if (!record) throw new Error("Invalid or expired token");

  await db.resetTokens.update({
    where: { id: record.id },
    data: { used: true },
  });

  return record.userId;
}

Two concurrent requests with the same token both read used: false. Both pass the check. The token gets consumed twice, and an attacker who intercepts a reset link can race the legitimate user to exploit it.

Same fix: make the invalidation conditional on the current state.

const result = await db.resetTokens.updateMany({
  where: { token, used: false },
  data: { used: true },
});

if (result.count === 0) {
  throw new Error("Invalid or expired token");
}

One request wins. The other finds nothing to update and fails cleanly.

Rate limiting is not immune

Vibe coders often reach for in-memory rate limiting: increment a counter in a Map and check it before processing a request. This breaks the moment you run more than one process, which happens immediately on platforms that auto-scale or run multiple workers.

Even with Redis, the read-increment-write sequence has the same TOCTOU hole unless you use an atomic command. INCR in Redis is atomic. A GET followed by a SET is not.

# Wrong: two requests can both read 0, both write 1
redis-cli GET rate:user:123
redis-cli SET rate:user:123 1

# Right: INCR is atomic
redis-cli INCR rate:user:123

If you're using a library like @upstash/ratelimit, this is handled for you. If you're rolling your own, you need atomic operations, not a fetch-then-store sequence.

Slug generation: a quieter version of the same bug

This one is rarely framed as a security issue, but it has real operational consequences. AI-generated slug generation for posts or user profile URLs often does a lookup to check uniqueness, then inserts a new row. Under concurrent writes, duplicates get through.

The reliable fix is a unique constraint at the database level plus a retry loop on conflict. The database constraint is the real guard. The application-level check is just an optimisation to produce a better error message.

Why this matters more for AI-assisted code

Traditional developers hit these issues too. The difference is that a developer who wrote the code knows there's a database involved, knows concurrency exists, and at minimum knows to search for how to do an atomic update.

AI-generated code arrives as a finished-looking implementation. The developer reviewing it sees logic that appears complete and correct. There's no obvious gap to investigate. The race condition stays hidden until production load or a deliberate exploit surfaces it.

The practical advice is not "don't use AI for API code." It's this: treat AI-generated read-then-write patterns as suspects. Any time you see a fetch followed by a conditional followed by a write, ask whether those operations need to be atomic. Often, they do.

Seaworthy's static analysis checks for TOCTOU patterns in your API routes and flags read-modify-write sequences that lack transactional guards.

Este artículo fue generado por IA y resume fuentes públicamente disponibles.

De los creadores de Seaworthy

Seaworthy analiza tu repositorio en busca de los problemas que se tratan en artículos como este.

Brechas de seguridad, secretos expuestos y configuraciones erróneas detectados antes de desplegar. Gratis, sin cuenta necesaria.

$ npx seaworthycode