Row-level locking with select_for_update()
coreadvancedselect_for_update() adds SQL's SELECT ... FOR UPDATE — it locks the matched rows until the enclosing transaction ends, so no other transaction can lock (or update) those same rows until this one commits or rolls back. It MUST be inside transaction.atomic() — calling it in autocommit mode raises TransactionManagementError. nowait=True fails immediately instead of waiting if a row is already locked; skip_locked=True silently skips already-locked rows instead. of=(...) narrows which of several JOINed tables actually get locked.
Think of it as
A normal SELECT never blocks anything — it reads a snapshot and moves on, letting other transactions freely read or write the same rows. SELECT ... FOR UPDATE changes that: it says "I intend to update these rows, so hold them for me until I'm done" — any OTHER transaction trying to lock (or update) the same rows has to wait until this transaction commits or rolls back. This is exactly the tool for "read a value, then write based on it, and nobody else may sneak in a conflicting write in between" — the classic read-then-write race. The lock only lasts as long as the enclosing transaction, which is precisely why atomic() is required: without an explicit transaction boundary, there is no well-defined moment for the lock to be released.
What we're doing: Safely decrement inventory quantity, guaranteeing no two concurrent requests both see quantity=1 and both decrement it, resulting in -1.
- 2
- select_for_update() here means a SECOND concurrent call to reserve_item() for the same item BLOCKS at this line until the first call's transaction commits or rolls back — it cannot read a stale quantity while the first is still deciding.
- 3
- By the time this line runs, item.quantity reflects the truly current value — no other transaction could have changed it since the lock was acquired.
Why this works: Without select_for_update(), two concurrent requests could both read quantity=1 at nearly the same moment, both decide "still in stock," and both decrement — ending at quantity=-1, having sold an item that didn't exist. Locking the row for the duration of the transaction serializes exactly the two operations that need to be serialized, while leaving every OTHER row (every other item) completely unaffected.
Calling select_for_update() outside any transaction.atomic() block
Wrong
Better
What you see: django.db.transaction.TransactionManagementError: select_for_update cannot be used outside of a transaction. — raised as soon as the queryset is evaluated, on backends that support SELECT ... FOR UPDATE.
Why: A lock acquired by SELECT ... FOR UPDATE is released when the enclosing transaction ends — in Django's default autocommit mode, there IS no enclosing transaction (every statement commits immediately on its own), so there would be no well-defined moment for the lock to ever be released. Django raises this error specifically to prevent that undefined, dangerous state rather than silently doing something unpredictable.
- Request A → InventoryItem row: select_for_update().get(pk=...)
- Request B → InventoryItem row: select_for_update().get(pk=...) — blocks
- Request A → InventoryItem row: quantity -= 1; save(); commit
- InventoryItem row → Request B: lock released — now proceeds
select_for_update() arguments
Together
Remember: select_for_update() locks matched rows for the duration of the enclosing transaction.atomic() block — required, or it raises TransactionManagementError. nowait=True fails fast instead of waiting; skip_locked=True silently skips locked rows. Combined with select_related() on a NULLABLE relation, it needs .exclude(field=None) first, or it raises NotSupportedError.
See also: use cases and limits · atomic and nested blocks · locks and deadlocks

