Django cookbook · Intermediate
Update stock safely with transactions, F() and select_for_update
Two customers buy the last item at the same time and both orders succeed.
Approach
Do the check and the update in the database, atomically. A conditional UPDATE ... SET stock = stock - 1 WHERE stock >= 1 (with F()) is the simplest; when you need the row in Python, lock it with select_for_update()
inside transaction.atomic().
Solution
from django.db import transaction
from django.db.models import F
from shop.models import Order, Product
class OutOfStock(Exception):
pass
def reserve(product_id: int, quantity: int) -> None:
updated = Product.objects.filter(id=product_id, stock__gte=quantity).update(stock=F("stock") - quantity)
if updated == 0:
raise OutOfStock(product_id)
@transaction.atomic
def place_order(user, product_id: int, quantity: int) -> Order:
product = Product.objects.select_for_update().get(id=product_id) # row locked until commit
if product.stock < quantity:
raise OutOfStock(product_id)
product.stock -= quantity
product.save(update_fields=["stock"])
return Order.objects.create(customer=user, product=product, quantity=quantity, unit_price=product.price)
Pitfalls
- Reading
product.stock, checking it in Python and saving later without a lock or conditional update. - Calling external services (payments, email) inside the atomic block while rows are locked.
select_for_update()outside a transaction, which raises an error.
Variations
- Add a
CheckConstraint(condition=Q(stock__gte=0), name="stock_not_negative")as a safety net. - Use
select_for_update(skip_locked=True)for job-queue style tables.
Further reading: select_for_update
Reference: https://docs.djangoproject.com/en/6.0/ref/models/querysets/#select-for-update
More Django recipes
- Add a required column without downtime
- Add full-text search with PostgreSQL
- Add health checks and structured logging
- Avoid N+1 queries with select_related and prefetch_related
- Cache expensive queries and invalidate on change
- Call external APIs concurrently in async views
- Configure settings from environment variables
- Enforce rules with database constraints