skip to content

What does Django's ATOMIC_REQUESTS database setting wrap in a transaction, what stays outside it, and when do you opt a view out?

level: middleimportance: should knowfreq 44%

answer

  1. per database alias, off by default
  2. the view, not the middleware
  3. a caught error still commits
  4. non_atomic_requests on the view
  5. async views refuse it

basics

~20 s

ATOMIC_REQUESTS, set per database and False by default, wraps each view call in transaction.atomic: commit if the view returns, rollback if it raises. Middleware, template response rendering and streaming output run outside; non_atomic_requests opts a view out.

solid answer

~40 s

`ATOMIC_REQUESTS` is a key in each `DATABASES` entry, default `False`. When `True`, Django's request handler wraps the resolved view in `transaction.atomic(using=alias)` for every alias that sets it, so the view's writes commit if it returns a response and roll back if it raises. Only the view is covered: middleware, the rendering of a `TemplateResponse` and the body of a `StreamingHttpResponse` run after the transaction has ended. A view that catches an error and returns a 400 has *returned*, so its writes commit. You opt a view out with `@transaction.non_atomic_requests` (or `non_atomic_requests(using="other")`), applied to the view itself — typically for long views, views that call external services, or views managing their own blocks. Async views cannot use it: Django raises `RuntimeError`.

go deeper

for a junior

Recall that ATOMIC_REQUESTS is a per-database option, off by default, that makes each view one transaction.

for a middle

Explain exactly what is wrapped, why middleware and response rendering are outside, and how non_atomic_requests opts a view out.

for a senior

Catch the commit-on-handled-error trap, long transactions around external calls, and the async RuntimeError before they reach production.

for a principal

Decide between per-request transactions and explicit service-level blocks for the codebase, and how the choice will survive a move to async views.

## What the setting is `ATOMIC_REQUESTS` lives inside each database entry in `DATABASES`, next to `ENGINE` and `NAME`: ```python DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "wallets", "ATOMIC_REQUESTS": True, } } ``` Its default is `False`. It is per alias: a project with `default` and `ledger` can enable it on one and not the other, and with both enabled the view is wrapped once per alias — two separate transactions, not one shared commit. ## What it wraps When the handler has resolved a URL to a view, it wraps the view callable in `transaction.atomic(using=alias)` for each alias with the setting on. From there the ordinary `atomic` rules apply: - the view **returns** a response → commit; - the view **raises** → rollback, and the exception continues to the exception middleware and the error handlers; - an `atomic` block inside the view becomes a **savepoint**, because the request transaction is already open; - a `durable=True` block inside the view raises `RuntimeError`, since it is nested. ## What it leaves outside | Part of the request | Inside the transaction? | |---|---| | Middleware, including `process_view` hooks | No | | The view function or class-based view `dispatch()` | Yes | | Rendering a `TemplateResponse` after the view returns | No | | Generating a `StreamingHttpResponse` body | No | | Signal receivers triggered by the view's saves | Yes | The docs warn against writing to the database while generating a streaming response, since the transaction is gone and errors cannot be reported cleanly. ## The traps 1. **A handled error commits.** A view that catches `ValidationError` or a service exception and returns a 400 page has not raised, so every write it made before the error is committed. Either raise, or roll back deliberately. 2. **Swallowed database errors** inside the view break the request transaction exactly like any other `atomic` block; the fix is an inner block around the risky write. 3. **Long transactions.** The transaction and its locks are held for the whole view — including any HTTP call to a payment provider. The docs warn that the overhead grows with traffic. 4. **Async views.** If a coroutine view would be wrapped, Django raises `RuntimeError("You cannot use ATOMIC_REQUESTS with async views.")`; async code must manage its own blocks. ## Opting out ```python from django.db import transaction @transaction.non_atomic_requests def wallet_export(request): ... @transaction.non_atomic_requests(using="ledger") def ledger_report(request): ... ``` `non_atomic_requests` marks the view function so the handler skips wrapping it. It only works on the view itself — decorating a helper the view calls has no effect. ## When teams choose it - **For**: small CRUD apps where "the request is the unit of work" is true and nobody has to remember `atomic`. - **Against**: services with external calls, long reports or streaming, and code that wants its boundaries visible next to the business logic — there, explicit `atomic` blocks in service functions are clearer. ## Rolling back without raising A view that must answer with a normal page yet discard its writes can call `transaction.set_rollback(True)` before returning: the wrapping block then rolls back on exit instead of committing. It is a precise tool for the handled-error trap above, but raising a proper exception and mapping it to a response in one place is usually clearer than scattering rollback flags across views. ## Checking a project quickly - Search `DATABASES` for `ATOMIC_REQUESTS` on each alias. - Search views for `non_atomic_requests` to find the deliberate exceptions. - For every view that calls an external service, ask whether that call should really sit inside the request transaction.

  • A view under ATOMIC_REQUESTS debits a wallet, then catches a limit error and returns a 400. Is the debit committed?
    Yes. The view returned a response instead of raising, so the wrapping atomic block commits. To undo it, let the exception propagate, wrap the work in an inner block that raises, or call `transaction.set_rollback(True)` before returning.
  • Why does non_atomic_requests have to decorate the view rather than a function the view calls?
    The handler decides whether to wrap by reading a marker the decorator sets on the view callable it resolved. By the time the view calls a helper, the transaction is already open, so a marker on the helper is never read.

saying these in an interview costs you the question

  • ATOMIC_REQUESTS is on by default in new projects
  • Middleware runs inside the request transaction
  • Returning an error response rolls the transaction back
  • non_atomic_requests works on any function the view calls
  • It works the same way for async views