Get featured

Run things at the same time safely

Guide · checked against the docs on

Two players buy the last item at the same moment. A user double-clicks "Pay". Two people edit the same row. If your app isn't ready for that, one change can quietly overwrite another, or the same action can happen twice.

This guide is a checklist of 8 checks for anyone building a small web app or a FiveM/RedM server script with AI coding agents. It's quoted from MITRE, PostgreSQL, Supabase, Prisma, oxmysql, Stripe, MDN, IETF and Redis docs, checked Oct 2026.

Not affiliated with or endorsed by MITRE, the PostgreSQL project, Supabase, Prisma, Overextended, Stripe, Mozilla, the IETF or Redis. Check their current docs before relying on these details.

Before you change how your app writes data, back it up. See Before you ship: lock the database, back it up, and log what happens and Deploy safely with a way to roll back. This guide doesn't repeat them.

Part 1: Know the problem

Check 1: Know what a race condition is

MITRE describes a race condition as code that "requires temporary, exclusive access to a shared resource, but a timing window exists in which the shared resource can be modified by another code sequence operating concurrently." It adds: "A race condition occurs within concurrent environments, and it is effectively a property of a code sequence." (source)

A common shape is "check, then act": your code checks something (is there stock? does the player have enough money?), then acts on it. MITRE calls this a time-of-check time-of-use race. In its own example, "the file could have been updated between the time of the check and the lstat." (source) In plain words: the thing you checked can change before you use it.

Part 2: Protect your writes

Prisma's docs put it simply: "Run several writes so they all succeed or all fail together". They say to "Use a transaction whenever one business operation spans more than one write: creating a user with their first records, moving a value between two rows, or deleting a parent after its children." (source)

If you use Prisma, follow the Prisma transactions page for the code. The page documents a newer API, and the syntax differs between Prisma versions, so check which version your project uses.

Check 3: Lock the row you're about to change

When two users update the same row, PostgreSQL can lock it for you: "FOR UPDATE causes the rows retrieved by the SELECT statement to be locked as though for update." Other transactions that try to change or lock those rows "will be blocked until the current transaction ends". (source)

If you'd rather not wait: "To prevent the operation from waiting for other transactions to commit, use either the NOWAIT or SKIP LOCKED option." (source)

Locks can also get stuck waiting on each other, which is called a deadlock. PostgreSQL has a deadlocks section on the same page.

Check 4: Don't read, change and write back without protection

A "lost update" happens when two requests read the same value, both change it in app code, and the second write erases the first. The HTTP standard calls this "one client accidentally overwriting the work of another client that has been acting in parallel." (source) Know your database's default: "Read Committed is the default isolation level in PostgreSQL." At that level, a plain SELECT "sees only data committed before the query began." (source) Prisma notes that "Every transaction runs at the database's default isolation level, READ COMMITTED on PostgreSQL, and you cannot ask for another level yet." (source)

The strictest level is Serializable. PostgreSQL says the standard guarantees that "any concurrent execution of a set of Serializable transactions is guaranteed to produce the same effect as running them one at a time in some order." The catch: "Applications using this level must be prepared to retry transactions due to serialization failures." (source)

Check 5: Plan for retries when a write is refused

Sometimes the database refuses your write because of a conflict. Prisma explains: "When PostgreSQL cannot commit your transaction because another transaction touched the same rows, it aborts yours." And: "Prisma ORM does not retry write conflicts for you, so write the retry yourself". (source)

PostgreSQL says what to do: "When an application receives this error message, it should abort the current transaction and retry the whole transaction from the beginning." (source)

Part 3: Double clicks and repeat requests

Check 6: Make submits safe to send twice

Some requests are safe to repeat and some aren't. MDN defines it: "An HTTP method is idempotent if the intended effect on the server of making a single request is the same as the effect of making several identical requests." But: "The POST and PATCH methods are not guaranteed to be idempotent." (source) The HTTP standard explains why it matters: idempotent requests "can be repeated automatically if a communication failure occurs before the client is able to read the server's response." (source)

Most form submits are POST, so a double click or a retry can run the action twice. One fix is an idempotency key. Stripe describes the idea: "A client generates an idempotency key, which is a unique key that the server uses to recognize subsequent retries of the same request." For the key, Stripe suggests "using V4 UUIDs, or another random string with enough entropy to avoid collisions." (source) You can use the same idea in your own app: give each submit a unique key, and if the server sees that key again, it doesn't repeat the action.

Our advice, not a vendor quote: disabling the submit button after the first click helps, but it isn't enough on its own. The server still needs to ignore repeats.

Check 7: Keep multi-step logic in the database when you can

Supabase says "Postgres has built-in support for SQL functions." And: "These functions live inside your database, and you can call them from your app with rpc()." (source)

Our advice, not a vendor quote: putting a check-then-change step in one database function means your app makes one call instead of several. Supabase's functions page doesn't say a function runs as one all-or-nothing transaction, so test how yours behaves if a step fails before you rely on it.

Part 4: Game servers and caches

Check 8: Game servers: wrap item and money changes in a transaction

If your server uses oxmysql, it has transactions: "A transaction executes multiple queries and commits them only if all succeed." And: "If one fails, none of the queries are committed." Check the result, too: "The return value is a boolean, which is the result of the transaction." (source)

Only if you already use Redis: "WATCH is used to provide a check-and-set (CAS) behavior to Redis transactions." "If at least one watched key is modified before the EXEC command, the whole transaction aborts, and EXEC returns a Null reply to notify that the transaction failed." (source) You don't need to add Redis for this.

Ask your agent to find the risky spots

Give your agent a prompt like this (we haven't tried it ourselves):

Find every place where this app reads a value, changes it, then
writes it back (balances, stock, inventory, counters). For each,
wrap the steps in one transaction, lock the row or use a
conditional update, add a retry on write conflicts, and make form
submits safe to send twice with a unique request key. Show me the
changes before applying them and don't touch the live database.

See AI agent permissions for beginners to make the agent ask before it changes anything.

Next steps

Sources

All checked Oct 15, 2026:

Not affiliated with or endorsed by MITRE, the PostgreSQL project, Supabase, Prisma, Overextended, Stripe, Mozilla, the IETF or Redis. Check their current docs before relying on these details.