Skip to main content

Run follow-up work only after a Django transaction commits

2026-11-11 16:36:24.189889+00:00
Run follow-up work only after a Django transaction commits

A worker can receive a task before the request that created its database row commits. If the request later rolls back, the worker may act on something the application never successfully created. Register follow-up work for the commit boundary.

Transaction timeline with callback occurring after commit.

Register the callback inside the transaction

This Django service fragment assumes a toy Order model and a Celery task named prepare_receipt:

from functools import partial
from django.db import transaction

def place_order(customer_id):
    with transaction.atomic():
        order = Order.objects.create(customer_id=customer_id)
        transaction.on_commit(
            partial(prepare_receipt.delay, order.pk)
        )
    return order

The partial captures the specific primary key for the callback. That avoids a common loop-closure mistake where several callbacks all use the last value of a variable.

The receipt task still needs to validate the row's current state when it runs. It may be delayed, retried or receive an ID whose order was subsequently cancelled. A commit callback establishes when enqueueing is attempted; it does not freeze the business state forever.

Check commit and rollback separately

In a Django TestCase, callbacks do not behave like ordinary immediate function calls because the test itself is transaction-wrapped. Use captureOnCommitCallbacks when the goal is to inspect registrations and emulate their execution. A transaction-focused integration test can exercise actual commit behavior on a disposable database.

Test a successful order and a deliberate exception before the atomic block exits. The successful path should register the follow-up; the rolled-back path should not publish it. Also test multiple orders if callbacks are created in a loop.

There is still a failure window after the database commits. The process can stop before the callback successfully reaches the broker, or the broker can reject the enqueue. The order remains committed in either case.

For work that must eventually be attempted, write an outbox or pending-work record in the same transaction as the order, then have a separate process publish and reconcile it. That process still needs duplicate-safe task handling. Calling a callback "reliable delivery" without addressing this gap would overstate the guarantee.

The callback should receive stable identifiers; passing a mutable request object makes later execution harder to reason about. The Nirogidhara project provides related Django workflow context. Django documents on_commit and the test callback helper.