# Race condition on balance updates: Pessimistic Lock or Redlock?

> Keep the invariant in the DB with an atomic `UPDATE ... WHERE balance >= 40` or a `SELECT ... FOR UPDATE`, and save Redlock for non-DB resources.

- Asked: 2026-06-05
- Answered: 2026-06-08
- Asked by: Polat
- Tags: veritabani, queue, redis
- Source: https://www.muhammetsafak.com.tr/en/just-ask/preventing-race-conditions-on-balance-updates-pessimistic-vs-redlock/
- Language: en-US
- Author: Muhammet Şafak

---
**Question:** We have a queue job that deducts money from a user's balance. If the user triggers two operations in quick succession, two jobs for the same user end up in the queue; we run 10 parallel workers. Worker 1 and 2 read the old balance at the same time (100), both deduct 40 and write 60 (it should be 20).

To prevent this race condition, should I use Pessimistic Locking or a Redis distributed lock (Redlock)?


Short answer: what you're hitting is the classic **lost-update** problem. When money is involved, keep the invariant in the DB; the cleanest fix is an atomic conditional `UPDATE`, with `SELECT ... FOR UPDATE` as the second choice. Don't reach for Redlock.

The core of your scenario: the read-modify-write cycle is split across parallel workers. Both read the old value, both write over it, and one update is lost. The fix is to make that cycle atomic.

1. **Best: an atomic conditional `UPDATE`.** Write `UPDATE accounts SET balance = balance - 40 WHERE id = ? AND balance >= 40`. Read and write are one statement; no app-side lock needed. The DB manages the row lock itself, which both resolves the race and refuses to overdraw via the `balance >= 40` condition. If zero rows are affected, the operation failed on insufficient funds.
2. **Alternative: `SELECT ... FOR UPDATE`.** If your logic between read and write is complex, lock the balance row inside a transaction with `FOR UPDATE`. The second worker that reaches that row waits for the first to commit; they serialize, no clash. That's pessimistic locking.
3. **Optimistic locking works under low contention too.** Add a `version` column to the row; on update include `WHERE version = ?` and retry if it doesn't match. Cheap when conflicts are rare, but it creates a retry storm under heavy contention.
4. **Use Redlock only for a non-DB resource.** A Redis distributed lock (Redlock) makes sense when the thing you're protecting *isn't* a single DB row (cross-service/cross-resource coordination) — and it has edge cases around clock drift/timeouts. When a balance can already be protected by the DB's transaction, don't entrust it to a distributed lock.

**Bottom line:** use an atomic conditional `UPDATE` (or `SELECT FOR UPDATE`); save Redlock for non-DB resources. For money, keep the rule where the transaction guarantees it — in the DB; a distributed lock is too fragile a tool to protect an invariant the database itself could enforce.

## Related Reading

- [Async Job Processing with Laravel Queue and Supervisor](/en/blog/async-job-processing-with-laravel-queue-and-supervisor/) — Blog
- [Why should I use a Sorted Set for a live leaderboard in Redis?](https://www.muhammetsafak.com.tr/en/just-ask/redis-data-types-using-sorted-sets-for-a-leaderboard/) — Just Ask
- [How should I carry a correlation ID from the HTTP request all the way into queued jobs?](https://www.muhammetsafak.com.tr/en/just-ask/how-should-i-carry-a-correlation-id-from-the-http-request/) — Just Ask
- [Should I use a partial index on a queue table where I only ever scan 'pending' rows?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-use-a-partial-index-on-a-queue-table-where/) — Just Ask
