Run things at the same time safely
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.
- You know which data in your app is shared: balances, stock, inventory, counters.
- You look for places where code checks a value and then acts on it in a separate step.
Part 2: Protect your writes
Check 2: Group related writes so they all succeed or all fail
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.
- Every action that writes more than once (like moving money or items between two rows) runs inside one transaction.
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.
- When you read a row so you can change it, you lock it with
FOR UPDATEinside a transaction.
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)
- For balances, stock and counters, you either lock the row (Check 3) or use a stricter isolation level and handle conflicts (Check 5).
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)
- When a transaction fails because of a conflict, your code retries the whole thing from the start.
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.
- Every form that creates, pays or changes something sends a unique key, and the server ignores repeats of that key.
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.
- If you use Supabase, multi-step changes to shared data live in a database function you call with
rpc(), and you've tested what happens when a step fails.
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)
- Trades, purchases and item moves run as one oxmysql transaction.
- Your script checks the returned true or false before telling the player it worked.
- You back up your database before changing these scripts (how to back up).
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
- Back up before you change data: lock the database, back it up, and log what happens
- Have a way back: deploy safely with a way to roll back
- Control what your agent can run: AI agent permissions for beginners
Sources
All checked Oct 15, 2026:
- MITRE CWE-362, race condition
- MITRE CWE-367, time-of-check time-of-use race condition
- PostgreSQL transaction isolation
- PostgreSQL explicit locking
- PostgreSQL SELECT, locking clause
- Supabase database functions
- Prisma ORM transactions
- oxmysql transaction
- Stripe idempotent requests
- MDN, idempotent
- RFC 9110, HTTP semantics
- Redis transactions
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.