# 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://muhammetsafak.com/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.

## Short answer

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. Once you move to row locking with `FOR UPDATE`, the lock ordering has to stay consistent; I covered how deadlocks arise and how to prevent them in [the deadlock answer](/just-ask/detecting-and-preventing-database-deadlocks/).

## Why

1. **The database, not the app, should protect the invariant.** When read and write collapse into one statement, the DB manages the row lock itself — which both resolves the race and refuses to overdraw via a condition like `balance >= 40`.
2. **Redlock is too fragile for an in-DB invariant.** 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.
3. **Optimistic locking backfires under contention.** The `version`-column approach is cheap when conflicts are rare, but it creates a retry storm under heavy contention — and in your scenario the same user fires jobs back to back.

## What to do

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. 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. **Consider optimistic locking under low contention.** Add a `version` column to the row; on update include `WHERE version = ?` and retry if it doesn't match.
4. **Use Redlock only for a non-DB resource.** If what you're protecting isn't a DB row — a cross-service resource, say — that's when a distributed lock earns its place.

**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](/blog/async-job-processing-with-laravel-queue-and-supervisor/) — Blog
- [Why should I use a Sorted Set for a live leaderboard in Redis?](https://muhammetsafak.com/just-ask/redis-data-types-using-sorted-sets-for-a-leaderboard/) — Just Ask
- [When should I switch to a covering index with INCLUDE to get an index-only scan?](https://muhammetsafak.com/just-ask/switch-covering-index-include-get-index-only-scan/) — Just Ask
- [How should I carry a correlation ID from the HTTP request all the way into queued jobs?](https://muhammetsafak.com/just-ask/how-should-i-carry-a-correlation-id-from-the-http-request/) — Just Ask
