Introduction
Database transactions ensure that a series of database operations are executed as a single unit of work. Django provides transaction support through the django.db.transaction module.
The atomic() Decorator and Context Manager
The atomic() function is the main way to handle transactions in Django. It can be used as both a decorator and a context manager.
From Django’s source at /django/db/transaction.py:142:
class Atomic(ContextDecorator):
"""
Guarantee the atomic execution of a given block.
An instance can be used either as a decorator or as a context manager.
When it's used as a decorator, __call__ wraps the execution of the
decorated function in the instance itself, used as a context manager.
When it's used as a context manager, __enter__ creates a transaction or a
savepoint, depending on whether a transaction is already in progress, and
__exit__ commits the transaction or releases the savepoint on normal exit,
and rolls back the transaction or to the savepoint on exceptions.
"""
Using atomic() as a Decorator
from django.db import transaction
from myapp.models import Account, Transaction
@transaction.atomic
def transfer_funds(from_account_id, to_account_id, amount):
"""
Transfer funds between accounts.
All operations succeed or all fail.
"""
# Deduct from source account
from_account = Account.objects.get(id=from_account_id)
from_account.balance -= amount
from_account.save()
# Add to destination account
to_account = Account.objects.get(id=to_account_id)
to_account.balance += amount
to_account.save()
# Record the transaction
Transaction.objects.create(
from_account=from_account,
to_account=to_account,
amount=amount
)
# If any exception occurs, all changes are rolled back
Using atomic() as a Context Manager
from django.db import transaction
def process_order(order_data):
order = Order.objects.create(**order_data)
try:
with transaction.atomic():
# Create order items
for item_data in order_data['items']:
OrderItem.objects.create(
order=order,
**item_data
)
# Update inventory
for item_data in order_data['items']:
product = Product.objects.get(id=item_data['product_id'])
product.stock -= item_data['quantity']
product.save()
# If all succeeds, changes are committed
except Exception as e:
# If any error occurs, changes within atomic block are rolled back
order.status = 'failed'
order.save()
raise
When using atomic() as a decorator, the entire function executes within a transaction. When used as a context manager, only the code within the with block is transactional.
Autocommit Mode
By default, Django runs in autocommit mode. Each database query is immediately committed unless you’re inside an atomic() block.
from django.db import transaction
# Check autocommit status
is_autocommit = transaction.get_autocommit()
# Manually control autocommit (not recommended)
transaction.set_autocommit(False)
try:
# Your code here
transaction.commit()
except:
transaction.rollback()
finally:
transaction.set_autocommit(True)
Manually controlling autocommit is rarely necessary and can lead to bugs. Always prefer using atomic() for transaction management.
Nested Transactions with Savepoints
When you nest atomic() blocks, Django uses database savepoints to implement nested transactions.
From Django’s source at /django/db/transaction.py:48:
def savepoint(using=None):
"""
Create a savepoint (if supported and required by the backend) inside the
current transaction. Return an identifier for the savepoint that will be
used for the subsequent rollback or commit.
"""
return get_connection(using).savepoint()
def savepoint_rollback(sid, using=None):
"""
Roll back the most recent savepoint (if one exists). Do nothing if
savepoints are not supported.
"""
get_connection(using).savepoint_rollback(sid)
def savepoint_commit(sid, using=None):
"""
Commit the most recent savepoint (if one exists). Do nothing if
savepoints are not supported.
"""
get_connection(using).savepoint_commit(sid)
Nested atomic() Blocks
from django.db import transaction
@transaction.atomic
def create_user_with_profile(user_data, profile_data):
# Outer transaction
user = User.objects.create(**user_data)
try:
with transaction.atomic():
# Inner transaction (savepoint)
profile = Profile.objects.create(
user=user,
**profile_data
)
# Send welcome email (might fail)
send_welcome_email(user.email)
except EmailError:
# Profile creation rolled back, but user creation preserved
logger.error(f"Failed to send welcome email to {user.email}")
return user
Manual Savepoint Management
from django.db import transaction
def complex_operation():
with transaction.atomic():
# Create a savepoint
sid = transaction.savepoint()
try:
# Risky operation
perform_risky_operation()
# Commit the savepoint
transaction.savepoint_commit(sid)
except Exception:
# Roll back to savepoint
transaction.savepoint_rollback(sid)
# Continue with alternative approach
perform_alternative_operation()
Disabling Savepoints
You can disable automatic savepoint creation for performance:
@transaction.atomic(savepoint=False)
def bulk_operation():
# No savepoint created
# Slightly better performance
# But cannot be nested safely
for i in range(1000):
Model.objects.create(value=i)
Disable savepoints for bulk operations where you don’t need nested transaction support. This can improve performance in high-throughput scenarios.
Transaction Isolation and Locking
Select for Update
Lock rows to prevent concurrent modifications:
from django.db import transaction
@transaction.atomic
def decrement_stock(product_id, quantity):
# Lock the product row until transaction completes
product = Product.objects.select_for_update().get(id=product_id)
if product.stock < quantity:
raise ValueError("Insufficient stock")
product.stock -= quantity
product.save()
# Row is automatically unlocked when transaction commits
Select for Update Variants
# Wait for lock to be released (default)
product = Product.objects.select_for_update().get(id=1)
# Skip locked rows (PostgreSQL, Oracle, MySQL 8.0+)
available_products = Product.objects.select_for_update(skip_locked=True).filter(
stock__gt=0
)
# Raise exception if row is locked (PostgreSQL, Oracle, MySQL 8.0+)
try:
product = Product.objects.select_for_update(nowait=True).get(id=1)
except DatabaseError:
print("Product is currently locked")
# Lock related objects too
order = Order.objects.select_for_update(of=('self', 'customer')).get(id=1)
select_for_update() must be used inside a transaction (atomic() block). Using it outside a transaction will raise an exception.
Handling Transaction Errors
Rolling Back on Errors
from django.db import transaction, IntegrityError
def create_user(username, email):
try:
with transaction.atomic():
user = User.objects.create(
username=username,
email=email
)
# If this fails, user creation is also rolled back
Profile.objects.create(user=user)
return user
except IntegrityError as e:
# Handle duplicate username/email
raise ValueError(f"User creation failed: {e}")
Forcing Rollback
Use set_rollback() to force a rollback:
from django.db import transaction
def risky_operation():
with transaction.atomic():
# Make some changes
obj = Model.objects.create(value='test')
# Check some condition
if not validate_business_rules(obj):
# Force rollback without raising exception
transaction.set_rollback(True)
return False
return True
From Django’s source at /django/db/transaction.py:85:
def set_rollback(rollback, using=None):
"""
Set or unset the "needs rollback" flag -- for *advanced use* only.
When `rollback` is `True`, trigger a rollback when exiting the innermost
enclosing atomic block that has `savepoint=True` (that's the default). Use
this to force a rollback without raising an exception.
When `rollback` is `False`, prevent such a rollback. Use this only after
rolling back to a known-good state! Otherwise, you break the atomic block
and data corruption may occur.
"""
return get_connection(using).set_rollback(rollback)
on_commit Hooks
Execute code after a successful transaction commit:
from django.db import transaction
def send_notification(user_id):
# This function runs only if transaction succeeds
user = User.objects.get(id=user_id)
send_email(user.email, "Account created successfully")
@transaction.atomic
def create_user(username, email):
user = User.objects.create(username=username, email=email)
# Register callback to run after commit
transaction.on_commit(
lambda: send_notification(user.id)
)
return user
From Django’s source at /django/db/transaction.py:129:
def on_commit(func, using=None, robust=False):
"""
Register `func` to be called when the current transaction is committed.
If the current transaction is rolled back, `func` will not be called.
"""
get_connection(using).on_commit(func, robust)
Robust on_commit
def unreliable_task():
# This might fail
external_api.notify()
@transaction.atomic
def create_order(order_data):
order = Order.objects.create(**order_data)
# If this callback fails, it won't affect the transaction
transaction.on_commit(
lambda: unreliable_task(),
robust=True
)
Callbacks registered with on_commit() run after the transaction is committed. If you’re inside nested atomic() blocks, the callback runs after the outermost transaction commits.
Multiple Database Transactions
Specify which database to use:
from django.db import transaction
# Transaction on specific database
@transaction.atomic(using='default')
def operation_on_default():
Model.objects.using('default').create(value='test')
# Multiple databases
def multi_database_operation():
with transaction.atomic(using='default'):
DefaultModel.objects.create(value='test')
with transaction.atomic(using='replica'):
ReplicaModel.objects.create(value='test')
# Coordinated transaction across databases (not atomic across both!)
def pseudo_distributed_transaction():
try:
with transaction.atomic(using='db1'):
Model1.objects.using('db1').create(value='test')
with transaction.atomic(using='db2'):
Model2.objects.using('db2').create(value='test')
except Exception:
# Note: db1 transaction might have committed already
# True distributed transactions require XA/2PC
pass
Django does not support true distributed transactions across multiple databases. Each database has its own independent transaction. For true distributed transactions, you need a transaction coordinator like XA.
Transaction Best Practices
1. Keep Transactions Short
# Good: Short transaction
@transaction.atomic
def update_user(user_id, data):
user = User.objects.get(id=user_id)
user.name = data['name']
user.save()
# Bad: Long-running transaction
@transaction.atomic
def process_batch():
for i in range(10000): # Holds lock for too long
process_item(i)
time.sleep(0.1) # Never sleep in a transaction!
2. Avoid External API Calls in Transactions
# Bad: External API call in transaction
@transaction.atomic
def create_order(order_data):
order = Order.objects.create(**order_data)
payment_api.charge(order.amount) # Don't do this!
order.status = 'paid'
order.save()
# Good: API call after transaction
@transaction.atomic
def create_order(order_data):
order = Order.objects.create(**order_data)
return order
def process_order(order_data):
order = create_order(order_data)
try:
payment_api.charge(order.amount)
order.status = 'paid'
order.save()
except PaymentError:
order.status = 'failed'
order.save()
3. Use select_for_update for Concurrent Updates
# Good: Prevents race conditions
@transaction.atomic
def increment_counter(counter_id):
counter = Counter.objects.select_for_update().get(id=counter_id)
counter.value += 1
counter.save()
# Bad: Race condition possible
@transaction.atomic
def increment_counter(counter_id):
counter = Counter.objects.get(id=counter_id) # No lock
counter.value += 1 # Another request might update between get and save
counter.save()
4. Use F Expressions for Atomic Updates
from django.db.models import F
# Good: Atomic database-level update
Counter.objects.filter(id=counter_id).update(value=F('value') + 1)
# Also good but requires transaction
@transaction.atomic
def increment_counter(counter_id):
counter = Counter.objects.select_for_update().get(id=counter_id)
counter.value = F('value') + 1
counter.save()
Durable Transactions
Durable transactions cannot be nested (raises RuntimeError if nested):
@transaction.atomic(durable=True)
def critical_operation():
# This ensures the operation is not nested in another transaction
# Useful for operations that must be immediately committed
CriticalModel.objects.create(value='important')
Use durable transactions for operations that must be immediately committed to the database, such as audit logs or payment records.
Common Patterns
Idempotent Operations
@transaction.atomic
def idempotent_create(unique_key, data):
obj, created = Model.objects.get_or_create(
unique_key=unique_key,
defaults=data
)
return obj, created
Batch Processing with Transactions
def process_batch(items, batch_size=100):
for i in range(0, len(items), batch_size):
batch = items[i:i + batch_size]
with transaction.atomic():
for item in batch:
process_item(item)
Retry on Deadlock
from django.db import transaction, DatabaseError
import time
def retry_on_deadlock(max_retries=3):
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except DatabaseError as e:
if 'deadlock' in str(e).lower() and attempt < max_retries - 1:
time.sleep(0.1 * (attempt + 1)) # Exponential backoff
continue
raise
return wrapper
return decorator
@retry_on_deadlock(max_retries=3)
@transaction.atomic
def concurrent_update(record_id):
record = Record.objects.select_for_update().get(id=record_id)
record.process()
record.save()