In Django, when exactly does a transaction.on_commit() callback registered inside a nested atomic block run, and when is it dropped?
answer
- inner exit is not a commit
- callbacks remember their savepoints
- rollback to savepoint prunes them
- registration order is call order
basics
~20 sA callback registered in a nested atomic block runs only after the outermost block commits. If the inner block rolls back to its savepoint, callbacks registered inside it are dropped while earlier ones survive; a full rollback drops them all.
solid answer
~40 sDjango records, with each callback, the savepoints that were active when it was registered. Leaving an inner `atomic` block normally only releases its savepoint, so nothing runs yet — all callbacks wait for the outermost block's commit and then run in registration order. If an inner block raises, Django rolls back to its savepoint and removes every callback registered while that savepoint was active; callbacks registered before it, in the outer block, still run if the outer block commits. A rollback of the outermost transaction discards everything. Under `ATOMIC_REQUESTS` the outermost block is the view, so callbacks run when the view returns and its transaction commits — before the response travels back through middleware.
code
python · 14 linesfrom functools import partial
from django.db import transaction
def checkout(order_id, voucher_code):
with transaction.atomic():
transaction.on_commit(partial(send_receipt, order_id))
try:
with transaction.atomic():
redeem_voucher(order_id, voucher_code)
transaction.on_commit(partial(email_voucher_used, order_id))
except VoucherError:
pass # voucher email is dropped, receipt still sentgo deeper
Remember that callbacks wait for the outermost block's commit, even when registered in an inner block.
Explain how savepoint rollback prunes only the callbacks registered inside it, and that callbacks run in registration order.
Predict callback fate in layered service code and account for callback latency in ATOMIC_REQUESTS views.
Judge whether service functions should register their own follow-ups or return them to a single orchestrating layer, given how nesting decides their fate.
## Terms first - An **atomic block** is `transaction.atomic()`; only the **outermost** one opens and commits the transaction. - A **nested** block creates a **savepoint** — a marker the transaction can roll back to — and on normal exit merely releases it. - `transaction.on_commit(func)` registers `func` to run after the transaction commits. ## How Django tracks callbacks When you register a callback inside a transaction, the connection stores it together with the **set of savepoint IDs active at that moment**. Two events act on that list: 1. **Rolling back to a savepoint** removes every callback whose recorded set contains that savepoint — the ones registered inside the failed block, including deeper nested blocks. 2. **Committing the outermost block** runs the remaining callbacks, in registration order, once autocommit is back on; a **full rollback** clears the list. Releasing a savepoint (a nested block exiting normally) runs nothing. ## Walking through the receipt flow ```python with transaction.atomic(): # transaction order = Order.objects.create(...) transaction.on_commit(partial(send_receipt, order.pk)) # A try: with transaction.atomic(): # savepoint apply_voucher(order) transaction.on_commit(partial(notify_voucher, order.pk)) # B raise VoucherExpired() except VoucherExpired: pass transaction.on_commit(partial(update_stats, order.pk)) # C # after commit: A, then C run; B was dropped with its savepoint ``` | Callback | Registered in | Fate | |---|---|---| | A | outer block | runs after commit | | B | inner block that rolled back | removed at savepoint rollback | | C | outer block, after the recovery | runs after commit, after A | Had the inner block succeeded, B would also run — after the outer commit, between A and C, not at the inner block's exit. ## Timing relative to the rest of the request - **Explicit block**: callbacks run inside the `with` statement's exit, before the next line of your function. - **`ATOMIC_REQUESTS`**: the view is wrapped in the outermost block, so callbacks run as the view returns — before middleware sees the response and before a `TemplateResponse` is rendered. A slow callback delays the response. - **No block, autocommit**: the callable runs immediately inside `on_commit()`. ## Edge cases worth naming - **Callbacks registered by callbacks**: by the time a callback runs, the transaction is over and the connection is back in autocommit, so an `on_commit()` call made inside it runs its callable immediately. - **Other aliases**: a callback registered with `using="ledger"` waits for the `ledger` transaction, not for `default`. - **Swallowed errors**: if a database error is caught *inside* an atomic block rather than around it, that block still rolls back on exit and the callbacks registered inside it are discarded — the email silently never goes out. ## Why this design Callbacks follow the data: a callback describes a side effect of writes, so it lives or dies with the savepoint that holds those writes. That lets a service function register its own follow-ups without knowing whether a caller has wrapped it in a larger transaction. ## How interviewers probe it - They nest a block, raise inside it, catch outside, and ask which callbacks run — the answer turns on which savepoint was active at registration. - They ask whether the inner block's exit triggers anything — it does not. - They ask where the callback runs relative to the HTTP response under `ATOMIC_REQUESTS` — inside the handler, before middleware. Answering with the savepoint-set rule, rather than a memorised example, handles every variant they can construct.
- Does leaving a nested atomic block normally run the callbacks registered inside it?No. A nested block's normal exit only releases its savepoint; nothing is committed, so its callbacks keep waiting. They run after the outermost block commits, in the order all callbacks were registered, or are discarded if the outer transaction rolls back.
- Under ATOMIC_REQUESTS, does an on_commit callback run before or after the response passes through middleware?Before. The request transaction wraps only the view, so it commits as the view returns, and the callbacks run at that point, inside the handler. Middleware and template response rendering run afterwards, so a slow callback adds directly to response time.
saying these in an interview costs you the question
- Callbacks in an inner block run as soon as the inner block exits
- A savepoint rollback drops every callback in the transaction
- Callbacks run in reverse order of registration
- Under ATOMIC_REQUESTS callbacks run after the response is sent