Filter concepts by levelShowing all levels.

Django · Section 30

select_for_update()

Level
advanced
Read
20 min
Concepts
2

select_for_update() adds SQL's SELECT ... FOR UPDATE, locking matched rows for the duration of the enclosing transaction.atomic() block — required, or it raises TransactionManagementError. nowait/skip_locked control what happens when a row is already locked; of=(...) narrows locking to specific joined tables; combining it with select_related() on a nullable relation needs .exclude(field=None) first. The real use cases — inventory reservation, financial balance changes, workflow state transitions — all share the same shape: read a row, decide based on its current value, then write, with no other transaction allowed to interleave. The roadmap's own explicit caveat: locking is not a generic concurrency solution — F() is cheaper for a simple unconditional change, and a unique constraint is the right tool for "must never be duplicated." Testing it realistically requires TransactionTestCase, not TestCase, whose own wrapping transaction interferes with select_for_update()'s real locking semantics.

This section

What is true here

  1. select_for_update() locks matched rows for the duration of the enclosing transaction — it must be inside transaction.atomic(), or it raises TransactionManagementError.
  2. nowait=True fails immediately if a row is already locked; skip_locked=True silently excludes already-locked rows — they are mutually exclusive.
  3. Combined with select_related() on a NULLABLE foreign key, it raises NotSupportedError unless rows with a NULL relation are excluded first.
  4. The right shape for select_for_update() is read-then-decide-then-write — inventory, balances, workflow transitions; a simple unconditional change is cheaper with F(), and "must never be duplicated" is better served by a unique constraint.
  5. TestCase wraps every test in its own transaction, which interferes with testing select_for_update()'s real locking behavior — TransactionTestCase is required to test it realistically.

What you will be able to do

  • Use select_for_update() correctly inside a transaction.atomic() block
  • Choose nowait/skip_locked/of appropriately for a given locking scenario
  • Recognize when F() or a unique constraint is the better, lighter tool instead
  • Test select_for_update() behavior correctly with TransactionTestCase

Row-level locking

SELECT ... FOR UPDATE, the atomic() requirement, and the arguments that control locking behavior.

Row-level locking with select_for_update()

coreadvanced

select_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.

python
with transaction.atomic():
    obj = Model.objects.select_for_update().get(pk=pk)
    # ... read obj's current state and write based on it, safely ...

What we're doing: Safely decrement inventory quantity, guaranteeing no two concurrent requests both see quantity=1 and both decrement it, resulting in -1.

inventory/services.pypython
def reserve_item(item_id):
    with transaction.atomic():
        item = InventoryItem.objects.select_for_update().get(pk=item_id)
        if item.quantity <= 0:
            raise OutOfStock(item_id)
        item.quantity -= 1
        item.save(update_fields=["quantity"])
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

python
item = InventoryItem.objects.select_for_update().get(pk=item_id)   # autocommit mode
# TransactionManagementError

Better

python
with transaction.atomic():
    item = InventoryItem.objects.select_for_update().get(pk=item_id)

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.

A second concurrent call waits for the lock to release
Request A
InventoryItem row
Request B
  1. 1. select_for_update().get(pk=...)
  2. 2. select_for_update().get(pk=...) — blocks
  3. 3. quantity -= 1; save(); commit
  4. 4. lock released — now proceeds
  1. Request A → InventoryItem row: select_for_update().get(pk=...)
  2. Request B → InventoryItem row: select_for_update().get(pk=...) — blocks
  3. Request A → InventoryItem row: quantity -= 1; save(); commit
  4. InventoryItem row → Request B: lock released — now proceeds

select_for_update() arguments

select_for_update() arguments
ArgumentBehavior
(default)wait for the lock to become available
nowait=Trueraise DatabaseError immediately if already locked
skip_locked=Truesilently exclude already-locked rows from the result
of=(...)lock only the specified tables, not every joined table

Together

python
with transaction.atomic():
    item = InventoryItem.objects.select_for_update().get(pk=item_id)
    if item.quantity > 0:
        item.quantity -= 1
        item.save()

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

Advertisement

When to reach for it, and when not to

Real use cases, lighter alternatives, and the testing gotcha specific to this method.

When to reach for it, and when not to

coreadvanced

select_for_update() fits a specific shape: read a row, decide something based on its CURRENT value, then write — inventory reservation, financial balance changes, workflow state transitions (only allow PENDING → PAID, never twice). It is not a generic answer to every concurrency problem — F() expressions handle a simple atomic increment far more cheaply, and a unique constraint handles "this must never be duplicated" without any locking at all. Django's TestCase wraps every test in a transaction, which silently changes select_for_update()'s real behavior under test — TransactionTestCase is needed to test it properly.

Think of it as

select_for_update() earns its cost (holding a lock, blocking other transactions) specifically when the operation genuinely needs to READ before it can decide how to WRITE — "is there still inventory," "what is the current workflow state," "what is the current balance before applying this change." If the operation can be expressed as a pure, unconditional database-side computation instead (F("count") + 1, an UPSERT, a unique constraint), that is almost always cheaper and simpler than locking, because it never needs to read the current value in the first place — the database just computes the new value directly. Reaching for select_for_update() reflexively, even where a lighter tool would do, adds real lock contention for no benefit.

python
class MyTests(TransactionTestCase):   # not TestCase — needed to test select_for_update() realistically
    def test_concurrent_reservation(self):
        ...

What we're doing: Enforce a legal order-status transition (only PENDING can become PAID) safely against two concurrent requests trying to pay the same order.

orders/services.pypython
def mark_paid(order_id):
    with transaction.atomic():
        order = Order.objects.select_for_update().get(pk=order_id)
        if order.status != Order.Status.PENDING:
            raise InvalidTransition(f"Cannot pay an order in status {order.status}")
        order.status = Order.Status.PAID
        order.save(update_fields=["status"])
3
select_for_update() ensures a SECOND concurrent call to mark_paid() for the same order waits until the first call's transaction fully commits — it can never read the stale PENDING status while the first request is mid-decision.
4
This check is exactly the "read, then decide" step select_for_update() protects — without the lock, both concurrent calls could read status=PENDING before either writes, and both would (wrongly) proceed to mark the order paid.

Why this works: A workflow transition like this genuinely needs to read the CURRENT state before deciding whether the transition is legal — a plain F() expression has no way to express "only change if the current value is X," which is exactly the conditional-write shape select_for_update() (combined with an explicit check) is built for.

Reaching for select_for_update() for a simple counter increment, where F() would be both cheaper and simpler

Wrong

python
with transaction.atomic():
    article = Article.objects.select_for_update().get(pk=article_id)
    article.view_count += 1
    article.save()

Better

python
Article.objects.filter(pk=article_id).update(view_count=F("view_count") + 1)
# no transaction.atomic() needed, no lock held, no read-then-write race possible at all

What you see: Not a bug — unnecessary lock contention: a high-traffic page view counter holds a row lock (blocking other concurrent view-count updates to the SAME article) for no real benefit, since the operation never actually needed to read the current value in the first place.

Why: An unconditional increment has no decision to make based on the current value — F() lets the database perform "current value + 1" as a single atomic operation with no read step at all, which is both simpler code and avoids taking any lock whatsoever. select_for_update() is the heavier tool, worth its cost only when the write genuinely depends on validating something about the CURRENT value first.

select_for_update() vs. a lighter tool

select_for_update()

  • +Genuine read-then-decide-then-write need
  • +Inventory, balances, workflow transitions
  • +Costs a real lock — worth it only for this shape

F() / a unique constraint

  • A simple unconditional change → F()
  • No read step, no lock needed at all
  • "Never duplicated" → a unique constraint instead
  • select_for_update()
    • Genuine read-then-decide-then-write need
    • Inventory, balances, workflow transitions
    • Costs a real lock — worth it only for this shape
  • F() / a unique constraint
    • A simple unconditional change → F()
    • No read step, no lock needed at all
    • "Never duplicated" → a unique constraint instead

select_for_update() vs lighter alternatives

select_for_update() vs lighter alternatives
NeedRight tool
Read current value, validate, then write ("is there enough stock")select_for_update()
A simple, unconditional increment/decrementF("field") + 1 — no read-then-decide step needed
"This must never exist twice" (no read/decide logic)a unique constraint — the database rejects duplicates on its own
Enforce a legal state transition (PENDING → PAID, never twice)select_for_update() + an explicit check before the write

Together

python
# select_for_update(): a real read-then-decide-then-write need
with transaction.atomic():
    order = Order.objects.select_for_update().get(pk=order_id)
    if order.status != Order.Status.PENDING:
        raise InvalidTransition(order.status)
    order.status = Order.Status.PAID
    order.save()

# F(): no read-then-decide needed, cheaper
Article.objects.filter(pk=article_id).update(view_count=F("view_count") + 1)

Remember: select_for_update() is for a genuine read-then-decide-then-write need — inventory, balances, workflow transitions. A simple unconditional change is cheaper with F(); "must never be duplicated" is better served by a unique constraint. Use TransactionTestCase (not TestCase) to test select_for_update()'s real locking behavior — TestCase's own wrapping transaction interferes with it.

See also: row level locking · recognizing the pattern · f and q expressions

Advertisement