Race conditions in CRUD apps can cause duplicate orders and wrong totals. Learn common collision points and fixes with constraints, locks, and UX guards.

A race condition happens when two (or more) requests update the same data almost at the same time, and the final result depends on timing. Each request looks correct on its own. Together, they produce a wrong outcome.
A simple example: two people click Save on the same customer record within a second. One updates the email, the other updates the phone number. If both requests send the full record, the second write can overwrite the first, and one change disappears with no error.
You see this more in fast apps because users can trigger more actions per minute. It also spikes during busy moments: flash sales, end-of-month reporting, a big email campaign, or any time a backlog of requests hits the same rows.
Users rarely report "a race condition." They report symptoms: duplicate orders or comments, missing updates ("I saved it, but it went back"), strange totals (inventory goes negative, counters jump backward), or statuses that flip unexpectedly (approved, then back to pending).
Retries make it worse. People double-click, refresh after a slow response, submit from two tabs, or deal with flaky networks that cause browsers and mobile apps to resend. If the server treats every request as a fresh write, you can get two creates, two payments, or two status changes that were meant to happen once.
Most CRUD apps feel simple: read a row, change a field, save it. The catch is that your app doesn't control timing. The database, the network, retries, background work, and user behavior all overlap.
One common trigger is two people editing the same record. Both load the same "current" values, both make valid changes, and the last save silently overwrites the first. Nobody did anything wrong, but one update is lost.
It also happens with one person. A double click on a Save button, tapping back and forward, or a slow connection that pushes someone to press Submit again can send the same write twice. If the endpoint isn't idempotent, you can create duplicates, charge twice, or move a status forward two steps.
Modern usage adds more overlap. Multiple tabs or devices signed into the same account can fire conflicting updates. Background jobs (emails, billing, sync, cleanup) can touch the same rows as web requests. Automatic retries at the client, load balancer, or job runner can repeat a request that already succeeded.
If you're shipping features quickly, the same record often gets updated from more places than anyone remembers. If you're using a chat-driven builder like Koder.ai, the app can grow even faster, so it's worth treating concurrency as normal behavior, not an edge case.
Race conditions rarely show up in "create a record" demos. They show up where two requests touch the same piece of truth at nearly the same moment. Knowing the usual hotspots helps you design safe writes from day one.
Anything that feels like "just add 1" can break under load: likes, view counts, totals, invoice numbers, ticket numbers. The risky pattern is read the value, add, then write it back. Two requests can read the same starting value and overwrite each other.
Workflows like Draft -> Submitted -> Approved -> Paid look straightforward, but collisions are common. Trouble starts when two actions are possible at once (approve and edit, cancel and pay). Without guards, you can end up with a record that skips steps, flips back, or shows different states in different tables.
Treat status changes like a contract: allow only the next valid step and refuse anything else.
Seats left, stock counts, appointment slots, and "capacity remaining" fields create the classic oversell problem. Two buyers check out at the same time, both see availability, and both succeed. If the database isn't the final judge, you'll eventually sell more than you have.
Some rules are absolute: one email per account, one active subscription per user, one open cart per user. These often fail when you check first ("does one exist?") and then insert. Under concurrency, both requests can pass the check.
If you're generating CRUD flows quickly (for example, by chatting your app into existence on Koder.ai), write these hotspots down early and back them with constraints and safe writes, not just UI checks.
A lot of race conditions start with something boring: the same action gets sent twice. Users double-click. The network is slow so they click again. A phone registers two taps. Sometimes it isn't intentional at all: the page refreshes after a POST and the browser offers to resubmit.
When that happens, your backend can run two creates or updates in parallel. If both succeed, you get duplicates, wrong totals, or a status change that runs twice (for example, approve plus another approve). It looks random because it depends on timing.
The safest approach is defense in depth. Fix the UI, but assume the UI will fail.
Practical changes you can apply to most write flows:
Example: a user taps "Pay invoice" twice on mobile. The UI should block the second tap. The server should also reject the second request when it sees the same idempotency key, returning the original success result instead of charging twice.
Status fields feel simple until two things try to change them at once. A user clicks Approve while an automated job marks the same record Expired, or two team members work the same item in different tabs. Both updates can succeed, but the final status depends on timing, not your rules.
Treat status as a small state machine. Keep a short table of allowed moves (for example: Draft -> Submitted -> Approved, and Submitted -> Rejected). Then every write checks: "Is this move allowed from the current status?" If not, reject it instead of silently overwriting.
Optimistic locking helps you catch stale updates without blocking other users. Add a version number (or updated_at) and require it to match when you save. If someone else changed the row after you loaded it, your update affects zero rows and you can show a clear message like "This item changed, refresh and try again."
A simple pattern for status updates is:
Also, keep status changes in one place. If updates are scattered across screens, background jobs, and webhooks, you'll miss a rule. Put them behind a single function or endpoint that enforces the same transition checks every time.
The most common counter bug looks harmless: the app reads a value, adds 1, then writes it back. Under load, two requests can read the same number and both write the same new number, so one increment is lost. This is easy to miss because it "usually works" in testing.
If a value is just being incremented or decremented, let the database do it in one statement. Then the database applies changes safely even when many requests hit at once.
UPDATE posts
SET like_count = like_count + 1
WHERE id = $1;
The same idea applies to inventory, view counts, retry counters, and anything that can be expressed as "new = old + delta".
Totals often go wrong when you store a derived number (order_total, account_balance, project_hours) and then update it from multiple places. If you can compute the total from source rows (line items, ledger entries), you avoid a whole class of drift bugs.
When you must store a total for speed, treat it like a critical write. Keep updates to source rows and the stored total in the same transaction. Ensure only one writer can update the same total at a time (locking, guarded updates, or a single owner path). Add constraints that prevent impossible values (for example, negative inventory). Then reconcile occasionally with a background check that recomputes and flags mismatches.
A concrete example: two users add items to the same cart at the same time. If each request reads cart_total, adds its item price, and writes back, one addition can disappear. If you update the cart items and the cart total together in one transaction, the total stays correct even under heavy parallel clicks.
If you want fewer race conditions, start in the database. App code can retry, time out, or run twice. A database constraint is the final gate that stays correct even when two requests hit at the same time.
Unique constraints stop duplicates that "should never happen" but do: email addresses, order numbers, invoice IDs, or a "one active subscription per user" rule. When two signups land together, the database accepts one row and rejects the other.
Foreign keys prevent broken references. Without them, one request can delete a parent record while another creates a child record that points to nothing, leaving orphan rows that are hard to clean up later.
Check constraints keep values in a safe range and enforce simple state rules. For example, quantity >= 0, rating between 1 and 5, or status limited to an allowed set.
Treat constraint failures as expected outcomes, not "server errors." Catch unique, foreign key, and check violations, return a clear message like "That email is already in use," and log details for debugging without leaking internals.
Example: two people click "Create order" twice during lag. With a unique constraint on (user_id, cart_id), you don't get two orders. You get one order and one clean, explainable rejection.
Some writes aren't a single statement. You read a row, check a rule, update a status, and maybe insert an audit log. If two requests do that at the same time, they can both pass the check and both write. That's the classic failure pattern.
Wrap the multi-step write in one database transaction so all steps succeed together or none do. More importantly, the transaction gives you a place to control who is allowed to change the same data at the same time.
When only one actor can edit a record at a time, use a row-level lock. For example: lock the order row, confirm it's still in "pending" state, then flip it to "approved" and write the audit entry. The second request will wait, then re-check the state and stop.
Choose based on how often collisions happen:
Keep lock time short. Do as little work as possible while holding it: no external API calls, no slow file work, no big loops. If you're building flows in a tool like Koder.ai, keep the transaction around just the database steps, then do the rest after commit.
Pick one flow that can lose money or trust when it collides. A common one is: create an order, reserve stock, then set the order status to confirmed.
Write down the exact steps your code takes today, in order. Be specific about what is read, what is written, and what "success" means. Collisions hide in the gap between a read and a later write.
A hardening path that works in most stacks:
Add one test that proves the fix. Run two requests at the same time against the same product and quantity. Assert that exactly one order becomes confirmed, and the other fails in a controlled way (no negative stock, no duplicate reservation rows).
If you generate apps quickly (including with platforms like Koder.ai), this checklist is still worth doing on the few write paths that matter most.
One of the biggest causes of race conditions is trusting the UI. Disabled buttons and client-side checks help, but users can double-click, refresh, open two tabs, or replay a request from a flaky connection. If the server isn't idempotent, duplicates slip through.
Another quiet bug: you catch a database error (like a unique constraint violation) but continue the workflow anyway. That often turns into "create failed, but we still sent the email" or "payment failed, but we still marked the order paid." Once side effects happen, it's hard to unwind.
Long transactions are also a trap. If you keep a transaction open while calling email, payments, or third-party APIs, you hold locks longer than needed. That increases waiting, timeouts, and the chance that requests block each other.
Mixing background jobs and user actions without a single source of truth creates split-brain state. A job retries and updates a row while a user is editing it, and now both think they were the last writer.
A few "fixes" that don't actually fix it:
If you're building with a chat-to-app tool like Koder.ai, the same rules apply: ask for server-side constraints and clear transactional boundaries, not just nicer UI guards.
Race conditions often show up only under real traffic. A pre-ship pass can catch the most common collision points without a rewrite.
Start with the database. If something must be unique (emails, invoice numbers, one active subscription per user), make it a real unique constraint, not an app-level "we check first" rule. Then make sure your code expects the constraint to sometimes fail and returns a clear, safe response.
Next, look at state. Any status change (Draft -> Submitted -> Approved) should be validated against an explicit set of allowed transitions. If two requests try to move the same record, the second one should be rejected or become a no-op, not create an in-between state.
A practical pre-release checklist:
If you build flows in Koder.ai, treat these as acceptance criteria: the generated app should fail safely under repeats and concurrency, not just pass the happy path.
Two staff members open the same purchase request. Both click Approve within a few seconds. Both requests hit the server.
What can go wrong is messy: the request is "approved" twice, two notifications go out, and any totals tied to approvals (budget used, daily approvals count) can jump by 2. Both updates are valid on their own, but they collide.
Here is a fix plan that works well with a PostgreSQL-style database.
Add a rule that guarantees only one approval record can exist for a request. For example, store approvals in a separate table and enforce a unique constraint on request_id. Now the second insert fails even if the app code has a bug.
When approving, do the whole transition in one transaction:
If the second staff member arrives late, they either see 0 rows updated or a unique-constraint error. Either way, only one change wins.
After the fix, the first staff member sees Approved and gets the normal confirmation. The second staff member sees a friendly message like: "This request was already approved by someone else. Refresh to see the latest status." No spinning, no duplicate notifications, no silent failures.
If you're generating a CRUD flow in a platform like Koder.ai (Go backend with PostgreSQL), you can bake these checks into the approve action once and reuse the pattern for other "only one winner" actions.
Race conditions are easiest to fix when you treat them like a repeatable routine, not a one-off bug hunt. Focus on the few write paths that matter most, and make them boringly correct before you polish anything else.
Start by naming your top collision points. In many CRUD apps it's the same trio: counters (likes, inventory, balances), status changes (Draft -> Submitted -> Approved), and double submits (double-clicking, retries, slow networks).
A routine that holds up:
If you're building on Koder.ai, Planning Mode is a practical place to map each write flow as steps and rules before generating changes in Go and PostgreSQL. Snapshots and rollback are also handy when you're shipping new constraints or lock behavior and want a fast way back if you hit an edge case.
Over time, this becomes a habit: every new write feature gets a constraint, a transaction plan, and a concurrency test. That's how race conditions in CRUD apps stop being surprises.