<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Rafał Fuchs</title>
    <description>The latest articles on Forem by Rafał Fuchs (@eraefi).</description>
    <link>https://forem.com/eraefi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3870435%2Faf525286-1831-4e08-b966-1bd87531f5ce.jpg</url>
      <title>Forem: Rafał Fuchs</title>
      <link>https://forem.com/eraefi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/eraefi"/>
    <language>en</language>
    <item>
      <title>Transaction boundaries in Django: where consistency really ends</title>
      <dc:creator>Rafał Fuchs</dc:creator>
      <pubDate>Fri, 17 Apr 2026 14:50:24 +0000</pubDate>
      <link>https://forem.com/eraefi/transaction-boundaries-in-django-where-consistency-really-ends-4ilh</link>
      <guid>https://forem.com/eraefi/transaction-boundaries-in-django-where-consistency-really-ends-4ilh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR: &lt;code&gt;transaction.atomic()&lt;/code&gt; protects SQL work on one connection to one database. It does not protect queues, emails, webhooks, other databases, or third-party APIs. If you design a business process as if one &lt;code&gt;atomic&lt;/code&gt; block covered the whole thing, sooner or later you will ship a half-commit to production: DB state changed, side effects missing (or the other way around). This post walks through where the real consistency boundary is, and what to reach for when you need more.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;I'm a senior backend engineer working in Python/Django. More long-form writing at &lt;a href="https://rafalfuchs.dev/en" rel="noopener noreferrer"&gt;rafalfuchs.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The illusion &lt;code&gt;atomic()&lt;/code&gt; creates
&lt;/h2&gt;

&lt;p&gt;Most Django developers learn transactions through a comforting pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="n"&gt;Payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
    &lt;span class="nf"&gt;send_confirmation_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;publish_to_kafka&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order.created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads like one unit of work. It feels atomic. It is not.&lt;/p&gt;

&lt;p&gt;Only the first two lines are actually protected by the database transaction. The email and the Kafka publish happen inside the &lt;code&gt;with&lt;/code&gt; block, but they are side effects that do not roll back if the transaction aborts. Worse: if they fire before the commit and the commit then fails, you just notified the world about an order that does not exist.&lt;/p&gt;

&lt;p&gt;This is the core misconception I want to unpack: &lt;strong&gt;SQL commit is not business-process commit&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. What &lt;code&gt;atomic()&lt;/code&gt; actually guarantees
&lt;/h2&gt;

&lt;p&gt;Think of &lt;code&gt;atomic&lt;/code&gt; as a &lt;em&gt;local database safety boundary&lt;/em&gt;. Scoped to one connection, one database, one transaction.&lt;/p&gt;

&lt;p&gt;It does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;commit or roll back SQL changes together,&lt;/li&gt;
&lt;li&gt;support nesting via savepoints,&lt;/li&gt;
&lt;li&gt;preserve invariants inside that specific DB transaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;include external side effects (HTTP calls, emails, message brokers, cache writes),&lt;/li&gt;
&lt;li&gt;guarantee delivery of any asynchronous message,&lt;/li&gt;
&lt;li&gt;solve multi-database atomicity,&lt;/li&gt;
&lt;li&gt;protect you from a successful commit followed by a crash before your handler returns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point trips up a lot of people. The moment &lt;code&gt;__exit__&lt;/code&gt; on the context manager finishes, your transaction is committed. Anything afterwards is a new world, and the database has no idea whether your Celery task made it into Redis or not.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. &lt;code&gt;ATOMIC_REQUESTS&lt;/code&gt;: a sharp tool, not a default
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ATOMIC_REQUESTS = True&lt;/code&gt; wraps every request in a transaction. It feels like a sane default, and for small apps it genuinely reduces accidental partial writes.&lt;/p&gt;

&lt;p&gt;At higher traffic it starts to bite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transactions live longer (the full request lifecycle, not just the write),&lt;/li&gt;
&lt;li&gt;lock contention climbs on hot rows,&lt;/li&gt;
&lt;li&gt;throughput on mixed read/write endpoints drops,&lt;/li&gt;
&lt;li&gt;a slow external call inside a view now holds a DB transaction open for its entire duration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The better architectural question is rarely &lt;em&gt;"can we wrap the whole request?"&lt;/em&gt;. It is &lt;em&gt;"which specific write-critical section genuinely needs a transaction?"&lt;/em&gt;. Reach for explicit &lt;code&gt;with transaction.atomic():&lt;/code&gt; around that section, and let the rest of the request run without holding row locks.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The minimum viable guardrail: &lt;code&gt;transaction.on_commit&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you only take one pattern away from this post, take this one. Never fire a side effect from inside an &lt;code&gt;atomic&lt;/code&gt; block directly. Register it with &lt;code&gt;on_commit&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_invoice_and_enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;invoice_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;publish_invoice_created&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;on_commit&lt;/code&gt; holds the callback until the outermost transaction successfully commits. If the transaction rolls back, the callback never runs. No phantom notifications about invoices that no longer exist.&lt;/p&gt;

&lt;p&gt;But note what this still does not give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if the process crashes between commit and &lt;code&gt;on_commit&lt;/code&gt; execution, the callback is lost,&lt;/li&gt;
&lt;li&gt;if the broker is down when the callback fires, the message is gone,&lt;/li&gt;
&lt;li&gt;if the consumer processes the message twice, you get double side effects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;on_commit&lt;/code&gt; is a necessary guardrail, not a delivery guarantee. Pair it with retries on the producer side and &lt;strong&gt;idempotent consumers&lt;/strong&gt; on the receiver side, or move to something stronger (see section 6).&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Isolation levels and the race conditions you are not seeing
&lt;/h2&gt;

&lt;p&gt;Django on PostgreSQL defaults to &lt;code&gt;READ COMMITTED&lt;/code&gt;. That means: you see rows that were committed before your statement started. It does not mean: nobody can change a row between your &lt;code&gt;SELECT&lt;/code&gt; and your &lt;code&gt;UPDATE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Classic broken pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# BROKEN under concurrency
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reserve_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;available_qty&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Insufficient stock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;available_qty&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;
        &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;available_qty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two concurrent requests both read &lt;code&gt;available_qty = 5&lt;/code&gt;, both see enough stock for &lt;code&gt;qty = 3&lt;/code&gt;, both subtract, and you have just oversold by 1 unit. The transaction committed successfully. The business invariant is broken.&lt;/p&gt;

&lt;p&gt;Three tools to pick from, in rough order of cost:&lt;/p&gt;

&lt;h3&gt;
  
  
  4a. Pessimistic locking with &lt;code&gt;select_for_update&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reserve_stock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_for_update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;available_qty&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Insufficient stock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;available_qty&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;
        &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;available_qty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple, correct, and it serializes everyone hitting the same row. Use it for short critical sections on high-value state (stock, balance, seat booking). Keep the locked section small - do not put HTTP calls inside.&lt;/p&gt;

&lt;h3&gt;
  
  
  4b. Optimistic locking with a version column
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;expected_version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;available_qty&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;F&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;available_qty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;F&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ConcurrentUpdateError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lets readers through without blocking. The loser of a race has to retry. Good fit for read-heavy paths where contention is rare but must be detected.&lt;/p&gt;

&lt;h3&gt;
  
  
  4c. Database constraints as the last line of defence
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;UNIQUE&lt;/code&gt;, &lt;code&gt;CHECK&lt;/code&gt;, &lt;code&gt;FK&lt;/code&gt;, partial indexes. Your application logic will have bugs. The database is the one layer that will reliably catch a duplicate order number or a negative balance. Treat constraints as non-negotiable, not as "optimization for later".&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The moment you cross a process boundary, there is no global transaction
&lt;/h2&gt;

&lt;p&gt;The moment your use case touches Celery, a webhook, an email provider, a second database, or any external API, you are out of the ACID world. There is no protocol that wraps "insert row in Postgres" and "send message to SQS" into one atomic action. Two-phase commit exists on paper. Almost nobody runs it in production for good reasons.&lt;/p&gt;

&lt;p&gt;What you actually have is a distributed system with partial failures. Your options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Acceptable approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Side effect is nice-to-have (analytics event)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;on_commit&lt;/code&gt; + fire-and-forget, accept occasional loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Side effect must eventually happen&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;on_commit&lt;/code&gt; + retries + idempotent consumer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Side effect must happen exactly-once-ish, and loss is unacceptable&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Outbox pattern&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The outbox pattern is the one I reach for in anything touching billing, inventory, compliance, or audit.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The outbox pattern, concretely
&lt;/h2&gt;

&lt;p&gt;The idea: instead of publishing to a broker from application code, write the message to a regular table in the same transaction as your domain change. A separate worker reads the outbox and publishes. Because the write and the message land in one DB commit, they succeed or fail together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schema
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OutboxEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BigAutoField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;aggregate_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;aggregate_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;JSONField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auto_now_add&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;published_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;null&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;indexes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outbox_unpublished_idx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;published_at__isnull&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Writing the event
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;invoice_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;OutboxEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;aggregate_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;aggregate_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;on_commit&lt;/code&gt;, no direct broker call. The invoice row and the outbox row commit together, or neither exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  Relaying
&lt;/h3&gt;

&lt;p&gt;A separate process (Celery beat, a small dedicated worker, or a CDC tool like Debezium reading the WAL) pulls unpublished rows and publishes them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;relay_outbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;OutboxEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_for_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skip_locked&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;published_at__isnull&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;publish_to_broker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aggregate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# for consumer dedup
&lt;/span&gt;            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;published_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;skip_locked&lt;/code&gt; lets you run multiple relay workers without them fighting over the same rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you gain
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;no lost events on broker outage (they sit in the outbox),&lt;/li&gt;
&lt;li&gt;no phantom events on rollback (they never hit the outbox),&lt;/li&gt;
&lt;li&gt;an auditable history of what was published and when,&lt;/li&gt;
&lt;li&gt;a lag metric (&lt;code&gt;unpublished outbox rows&lt;/code&gt; and &lt;code&gt;oldest unpublished row age&lt;/code&gt;) you can alert on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What you still owe the consumer side
&lt;/h3&gt;

&lt;p&gt;Consumers must be &lt;strong&gt;idempotent&lt;/strong&gt;. Design every handler so that receiving the same message twice is a no-op. The outbox gives you at-least-once delivery, not exactly-once. The message ID (outbox row PK) is your dedup key.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Multiple databases: there is no &lt;code&gt;atomic&lt;/code&gt; across them
&lt;/h2&gt;

&lt;p&gt;Django supports multiple databases. It does not give you a cross-database transaction. This code is a lie:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Does NOT make the two writes atomic
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analytics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;using&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
        &lt;span class="n"&gt;AnalyticsEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;using&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;analytics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the &lt;code&gt;default&lt;/code&gt; commit succeeds and the &lt;code&gt;analytics&lt;/code&gt; commit fails (or the process dies in between), you have inconsistent state across databases with no automatic recovery.&lt;/p&gt;

&lt;p&gt;Practical rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep each business invariant anchored in &lt;strong&gt;one&lt;/strong&gt; database,&lt;/li&gt;
&lt;li&gt;if a flow genuinely spans databases, design it as eventual consistency: commit to the source of truth, then propagate via outbox or CDC,&lt;/li&gt;
&lt;li&gt;accept that "propagate" means "retry forever until it sticks, with alerts if lag grows".&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  8. Decision matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;th&gt;Avoid when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plain &lt;code&gt;atomic&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Single DB write, no external side effects, low business risk&lt;/td&gt;
&lt;td&gt;Any side effect leaves the DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;atomic&lt;/code&gt; + &lt;code&gt;on_commit&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Side effects must run after commit, brief loss is acceptable, basic retry in place&lt;/td&gt;
&lt;td&gt;Loss is genuinely unacceptable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outbox + idempotent consumers&lt;/td&gt;
&lt;td&gt;Billing, inventory, compliance, audit, anything where partial failure is a incident&lt;/td&gt;
&lt;td&gt;You have no consumers and never will&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saga / compensation&lt;/td&gt;
&lt;td&gt;Long-running workflows across multiple services&lt;/td&gt;
&lt;td&gt;Simple CRUD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not jump to the bottom of the table by default. Outbox has operational cost: another table, another worker, another dashboard, another runbook. Use it where the business cost of a lost event exceeds that.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Production checklist
&lt;/h2&gt;

&lt;p&gt;Before you ship any write path that has side effects, walk through this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is every critical write anchored in a single database?&lt;/li&gt;
&lt;li&gt;Are all external side effects deferred until after commit (either via &lt;code&gt;on_commit&lt;/code&gt; or outbox)?&lt;/li&gt;
&lt;li&gt;Are all message consumers idempotent? Do you have a dedup strategy with a concrete key?&lt;/li&gt;
&lt;li&gt;Do you track outbox lag (oldest unpublished row age) and retry rate, with alerts?&lt;/li&gt;
&lt;li&gt;Do your integration tests include concurrent writers hitting the same row?&lt;/li&gt;
&lt;li&gt;Is there a runbook for recovery from a half-commit? Who runs it at 03:00?&lt;/li&gt;
&lt;li&gt;Do you have constraints in the DB that catch the failure modes your application logic might miss?&lt;/li&gt;
&lt;li&gt;Are long-running operations (HTTP, file I/O) kept out of &lt;code&gt;atomic&lt;/code&gt; blocks?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any answer is "we will add it later", that is the thing that will page you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final verdict
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;transaction.atomic()&lt;/code&gt; gives you &lt;strong&gt;local transactional correctness inside one database&lt;/strong&gt;. It does not give you &lt;strong&gt;global process consistency&lt;/strong&gt;. Those are different layers, solved by different tools.&lt;/p&gt;

&lt;p&gt;Most "committed but broken" production incidents come from conflating them - trusting one &lt;code&gt;with transaction.atomic():&lt;/code&gt; block to cover a process that actually spans three systems. Treat the database transaction as one small, strict boundary, move every side effect to &lt;code&gt;on_commit&lt;/code&gt; at minimum, and reach for the outbox pattern when losing an event is unacceptable.&lt;/p&gt;

&lt;p&gt;Get that separation right and a whole class of weird half-state bugs disappears from your backlog.&lt;/p&gt;







&lt;p&gt;&lt;em&gt;If this was useful, I write more about Django architecture, backend design, and production consistency at &lt;a href="https://rafalfuchs.dev/en/blog" rel="noopener noreferrer"&gt;rafalfuchs.dev/en/blog&lt;/a&gt;. The original version of this post lives &lt;a href="https://rafalfuchs.dev/en/blog/django-transaction-boundaries-where-consistency-really-ends" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>django</category>
      <category>architecture</category>
      <category>python</category>
    </item>
    <item>
      <title>SEO Is Not Enough. What AIO (Generative Engine Optimization) Is and Why Your Company Needs It in 2026</title>
      <dc:creator>Rafał Fuchs</dc:creator>
      <pubDate>Thu, 09 Apr 2026 18:59:28 +0000</pubDate>
      <link>https://forem.com/eraefi/seo-is-not-enough-what-aio-generative-engine-optimization-is-and-why-your-company-needs-it-in-4gf5</link>
      <guid>https://forem.com/eraefi/seo-is-not-enough-what-aio-generative-engine-optimization-is-and-why-your-company-needs-it-in-4gf5</guid>
      <description>&lt;h1&gt;
  
  
  SEO Is Not Enough. What AIO (Generative Engine Optimization) Is and Why Your Company Needs It in 2026
&lt;/h1&gt;

&lt;p&gt;Google CTR is falling as more users consume answers without clicking. This guide explains AIO/GEO vs SEO and how to prepare your company for visibility in ChatGPT and AI assistants.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Decision Problem: Is SEO Alone Still Enough in 2026?
&lt;/h2&gt;

&lt;p&gt;If your growth strategy still assumes that winning means getting the click from Google, you are operating on a distribution model built for the previous decade.&lt;/p&gt;

&lt;p&gt;In 2026, a growing share of user intent gets consumed inside answer interfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google AI Overviews
&lt;/li&gt;
&lt;li&gt;ChatGPT Search
&lt;/li&gt;
&lt;li&gt;Perplexity
&lt;/li&gt;
&lt;li&gt;Claude
&lt;/li&gt;
&lt;li&gt;Gemini
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users get a synthesis and recommendations before they even decide whether to open a link.&lt;/p&gt;

&lt;p&gt;SEO is not dead. But SEO alone is no longer a complete solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed in Practice (and Why CTR Is Falling)
&lt;/h2&gt;

&lt;p&gt;This is not just a UI refresh. The information consumption model is changing.&lt;/p&gt;

&lt;p&gt;In July 2025, Pew Research Center reported that when users see an AI summary in Google, they are less likely to click traditional results and more likely to end the session without visiting a website.&lt;/p&gt;

&lt;p&gt;At the same time, OpenAI expanded ChatGPT Search as a full search interface, making it broadly available on February 5, 2025.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operational Conclusion
&lt;/h3&gt;

&lt;p&gt;A portion of traffic and purchase decision-making is shifting from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;“click a result” →&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“ask an assistant and choose a recommendation”&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  SEO vs AIO/GEO: The Architectural Difference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SEO&lt;/strong&gt; optimizes for indexing, ranking, and clicks.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;AIO/GEO&lt;/strong&gt; optimizes for how models understand and use your data in answers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Difference
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;SEO → &lt;em&gt;How do we rank higher in SERP?&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;AIO/GEO → &lt;em&gt;How do we ensure AI reconstructs our offer correctly and recommends us?&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the AI layer, the winner is not always the highest-ranking website—but the one with the clearest semantic structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Classic SEO Underperforms in Generative Search
&lt;/h2&gt;

&lt;p&gt;Modern websites are optimized for humans and front-end frameworks—not for machine understanding.&lt;/p&gt;

&lt;p&gt;For AI models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heavy HTML = harder parsing
&lt;/li&gt;
&lt;li&gt;Dynamic rendering = inconsistent context
&lt;/li&gt;
&lt;li&gt;Weak relationships = ambiguity
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What AI Actually Needs to Answer
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;What exactly do you sell?
&lt;/li&gt;
&lt;li&gt;Who is it for?
&lt;/li&gt;
&lt;li&gt;What are your packages or pricing?
&lt;/li&gt;
&lt;li&gt;What proves your credibility?
&lt;/li&gt;
&lt;li&gt;When should someone choose you (and when not)?
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SEO answers this indirectly.&lt;br&gt;&lt;br&gt;
AIO/GEO answers this directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  What AIO/GEO Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;AIO/GEO is best understood as a &lt;strong&gt;data and knowledge distribution layer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It’s not about more content—it’s about &lt;strong&gt;better structured information&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Layers
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Semantic layer&lt;/strong&gt; (JSON-LD, Schema.org)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM reference layer&lt;/strong&gt; (llms.txt, extended files)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI crawler accessibility layer&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability layer&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This makes AIO/GEO an &lt;strong&gt;architectural decision&lt;/strong&gt;, not a content task.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works Under the Hood
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. JSON-LD + Schema.org: Stop Forcing AI to Guess
&lt;/h3&gt;

&lt;p&gt;Without structured data, AI guesses meaning.&lt;/p&gt;

&lt;p&gt;With structured data, AI understands relationships:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who
&lt;/li&gt;
&lt;li&gt;what
&lt;/li&gt;
&lt;li&gt;for whom
&lt;/li&gt;
&lt;li&gt;where
&lt;/li&gt;
&lt;li&gt;under what conditions
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why AIO starts with a &lt;strong&gt;semantic audit&lt;/strong&gt;, not more blog posts.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. llms.txt and llms-full.txt: Reference Layer
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; acts as a &lt;strong&gt;knowledge capsule&lt;/strong&gt; for AI systems.&lt;/p&gt;

&lt;p&gt;Important:&lt;br&gt;&lt;br&gt;
It’s an emerging convention—not a strict standard like &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Extended approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;llms.txt&lt;/code&gt; → summary
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;llms-full.txt&lt;/code&gt; → full structured context
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best used when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your frontend is heavy
&lt;/li&gt;
&lt;li&gt;business context is hard to extract
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  3. AI Crawler Accessibility
&lt;/h3&gt;

&lt;p&gt;AIO fails if bots cannot read your content.&lt;/p&gt;

&lt;h4&gt;
  
  
  Must Be Verified
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;robots.txt for:

&lt;ul&gt;
&lt;li&gt;OAI-SearchBot
&lt;/li&gt;
&lt;li&gt;GPTBot
&lt;/li&gt;
&lt;li&gt;ClaudeBot
&lt;/li&gt;
&lt;li&gt;PerplexityBot
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;rendering consistency
&lt;/li&gt;

&lt;li&gt;metadata (Open Graph, titles, descriptions)
&lt;/li&gt;

&lt;li&gt;response times
&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;This is &lt;strong&gt;baseline infrastructure&lt;/strong&gt;, not optional.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Observability: Does AI Understand You?
&lt;/h3&gt;

&lt;p&gt;Core problem:&lt;br&gt;&lt;br&gt;
You don’t know how AI sees your company.&lt;/p&gt;

&lt;p&gt;Solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simulate buying-intent prompts
&lt;/li&gt;
&lt;li&gt;analyze answers
&lt;/li&gt;
&lt;li&gt;measure visibility
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This turns assumptions into &lt;strong&gt;measurable data&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where AIO/GEO Delivers the Highest ROI
&lt;/h2&gt;

&lt;p&gt;Best results appear in &lt;strong&gt;decision-stage queries&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  High-Impact Segments
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;B2B SaaS
&lt;/li&gt;
&lt;li&gt;Expert services / clinics
&lt;/li&gt;
&lt;li&gt;Local high-ticket services
&lt;/li&gt;
&lt;li&gt;Niche solutions
&lt;/li&gt;
&lt;li&gt;Companies with rising CAC from ads
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If users ask AI &lt;em&gt;“what should I choose?”&lt;/em&gt; → AIO becomes critical.&lt;/p&gt;




&lt;h2&gt;
  
  
  Trade-Offs: What AIO/GEO Won’t Fix
&lt;/h2&gt;

&lt;p&gt;AIO/GEO will NOT fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;weak offer
&lt;/li&gt;
&lt;li&gt;lack of proof
&lt;/li&gt;
&lt;li&gt;unclear positioning
&lt;/li&gt;
&lt;li&gt;inconsistent messaging
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It improves &lt;strong&gt;representation&lt;/strong&gt;, not &lt;strong&gt;business fundamentals&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Second limitation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no monitoring = lost visibility over time
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How to Roll It Out (Without Rebuilding Everything)
&lt;/h2&gt;

&lt;p&gt;You don’t need a full rebuild.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical Plan
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Run an AI visibility audit
&lt;/li&gt;
&lt;li&gt;Fix semantic structure
&lt;/li&gt;
&lt;li&gt;Add reference layer
&lt;/li&gt;
&lt;li&gt;Verify crawler access
&lt;/li&gt;
&lt;li&gt;Monitor and iterate (2–4 weeks cycles)
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;👉 Start here: &lt;a href="https://www.aivisible.pl/" rel="noopener noreferrer"&gt;AiVisible Audit&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where AiVisible Fits
&lt;/h2&gt;

&lt;p&gt;AiVisible focuses on &lt;strong&gt;visibility in AI-generated answers&lt;/strong&gt;, not just rankings.&lt;/p&gt;

&lt;h3&gt;
  
  
  What It Includes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AI interpretation audit
&lt;/li&gt;
&lt;li&gt;semantic mapping of your offer
&lt;/li&gt;
&lt;li&gt;plug-and-play implementation
&lt;/li&gt;
&lt;li&gt;missed-query analysis
&lt;/li&gt;
&lt;li&gt;iteration plan based on real prompts
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Try: &lt;a href="https://www.aivisible.pl/" rel="noopener noreferrer"&gt;AiVisible Audit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The value is not just traffic—it’s &lt;strong&gt;being recommended at decision moment&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Measure AIO/GEO Impact
&lt;/h2&gt;

&lt;p&gt;SEO metrics are not enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better Metrics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;brand share in AI answers
&lt;/li&gt;
&lt;li&gt;correctness of descriptions
&lt;/li&gt;
&lt;li&gt;missed queries vs competitors
&lt;/li&gt;
&lt;li&gt;time to first recommendation
&lt;/li&gt;
&lt;li&gt;lead quality from AI channels
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;SEO is not gone—but it’s no longer the only growth system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Winners Optimize Both
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;SEO → indexing &amp;amp; ranking
&lt;/li&gt;
&lt;li&gt;AIO/GEO → understanding &amp;amp; recommendation
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a &lt;strong&gt;shift in how the web delivers answers&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Challenge
&lt;/h2&gt;

&lt;p&gt;Your old SEO will not win here.&lt;/p&gt;

&lt;p&gt;Find out how ChatGPT perceives your company today:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.aivisible.pl/" rel="noopener noreferrer"&gt;AiVisible Audit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>ai</category>
    </item>
    <item>
      <title>A Complete Guide to Django Performance Optimization</title>
      <dc:creator>Rafał Fuchs</dc:creator>
      <pubDate>Thu, 09 Apr 2026 18:56:07 +0000</pubDate>
      <link>https://forem.com/eraefi/a-complete-guide-to-django-performance-optimization-4ig3</link>
      <guid>https://forem.com/eraefi/a-complete-guide-to-django-performance-optimization-4ig3</guid>
      <description>&lt;h1&gt;
  
  
  A Senior-Level Guide to Optimizing Django Applications
&lt;/h1&gt;

&lt;p&gt;A practical, production-focused guide covering ORM pitfalls, SQL performance, caching, API design, architecture, and real-world scaling decisions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Optimizing a Django application is something every production system eventually faces. Early on, everything feels fast. As users grow, data expands, and business logic becomes more complex, bottlenecks inevitably appear.&lt;/p&gt;

&lt;p&gt;This guide is based on real production issues, code audits, and hands-on experience with systems handling hundreds of thousands of requests per day.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Think About Django Optimization
&lt;/h2&gt;

&lt;p&gt;The most common mistake: &lt;strong&gt;optimizing blindly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Django is a high-level framework. Most performance issues don’t come from Django itself, but from architectural decisions.&lt;/p&gt;

&lt;p&gt;Before touching the code, answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where exactly is time being lost?&lt;/li&gt;
&lt;li&gt;Is the problem:

&lt;ul&gt;
&lt;li&gt;CPU-bound
&lt;/li&gt;
&lt;li&gt;Database-bound
&lt;/li&gt;
&lt;li&gt;I/O-bound
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Is the issue constant or dependent on data size?&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; Never optimize anything you haven’t measured.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Application Profiling
&lt;/h2&gt;

&lt;p&gt;Without profiling, optimization is guesswork.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Tools
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Django Debug Toolbar (local)&lt;/li&gt;
&lt;li&gt;django-silk / debug toolbar on staging&lt;/li&gt;
&lt;li&gt;SQL query logging&lt;/li&gt;
&lt;li&gt;APM tools:

&lt;ul&gt;
&lt;li&gt;New Relic&lt;/li&gt;
&lt;li&gt;Datadog&lt;/li&gt;
&lt;li&gt;Sentry Performance&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to Measure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Number of SQL queries per request&lt;/li&gt;
&lt;li&gt;Execution time of queries&lt;/li&gt;
&lt;li&gt;Serialization time&lt;/li&gt;
&lt;li&gt;View rendering time&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ORM and SQL Query Optimization
&lt;/h2&gt;

&lt;p&gt;Django ORM is powerful—but easy to misuse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Eliminating the N+1 Problem
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Classic issue in Django apps.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Symptoms
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Looping over objects&lt;/li&gt;
&lt;li&gt;Each related object access triggers a query&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Solutions
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;select_related()&lt;/code&gt; → ForeignKey / OneToOne&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prefetch_related()&lt;/code&gt; → ManyToMany / reverse FK&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Prefetch()&lt;/code&gt; with custom querysets&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; Any query inside a loop = performance bug.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Limiting Retrieved Data
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Model.objects.all()&lt;/code&gt; is often excessive.&lt;/p&gt;

&lt;h4&gt;
  
  
  Techniques
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;only()&lt;/code&gt; / &lt;code&gt;defer()&lt;/code&gt; for wide models&lt;/li&gt;
&lt;li&gt;Explicit fields in serializers&lt;/li&gt;
&lt;li&gt;Separate lightweight read models&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Benefits
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Faster serialization&lt;/li&gt;
&lt;li&gt;Lower memory usage&lt;/li&gt;
&lt;li&gt;Reduced latency&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Database Indexes
&lt;/h3&gt;

&lt;p&gt;Missing indexes = silent performance killers.&lt;/p&gt;

&lt;h4&gt;
  
  
  Focus On
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Fields in &lt;code&gt;filter()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fields in &lt;code&gt;order_by()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fields used in joins&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Decision Rules
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Query runs multiple times per second → add index&lt;/li&gt;
&lt;li&gt;Table &amp;gt; 100k rows → regular index audits&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Cache as an Architectural Component
&lt;/h2&gt;

&lt;p&gt;Cache is not an add-on. It’s part of system design.&lt;/p&gt;




&lt;h3&gt;
  
  
  View-Level Cache
&lt;/h3&gt;

&lt;p&gt;Best for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public endpoints&lt;/li&gt;
&lt;li&gt;Rarely changing data&lt;/li&gt;
&lt;li&gt;Dashboards / reports&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Tools
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cache_page&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Reverse proxies (Varnish, CDN)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Data-Level Cache
&lt;/h3&gt;

&lt;p&gt;Most flexible approach.&lt;/p&gt;

&lt;h4&gt;
  
  
  Examples
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Query results&lt;/li&gt;
&lt;li&gt;Aggregations&lt;/li&gt;
&lt;li&gt;Expensive computations&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Good Practices
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Keys based on business parameters&lt;/li&gt;
&lt;li&gt;Short TTL + manual invalidation&lt;/li&gt;
&lt;li&gt;Redis as production standard&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  API and Serialization Optimization
&lt;/h2&gt;

&lt;p&gt;APIs often become bottlenecks faster than the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Problems
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Deep serialization trees&lt;/li&gt;
&lt;li&gt;“Universal” serializers&lt;/li&gt;
&lt;li&gt;No pagination&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Solutions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Separate serializers (list vs detail)&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;SerializerMethodField&lt;/code&gt; sparingly&lt;/li&gt;
&lt;li&gt;Always paginate collections&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; Unpaginated endpoints = bug.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Asynchronous Processing and Background Tasks
&lt;/h2&gt;

&lt;p&gt;Not everything belongs in request–response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Good Async Candidates
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Emails&lt;/li&gt;
&lt;li&gt;Report generation&lt;/li&gt;
&lt;li&gt;External API integrations&lt;/li&gt;
&lt;li&gt;Heavy validation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Typical Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Celery + Redis / RabbitMQ&lt;/li&gt;
&lt;li&gt;Django Q&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Idempotency&lt;/li&gt;
&lt;li&gt;Retry-safe logic&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Application Architecture and Performance
&lt;/h2&gt;

&lt;p&gt;Performance often loses to “clean-looking code”.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Issues
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Fat models&lt;/li&gt;
&lt;li&gt;Business logic in serializers&lt;/li&gt;
&lt;li&gt;No read/write separation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Better Patterns
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Service layer&lt;/li&gt;
&lt;li&gt;CQRS (for larger systems)&lt;/li&gt;
&lt;li&gt;Read models optimized for use cases&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Scaling Django
&lt;/h2&gt;

&lt;p&gt;Django scales well—if designed properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Elements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Stateless backend&lt;/li&gt;
&lt;li&gt;Shared cache (Redis)&lt;/li&gt;
&lt;li&gt;Shared storage (S3, GCS)&lt;/li&gt;
&lt;li&gt;Load balancers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Decision Rules
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;More users → horizontal scaling
&lt;/li&gt;
&lt;li&gt;Slower queries → optimize data, not hardware
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When Django Is No Longer Enough
&lt;/h2&gt;

&lt;p&gt;Rare—but possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Warning Signs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Ultra-low latency (&amp;lt;50 ms)&lt;/li&gt;
&lt;li&gt;Heavy real-time workloads&lt;/li&gt;
&lt;li&gt;CPU-heavy request processing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Extract critical components&lt;/li&gt;
&lt;li&gt;Use microservices only where justified&lt;/li&gt;
&lt;li&gt;Keep Django as business core&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Django optimization is a &lt;strong&gt;process&lt;/strong&gt;, not a one-time task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Principles
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Measure before optimizing&lt;/li&gt;
&lt;li&gt;Treat ORM as a tool—not magic&lt;/li&gt;
&lt;li&gt;Cache is foundational&lt;/li&gt;
&lt;li&gt;Architecture &amp;gt; micro-optimizations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A well-designed Django application can handle large-scale systems without changing the technology stack.&lt;/p&gt;

&lt;p&gt;If you found this useful, you can read the full canonical version here:&lt;br&gt;&lt;br&gt;
&lt;a href="https://rafalfuchs.dev/en/blog/django-performance-optimization" rel="noopener noreferrer"&gt;django performace optimization&lt;/a&gt;&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
