skip to content

In Django, what happens when transaction.atomic blocks are nested, and how does a failing inner block affect the outer transaction?

level: middleimportance: should knowfreq 52%

answer

  1. only the outermost block is a transaction
  2. inner block equals savepoint
  3. release is not commit
  4. savepoint=False defers the rollback

basics

~20 s

Only the outermost atomic block opens a transaction; each nested block creates a savepoint. An inner failure rolls back to that savepoint, and if the exception is caught outside the inner block the outer transaction can continue and commit.

solid answer

~40 s

Nesting `transaction.atomic` does not start a second transaction. The outermost block opens one; every inner block with the default `savepoint=True` creates a savepoint on entry. If the inner block exits normally, Django releases the savepoint — its changes join the outer transaction and can still be rolled back later if the outer block fails. If an exception leaves the inner block, Django rolls back to the savepoint and re-raises; catching that exception *around* the inner block lets the outer block run more queries and commit. With `savepoint=False` there is nothing to roll back to, so the failure marks the whole transaction for rollback at the next enclosing block that has a savepoint, or at the outermost block. A decorated function called from inside another atomic block is nested the same way.

code

python · 13 lines
python
from django.db import transaction


@transaction.atomic
def record_fee(wallet_id, fee):
    Fee.objects.create(wallet_id=wallet_id, amount=fee)


@transaction.atomic
def transfer_with_fee(source_id, target_id, amount, fee):
    move_funds(source_id, target_id, amount)
    record_fee(source_id, fee)  # nested: runs inside a savepoint
    raise_if_limit_exceeded(source_id)  # raising here also undoes the fee

go deeper

for a junior

Remember that only the outermost atomic block is a real transaction and that inner blocks become savepoints.

for a middle

Explain release versus rollback-to-savepoint, why the try/except goes around the inner block, and what savepoint=False gives up.

for a senior

Show you can predict what commits when decorated service functions call each other, and spot savepoint overhead in loops.

for a principal

Weigh whether service functions should open their own blocks at all, given that nesting silently changes their commit semantics depending on the caller.

## One transaction, many savepoints A **savepoint** is a named marker inside an open transaction; the database can roll back to it without discarding the work done before it. Django maps nesting onto savepoints: - the **outermost** `atomic` block opens the transaction; - every **inner** block (default `savepoint=True`) creates a savepoint when it is entered; - leaving an inner block normally **releases** the savepoint; - leaving it with an exception **rolls back to** the savepoint, then releases it, then re-raises. "Nested" means nested at run time, not in the source file. A function decorated with `@transaction.atomic` that is called from inside another block becomes an inner block; the same function called on its own is an outermost block with its own commit. ## Release is not commit The most common misreading is that a successful inner block "commits" its work. It does not. Releasing a savepoint only folds its changes into the enclosing transaction. If the outer block later raises, everything — inner work included — is rolled back. Only the exit of the outermost block commits. ## Recovering from a failed inner block A transfer that also tries to grant a loyalty bonus can let the bonus fail without losing the transfer: ```python from django.db import IntegrityError, transaction with transaction.atomic(): # transaction debit(source, amount) try: with transaction.atomic(): # savepoint grant_bonus(source) # may raise IntegrityError except IntegrityError: log_bonus_skipped(source) # queries are allowed again credit(target, amount) ``` When `grant_bonus` fails, Django rolls back to the savepoint, so only the bonus rows disappear; the debit is still in the transaction, the handler may query, and the credit runs. The `try` sits **outside** the inner block — that placement is what makes the recovery legal. ## What `savepoint=False` changes | Inner block option | On success | On an exception leaving it | |---|---|---| | `savepoint=True` (default) | Releases its savepoint | Rolls back to its savepoint, re-raises | | `savepoint=False` | Nothing to release | Marks the transaction "needs rollback"; the rollback happens at the next enclosing block with a savepoint, or the outermost | `savepoint=False` skips the two savepoint statements per block and is meant only for hot paths where that overhead is measurable. The price is the recovery pattern above: after catching the exception, any further query in the same transaction raises `TransactionManagementError`, because nothing was rolled back yet. ## Lower-level savepoint functions Django also exposes `transaction.savepoint_create()`, `savepoint_commit(sid)` and `savepoint_rollback(sid)` for manual control inside a block. In Django 6.1, `savepoint_create()` is the new name; the old `transaction.savepoint()` still works but is deprecated. These functions do nothing when the connection is in autocommit mode, and when you roll back manually inside an `atomic` block you may also need `transaction.set_rollback(False)` so the block does not roll back everything on exit. Nested `atomic` blocks are the idiomatic way; the manual functions are rarely needed. ## Things to keep in mind - Savepoints cost a statement each on entry and exit; hundreds of nested blocks in a loop add up. - A savepoint rollback does not reset model instance attributes in Python. - Each alias has its own stack: an `atomic(using="other")` block nested inside a `default` one is a separate transaction, not a savepoint. ## Predicting the outcome, step by step When asked "what ends up committed?", walk the stack from the inside out: 1. For each inner block that raised, its work is gone (rolled back to its savepoint) — provided it had a savepoint. 2. Check whether the exception was **caught before reaching the outer block**. If not, the outer block rolls back too. 3. For a block with `savepoint=False` that raised, treat the whole transaction as doomed from that point, even if the exception was caught. 4. Finally, only the outermost block's normal exit turns everything that remains into committed data. This is the reasoning interviewers look for; reciting "nested blocks are savepoints" without it rarely survives the first follow-up.

  • An inner atomic block finishes successfully and then the outer block raises. Is the inner block's work kept?
    No. The inner block only released its savepoint, which merges its changes into the outer transaction. When the outer block rolls back, the inner changes go with it. Only the outermost block's normal exit commits anything.
  • When is atomic(savepoint=False) a reasonable choice?
    When a block runs in a tight loop inside an outer transaction, profiling shows the savepoint statements matter, and you never need to recover from a failure at that level. The block still guarantees it runs inside a transaction, but a failure marks the whole outer transaction for rollback instead of a partial one.

A savepoint is a bookmark in a draft you have not yet submitted: you can tear out every page after the bookmark, but removing the bookmark does not submit anything — only handing in the whole draft does.

saying these in an interview costs you the question

  • A nested atomic block commits its work when it exits successfully
  • Nested atomic blocks open independent transactions
  • savepoint=False makes the inner block commit immediately
  • The try/except belongs inside the inner atomic block
  • Nesting only counts when the with-statements are nested in the same function