<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Thomas Tartière's blog - django</title><link href="https://thomastartiere.com/" rel="alternate"/><link href="https://thomastartiere.com/feeds/django.atom.xml" rel="self"/><id>https://thomastartiere.com/</id><updated>2025-10-17T15:49:00-07:00</updated><subtitle>Product-minded CTO building data platforms</subtitle><entry><title>Understanding Cookies and CSRF with Django</title><link href="https://thomastartiere.com/understanding-cookies-and-csrf-with-django.html" rel="alternate"/><published>2025-10-17T15:49:00-07:00</published><updated>2025-10-17T15:49:00-07:00</updated><author><name>Thomas Tartière</name></author><id>tag:thomastartiere.com,2025-10-17:/understanding-cookies-and-csrf-with-django.html</id><summary type="html">&lt;p&gt;Clarifying how cookies and CSRF protection work together&lt;/p&gt;</summary><content type="html">&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;Over the past few weeks, as part of our annual SOC II audit and penetration test, I've been reviewing the security of our authentication workflow. I wrote this article to clarify how some concepts work in our products for the developers on my team, and thought this could be useful to others.&lt;/p&gt;
&lt;h3&gt;Table of Contents&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="#1-what-cookies-actually-are"&gt;What Cookies Actually Are&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#2-how-django-session-authentication-works"&gt;How Django Session Authentication Works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#3-what-csrf-attacks-are-and-why-we-care"&gt;What CSRF Attacks Are (and Why We Care)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#4-how-djangos-csrf-protection-stops-this"&gt;How Django's CSRF Protection Stops This&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#5-how-cookie-attributes-keep-things-safe"&gt;How Cookie Attributes Keep Things Safe&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#httponly"&gt;HttpOnly&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#secure"&gt;Secure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#path"&gt;Path&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#samesite"&gt;SameSite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#expires--max-age"&gt;Expires / Max-Age&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;1. What Cookies Actually Are&lt;/h2&gt;
&lt;p&gt;Cookies are small pieces of data that your browser stores for a website. Every time your browser sends a request to that site, it automatically includes any cookies that belong to it.&lt;/p&gt;
&lt;p&gt;They’re what let websites “remember” things — like that you’re logged in, what’s in your cart, or what theme you picked.&lt;/p&gt;
&lt;p&gt;Each cookie has a few properties that tell the browser how and when to send it, like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;its name and value,&lt;/li&gt;
&lt;li&gt;which domain and path it applies to,&lt;/li&gt;
&lt;li&gt;how long it lasts,&lt;/li&gt;
&lt;li&gt;and a few security flags like &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, and &lt;code&gt;SameSite&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without cookies, every request would look like it’s coming from a brand-new visitor — which is why they’re so central to authentication.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One important fact to be clear about:&lt;/strong&gt; Cookies are only sent with requests to the &lt;strong&gt;same domain that issued them&lt;/strong&gt;. If you log in to &lt;code&gt;app.mydomain.com&lt;/code&gt;, your browser will only send that cookie back to &lt;code&gt;app.mydomain.com&lt;/code&gt; — not to any other site. This is the foundation of how browsers isolate data between websites.&lt;/p&gt;
&lt;p&gt;When you are on a given website, you can open the dev tools &amp;gt; Storage and see the list of cookies:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Cookies in Dev Tools" src="images/blog/cookies-screenshot.png"&gt;&lt;/p&gt;
&lt;h2&gt;2. How Django Session Authentication Works&lt;/h2&gt;
&lt;p&gt;Django’s default way to handle login sessions is called &lt;strong&gt;session authentication&lt;/strong&gt;. Here’s how it works, step by step:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When a user logs in, Django creates a "session" on the server. Where the session is stored is controlled by the &lt;code&gt;SESSION_ENGINE&lt;/code&gt; settings, and for our products we use Redis, but it's also possible to store them in the database or in a file.&lt;/li&gt;
&lt;li&gt;Django gives the browser a cookie (usually called &lt;code&gt;sessionid&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The cookie doesn’t contain any personal data — just a random session key.&lt;/li&gt;
&lt;li&gt;On each request to the app domain name, the browser automatically includes that cookie, and Django looks up which user it belongs to.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So when you see &lt;code&gt;request.user&lt;/code&gt; in Django, that’s the session system doing its work behind the scenes.&lt;/p&gt;
&lt;p&gt;The key idea is that the cookie is just a pointer — all the real data stays safely on the server.&lt;/p&gt;
&lt;h2&gt;3. What CSRF Attacks Are (and Why We Care)&lt;/h2&gt;
&lt;p&gt;Now that we know cookies are sent automatically, let’s talk about a common attack that abuses this: &lt;strong&gt;Cross-Site Request Forgery (CSRF)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Imagine this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You’re logged into your app and your browser has your &lt;code&gt;sessionid&lt;/code&gt; cookie.&lt;/li&gt;
&lt;li&gt;While still logged in, you visit a shady website called &lt;code&gt;evil.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;That site quietly includes a hidden form that submits a POST request to &lt;code&gt;https://app.mydomain.com/api/delete_account/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When your browser submits the form, it automatically includes your &lt;code&gt;sessionid&lt;/code&gt; cookie — because it still belongs to &lt;code&gt;app.mydomain.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;From Django’s perspective, it looks like &lt;em&gt;you&lt;/em&gt; clicked “delete my account.”&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That’s a CSRF attack — the attacker “forged” a request using your browser and your valid session cookie.&lt;/p&gt;
&lt;p&gt;Without protection, a site could trick your browser into performing any action you’re authorized to do.&lt;/p&gt;
&lt;h2&gt;4. How Django’s CSRF Protection Stops This&lt;/h2&gt;
&lt;p&gt;Django solves this problem using a &lt;strong&gt;CSRF token&lt;/strong&gt; — a secret value that helps verify that every state-changing request truly came from your own site and not from somewhere else.&lt;/p&gt;
&lt;p&gt;Let’s revisit the example from above, but this time with Django’s CSRF protection enabled.&lt;/p&gt;
&lt;h3&gt;Step-by-Step Example&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;When you log in to &lt;code&gt;https://app.mydomain.com&lt;/code&gt;, Django sets two cookies:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sessionid&lt;/code&gt;: used to identify your login session.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;csrftoken&lt;/code&gt;: a random secret token used for CSRF protection.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;When Django serves a page that contains a form, it embeds the CSRF token in that page.
   For example, in a Django template, you'll often see:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;POST&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    {% csrf_token %}
    ...
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This template tag inserts a hidden input like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;hidden&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;csrfmiddlewaretoken&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;abc123...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;When you submit the form, both the cookie and the hidden token are sent back to the server.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The browser automatically sends the &lt;code&gt;csrftoken&lt;/code&gt; cookie.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The form submits the hidden token value in the POST body.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Django's CSRF middleware checks that the token in the form matches the one in the cookie. If they match, Django knows the request came from a genuine page served by &lt;code&gt;app.mydomain.com&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now imagine the malicious site again (&lt;code&gt;evil.com&lt;/code&gt;).
   It tries to submit the same POST request to &lt;code&gt;https://app.mydomain.com/api/delete_account/&lt;/code&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It cannot read your CSRF cookie (browsers don’t let one site read another site’s cookies).&lt;/li&gt;
&lt;li&gt;It cannot include the correct CSRF token in the request.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;As a result, Django sees a missing or invalid token and immediately rejects the request with a &lt;code&gt;403 Forbidden&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So even though your browser still sends the &lt;code&gt;sessionid&lt;/code&gt; cookie automatically, Django’s CSRF middleware stops the request from being accepted — because the secret CSRF token doesn’t match.&lt;/p&gt;
&lt;h3&gt;Why This Works So Well&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The CSRF cookie itself is harmless: it doesn’t grant access to anything; it just stores a random value.&lt;/li&gt;
&lt;li&gt;The protection relies on a &lt;em&gt;double submit&lt;/em&gt; principle: Django expects both a cookie and a matching token from the same origin.&lt;/li&gt;
&lt;li&gt;Since attackers can’t read cookies from another domain, they can’t guess or replay a valid CSRF token.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This small extra step effectively blocks one of the most common web vulnerabilities — all while being invisible to the user.&lt;/p&gt;
&lt;h2&gt;5. How Cookie Attributes Keep Things Safe&lt;/h2&gt;
&lt;p&gt;Below I break down the common cookie attributes we use and show, for each one, a short example of the kind of attack it helps mitigate.&lt;/p&gt;
&lt;h3&gt;HttpOnly&lt;/h3&gt;
&lt;h4&gt;What it does&lt;/h4&gt;
&lt;p&gt;Prevents client-side JavaScript from reading the cookie value.&lt;/p&gt;
&lt;h4&gt;Why we use it&lt;/h4&gt;
&lt;p&gt;Set &lt;code&gt;HttpOnly&lt;/code&gt; on cookies that don’t need to be read by JS (for us that’s typically &lt;code&gt;sessionid&lt;/code&gt;). This reduces the damage if an attacker can run JavaScript on the page (XSS).&lt;/p&gt;
&lt;h4&gt;Attack it mitigates — XSS token theft example&lt;/h4&gt;
&lt;p&gt;If an attacker finds an XSS hole and injects &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; that tries &lt;code&gt;document.cookie&lt;/code&gt;, any cookie with &lt;code&gt;HttpOnly&lt;/code&gt; won’t be returned to their script. Without &lt;code&gt;HttpOnly&lt;/code&gt;, a script could steal &lt;code&gt;sessionid&lt;/code&gt; and send it to the attacker, allowing session hijacking.&lt;/p&gt;
&lt;h4&gt;Notes&lt;/h4&gt;
&lt;p&gt;CSRF tokens must be readable by JS when the front-end needs to include them in AJAX requests, so &lt;code&gt;csrftoken&lt;/code&gt; is often not set to &lt;code&gt;HttpOnly&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Secure&lt;/h3&gt;
&lt;h4&gt;What it does&lt;/h4&gt;
&lt;p&gt;Instructs the browser to send the cookie only over HTTPS connections.&lt;/p&gt;
&lt;h4&gt;Why we use it&lt;/h4&gt;
&lt;p&gt;Protects cookie values from being exposed over plaintext HTTP, which can be sniffed on the network.&lt;/p&gt;
&lt;h4&gt;Attack it mitigates — network eavesdropping / man-in-the-middle example&lt;/h4&gt;
&lt;p&gt;If someone is intercepting traffic on a public Wi-Fi, cookies marked &lt;code&gt;Secure&lt;/code&gt; won’t be sent over plain HTTP requests the attacker can sniff. Without &lt;code&gt;Secure&lt;/code&gt;, an attacker could capture the cookie and reuse it to impersonate the user.&lt;/p&gt;
&lt;h4&gt;Notes&lt;/h4&gt;
&lt;p&gt;Always enable &lt;code&gt;Secure&lt;/code&gt; in production. Combining &lt;code&gt;Secure&lt;/code&gt; with &lt;code&gt;SameSite=None&lt;/code&gt; is required by modern browsers for cross-site cookies.&lt;/p&gt;
&lt;h3&gt;Path&lt;/h3&gt;
&lt;h4&gt;What it does&lt;/h4&gt;
&lt;p&gt;Limits which URL paths on the domain the browser will include the cookie with.&lt;/p&gt;
&lt;h4&gt;Why we use it&lt;/h4&gt;
&lt;p&gt;Narrowing the &lt;code&gt;Path&lt;/code&gt; reduces exposure — for example, an admin session cookie could be scoped to &lt;code&gt;/admin/&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Attack it mitigates — limited-scope misused cookie example&lt;/h4&gt;
&lt;p&gt;If an attacker can induce a request to a lower-risk endpoint (e.g., &lt;code&gt;/public/&lt;/code&gt;), scoping an admin cookie to &lt;code&gt;/admin/&lt;/code&gt; prevents that request from including the admin cookie. This lowers the chance of accidental exposure and reduces the blast radius if some endpoint is misused.&lt;/p&gt;
&lt;h4&gt;Notes&lt;/h4&gt;
&lt;p&gt;Be conservative: overly broad &lt;code&gt;Path&lt;/code&gt; values increase where the cookie is sent; overly narrow ones can break legitimate flows. Use it when you can segment responsibilities by path.&lt;/p&gt;
&lt;p&gt;In our current review of the LEAN authentication flow, I found out that we can use the path attribute to make sure that the refresh token is only sent to the &lt;code&gt;refresh&lt;/code&gt; endpoint and not sent otherwise.&lt;/p&gt;
&lt;h3&gt;SameSite&lt;/h3&gt;
&lt;h4&gt;What it does&lt;/h4&gt;
&lt;p&gt;Controls whether the browser includes the cookie on cross-site requests.&lt;/p&gt;
&lt;h4&gt;Why we use it&lt;/h4&gt;
&lt;p&gt;A key defense against CSRF. It’s the browser-level control that reduces cookies being sent automatically on cross-site requests.&lt;/p&gt;
&lt;p&gt;A cross-site request is a http request from one domain to another.&lt;/p&gt;
&lt;p&gt;For instance, consider a page hosted on &lt;code&gt;badguy.com&lt;/code&gt; that includes a form to submit a POST request to &lt;code&gt;app.mydomain.com&lt;/code&gt;. If SameSite is set to "None", the session and CSRF cookies will be sent along with the request when the form is submitted, which is obviously not good. To prevent this, the default value for the SameSite attribute is "Lax", which would prevent this scenario.&lt;/p&gt;
&lt;p&gt;There are 3 possible values for the SameSite attribute (more details here: &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#samesitesamesite-value"&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#samesitesamesite-value&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;Strict&lt;/code&gt;, the cookies are only sent with requests to the domain that issued them. This is great, except in one scenario: imagine that you are currently logged into our app and you receive an alert email with a link to click on. This is a cross-origin request, so when you click on it, the browser won't include your existing session cookie and the server will redirect you to the login page.&lt;/p&gt;
&lt;p&gt;In this scenario, this is a "safe" request (GET) only used for navigation, and that's what the &lt;code&gt;Lax&lt;/code&gt; value for the SameSite attribute handles: it prevents including the cookies in cross-origin requests &lt;strong&gt;except if they are for top-level navigation.&lt;/strong&gt; With &lt;code&gt;Lax&lt;/code&gt;, normal link navigation will send the cookies, providing a good user experience, but hidden POSTs or background requests from &lt;code&gt;evil.com&lt;/code&gt; will not send the cookie.&lt;/p&gt;
&lt;h4&gt;Notes&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Lax&lt;/code&gt; is a good balance for &lt;code&gt;sessionid&lt;/code&gt; — it preserves bookmark/link behavior while blocking many CSRF vectors. &lt;code&gt;Strict&lt;/code&gt; is safer but can break expected cross-site navigation. &lt;code&gt;None&lt;/code&gt; allows all cross-site sends and should be avoided unless necessary and secured with &lt;code&gt;Secure&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Expires / Max-Age&lt;/h3&gt;
&lt;h4&gt;What it does&lt;/h4&gt;
&lt;p&gt;Controls how long the cookie lives (session cookie vs persistent cookie).&lt;/p&gt;
&lt;h4&gt;Why we use it&lt;/h4&gt;
&lt;p&gt;Shorter lifetimes reduce the window of opportunity for attackers if a cookie is leaked; persistent cookies improve UX but increase risk.&lt;/p&gt;
&lt;h4&gt;Attack it mitigates — long-lived stolen cookie example&lt;/h4&gt;
&lt;p&gt;If a persistent cookie is stolen (e.g., via backup leaks), an attacker can reuse it for as long as it’s valid. By setting reasonable expiry times and using refresh mechanisms server-side (e.g., rotating session keys), we reduce exposure.&lt;/p&gt;
&lt;h4&gt;Notes&lt;/h4&gt;
&lt;p&gt;For very sensitive sessions, prefer shorter max-age and consider requiring re-authentication for sensitive actions.&lt;/p&gt;</content><category term="django"/><category term="django"/></entry><entry><title>Versioned Static Assets in Django with Docker</title><link href="https://thomastartiere.com/django-versioned-static-assets.html" rel="alternate"/><published>2024-12-31T16:34:00-08:00</published><updated>2024-12-31T16:34:00-08:00</updated><author><name>Thomas Tartière</name></author><id>tag:thomastartiere.com,2024-12-31:/django-versioned-static-assets.html</id><summary type="html">&lt;p&gt;A robust strategy to manage static assets with Docker-based Django projects&lt;/p&gt;</summary><content type="html">&lt;ol&gt;
&lt;li&gt;&lt;a href="#introduction"&gt;Introduction&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;&lt;a href="#problem"&gt;The Problem with Static Files in Django Deployments&lt;/a&gt;&lt;br&gt;
    2.1 &lt;a href="#option1"&gt;Option 1: Run &lt;code&gt;collectstatic&lt;/code&gt; During Deployment&lt;/a&gt;&lt;br&gt;
    2.2 &lt;a href="#option2"&gt;Option 2: Run &lt;code&gt;collectstatic&lt;/code&gt; During Docker Build&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#solution"&gt;A Versioned Approach to Static Assets&lt;/a&gt;&lt;br&gt;
    3.1 &lt;a href="#overview"&gt;Overview&lt;/a&gt;&lt;br&gt;
    3.2 &lt;a href="#implementation"&gt;Implementation Example with &lt;code&gt;django-storages&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusion"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;&lt;a name="introduction"&gt;&lt;/a&gt;Introduction&lt;/h1&gt;
&lt;p&gt;When deploying Django applications with Docker, managing static files efficiently and safely is a common challenge. The need to use the &lt;code&gt;collectstatic&lt;/code&gt; command—&lt;a href="https://docs.djangoproject.com/en/5.1/ref/contrib/staticfiles/#collectstatic"&gt;which aggregates and processes all static files&lt;/a&gt; —complicates deployments, especially in multi-environment setups like staging and production. This article explores various approaches to handling static files and introduces a versioning strategy for more reliable deployments.&lt;/p&gt;
&lt;h1&gt;&lt;a name="introduction"&gt;&lt;/a&gt;The Problem with Static Files in Django Deployments&lt;/h1&gt;
&lt;p&gt;The &lt;code&gt;collectstatic&lt;/code&gt; command is essential in Django to gather static files from various apps and prepare them for serving via a CDN from a storage service like AWS S3 or Azure Blob Storage. However, this step introduces deployment complexities:&lt;/p&gt;
&lt;h2&gt;&lt;a name="option1"&gt;&lt;/a&gt;Option 1: Run &lt;code&gt;collectstatic&lt;/code&gt; During Deployment&lt;/h2&gt;
&lt;p&gt;One approach is to avoid running &lt;code&gt;collectstatic&lt;/code&gt; during the Docker build process and instead execute it during deployment for each environment (e.g., staging or production).&lt;/p&gt;
&lt;p&gt;Problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Error Detection Delay&lt;/strong&gt;: Issues with static files are only discovered at deployment time, making debugging more challenging.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discrepancies Between Environments&lt;/strong&gt;: &lt;code&gt;collectstatic&lt;/code&gt; often performs additional optimizations like file compression (e.g., via &lt;code&gt;django-compressor&lt;/code&gt;). Running it multiple times for different environments increases the risk of inconsistencies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rollback Challenges&lt;/strong&gt;: Rolling back a deployment requires re-running &lt;code&gt;collectstatic&lt;/code&gt; for the previous version. This can be time-consuming, taking several minutes or even tens of minutes, and complicates rapid recovery.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a name="option1"&gt;&lt;/a&gt;Option 2: Run &lt;code&gt;collectstatic&lt;/code&gt; During Docker Build&lt;/h2&gt;
&lt;p&gt;Another approach is to execute &lt;code&gt;collectstatic&lt;/code&gt; as part of the Docker build process.&lt;/p&gt;
&lt;p&gt;Challenges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Risk of Overwriting Files&lt;/strong&gt;: If the new static files are pushed to the same storage location used by the production environment, there is a risk of overwriting or corrupting existing files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Increased Build Time&lt;/strong&gt;: Running &lt;code&gt;collectstatic&lt;/code&gt; during the build process increases the time required to build the Docker image.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;a name="solution"&gt;&lt;/a&gt;A Versioned Approach to Static Assets&lt;/h1&gt;
&lt;h2&gt;&lt;a name="overview"&gt;&lt;/a&gt;Overview&lt;/h2&gt;
&lt;p&gt;A more robust solution is to incorporate a versioning strategy during the Docker build process. By using a &lt;code&gt;RELEASE_VERSION&lt;/code&gt; build argument—following semantic versioning—we can organize static files by version in the storage account. Here’s how it works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Set a Version Identifier&lt;/strong&gt;: During the Docker build, pass a &lt;code&gt;RELEASE_VERSION&lt;/code&gt; argument to identify the application version (e.g., &lt;code&gt;v1.2.3&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Organize Static Files by Version&lt;/strong&gt;: Modify the &lt;code&gt;collectstatic&lt;/code&gt; process to store files in a folder named after the version (e.g., &lt;code&gt;static/v1.2.3/&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ensure Environment Independence&lt;/strong&gt;: Use a single storage account for all environments. Static files are an integral part of the application and should not depend on a specific environment. Since static files are versioned, there is no risk of interference between environments..&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This strategy provides several benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Easy Rollbacks&lt;/strong&gt;: To roll back, simply configure the application to use the static files from the previous version. No re-running of &lt;code&gt;collectstatic&lt;/code&gt; is required.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment Consistency&lt;/strong&gt;: All environments use the same set of static files for a given version, eliminating discrepancies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preservation of Static Files&lt;/strong&gt;: Files from different versions are stored separately, avoiding overwrites and corruption. This approach also facilitates concurrent deployments and aligns well with blue-green deployment strategies, as each version's static files remain isolated and accessible without interference. When deploying a new version in a cluster, containers are typically updated incrementally rather than all at once. With this method, each running version maintains access to its specific static files until all containers are successfully updated to the latest version.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a name="implementation"&gt;&lt;/a&gt;Implementation Example with &lt;code&gt;django-storages&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;django-storages&lt;/code&gt; is a typical package that simplifies using remote storage accounts in a Django application (see documentation at &lt;a href="https://django-storages.readthedocs.io"&gt;https://django-storages.readthedocs.io&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Implementing the approach described in the previous section requires to modify the default behaviour of django-storages. Below is an implementation of the versioned approach using Azure Blob Storage. A similar approach can be used with AWS S3 or other storage services.&lt;/p&gt;
&lt;p&gt;We first need to define a custom storage class to prefix each static filepath with the current app version, as shown below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/storages.py&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.conf&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;storages.backends.azure_storage&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AzureStorage&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StaticAzureStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AzureStorage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Define separate azure storage&lt;/span&gt;
&lt;span class="sd"&gt;    container for public files.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;account_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AZURE_ACCOUNT_NAME_STATIC&lt;/span&gt;
    &lt;span class="n"&gt;account_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AZURE_ACCOUNT_KEY_STATIC&lt;/span&gt;
    &lt;span class="n"&gt;azure_container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AZURE_CONTAINER_STATIC&lt;/span&gt;
    &lt;span class="n"&gt;expiration_secs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_get_valid_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Concatenate the app version to the filename to make&lt;/span&gt;
&lt;span class="sd"&gt;        sure all files are stored within a versioned folder&lt;/span&gt;

&lt;span class="sd"&gt;        Args:&lt;/span&gt;
&lt;span class="sd"&gt;            name (str): filename&lt;/span&gt;

&lt;span class="sd"&gt;        Returns:&lt;/span&gt;
&lt;span class="sd"&gt;            str: filepath that includes the app version&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&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;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RELEASE_VERSION&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_get_valid_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We can then configure the Django settings to use this custom storage class:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/settings/base.py&lt;/span&gt;

&lt;span class="c1"&gt;# -------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# Azure Storage Configuration&lt;/span&gt;
&lt;span class="c1"&gt;# ---------------------------------&lt;/span&gt;

&lt;span class="c1"&gt;# Azure blob storage for static files&lt;/span&gt;
&lt;span class="n"&gt;AZURE_ACCOUNT_KEY_STATIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AZURE_STORAGE_ACCOUNT_KEY_STATIC&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;AZURE_ACCOUNT_NAME_STATIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AZURE_STORAGE_ACCOUNT_NAME_STATIC&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;AZURE_CONTAINER_STATIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;AZURE_STORAGE_CONTAINER_STATIC&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lean-api-static&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;AZURE_STORAGE_FOR_STATIC_ENABLED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AZURE_ACCOUNT_NAME_STATIC&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;AZURE_CONTAINER_STATIC&lt;/span&gt;
&lt;span class="n"&gt;AZURE_CUSTOM_DOMAIN_STATIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AZURE_ACCOUNT_NAME_STATIC&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.blob.core.windows.net&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;AZURE_ACCOUNT_NAME_STATIC&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# Default storage options&lt;/span&gt;
&lt;span class="c1"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="n"&gt;STORAGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# See defaults in, https://docs.djangoproject.com/en/4.2/ref/settings/#storages&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;default&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;BACKEND&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;django.core.files.storage.FileSystemStorage&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;staticfiles&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;BACKEND&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;django.contrib.staticfiles.storage.StaticFilesStorage&amp;quot;&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="c1"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# STATIC files&lt;/span&gt;
&lt;span class="c1"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;

&lt;span class="n"&gt;STATIC_ROOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASE_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;static&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;STATIC_HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DJANGO_STATIC_HOST&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;STATIC_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;STATIC_HOST&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;/static/&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders&lt;/span&gt;
&lt;span class="n"&gt;STATICFILES_FINDERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;django.contrib.staticfiles.finders.FileSystemFinder&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;django.contrib.staticfiles.finders.AppDirectoriesFinder&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# https://docs.djangoproject.com/en/4.0/ref/settings/#prefixes-optional&lt;/span&gt;
&lt;span class="n"&gt;STATICFILES_DIRS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# Specify static files in &amp;lt;root&amp;gt;/templates/static directory to be collected by collectstatic command&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;templates&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;templates/static&amp;quot;&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;AZURE_CUSTOM_DOMAIN_STATIC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Azure Blob Storage&lt;/span&gt;
    &lt;span class="c1"&gt;# ---------------------------------------------------------------------------&lt;/span&gt;
    &lt;span class="n"&gt;STORAGES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;staticfiles&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;BACKEND&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;config.storages.StaticAzureStorage&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;STATIC_HOST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;STATIC_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;STATIC_HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AZURE_CONTAINER_STATIC&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# if a CDN host is not provided, then we set the static url to the&lt;/span&gt;
        &lt;span class="c1"&gt;# Blob storage container url&lt;/span&gt;
        &lt;span class="n"&gt;STATIC_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AZURE_CUSTOM_DOMAIN_STATIC&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AZURE_CONTAINER_STATIC&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The last step is to modify our Dockerfile to run &lt;code&gt;collecstatic&lt;/code&gt; command during build:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;

&lt;span class="c"&gt;# define the build arguments&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;AZURE_STORAGE_ACCOUNT_NAME_STATIC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;AZURE_STORAGE_CONTAINER_STATIC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;RELEASE_VERSION

&lt;span class="c"&gt;# save build-arguments as environment variables in the image&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;DJANGO_SETTINGS_MODULE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;config.settings.production&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;AZURE_STORAGE_ACCOUNT_NAME_STATIC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$AZURE_STORAGE_ACCOUNT_NAME_STATIC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;AZURE_STORAGE_CONTAINER_STATIC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$AZURE_STORAGE_CONTAINER_STATIC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;RELEASE_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$RELEASE_VERSION&lt;/span&gt;

&lt;span class="c"&gt;# Collect static files&lt;/span&gt;
&lt;span class="c"&gt;# if it is a production build, use the azure storage key passed as a&lt;/span&gt;
&lt;span class="c"&gt;# build secret to store all the static files in the azure storage account.&lt;/span&gt;
&lt;span class="c"&gt;# To use this, run docker build --secret id=azure_storage_account_key_static,env=AZURE_STORAGE_ACCOUNT_KEY_STATIC ....&lt;/span&gt;
&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--mount&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret,id&lt;span class="o"&gt;=&lt;/span&gt;azure_storage_account_key_static,required&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;AZURE_STORAGE_ACCOUNT_KEY_STATIC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;/run/secrets/azure_storage_account_key_static&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;manage.py&lt;span class="w"&gt; &lt;/span&gt;collectstatic&lt;span class="w"&gt; &lt;/span&gt;--no-input
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Sending static files to the storage account in Azure requires the name of the container, the name of the storage account and an account key. This last parameter is a secret and shouldn't be saved as an environment variable in the image. To prevent this, it is passed as a &lt;a href="https://docs.docker.com/build/building/secrets/"&gt;build secret&lt;/a&gt;. A build secret is temporarily mount inside the build container, for the duration of the build instruction.&lt;/p&gt;
&lt;h4&gt;Docker build command&lt;/h4&gt;
&lt;p&gt;When building the docker image, we now need to provide the release version, storage account name and container name as build argument, and the storage account key as a build secret:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v1.2.3

&lt;span class="c1"&gt;# build the image&lt;/span&gt;
docker&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;your-app:&lt;span class="k"&gt;$(&lt;/span&gt;RELEASE_VERSION&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;Dockerfile&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--build-arg&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;RELEASE_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;RELEASE_VERSION&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--build-arg&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;AZURE_STORAGE_ACCOUNT_NAME_STATIC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;name&lt;span class="w"&gt; &lt;/span&gt;of&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;storage&lt;span class="w"&gt; &lt;/span&gt;account&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--build-arg&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;AZURE_STORAGE_CONTAINER_STATIC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;name&lt;span class="w"&gt; &lt;/span&gt;of&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;container&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--secret&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;azure_storage_account_key_static,env&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;private&lt;span class="w"&gt; &lt;/span&gt;key&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;.

&lt;span class="c1"&gt;# push the image to the registry&lt;/span&gt;
docker&lt;span class="w"&gt; &lt;/span&gt;push&lt;span class="w"&gt; &lt;/span&gt;your-app:&lt;span class="nv"&gt;$VERSION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Key Considerations&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Storage Costs&lt;/strong&gt;: Versioning static files increases storage usage. Without the approach described here, it's very hard to free space as the static files accumulate in the same folder. With this approach, it's easy to delete the folder matching old versions of the application that we don't intend to use in production anymore.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment Setup&lt;/strong&gt;: Since the Docker image contains the reference to the storage account as an environment variable, the Docker image can be deployed anywhere easily, ensuring all environments fetch static files from the correct version folder.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;a name="conclusion"&gt;&lt;/a&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;By adopting a versioning strategy for static assets, you can address key challenges in Django deployments effectively. This approach ensures seamless rollbacks, maintains environment consistency, and avoids file overwrites or corruption. Additionally, because the Docker image includes environment-independent references to the storage account, deployment is streamlined across environments. While this strategy requires some upfront configuration and storage management, its reliability and scalability make it a worthwhile investment for production-grade applications.&lt;/p&gt;</content><category term="django"/><category term="django"/><category term="python"/><category term="webdev"/></entry><entry><title>Avoiding Lost Updates in Web Applications</title><link href="https://thomastartiere.com/avoiding-lost-updates-in-web-applications.html" rel="alternate"/><published>2024-12-18T13:49:00-08:00</published><updated>2024-12-18T13:49:00-08:00</updated><author><name>Thomas Tartière</name></author><id>tag:thomastartiere.com,2024-12-18:/avoiding-lost-updates-in-web-applications.html</id><summary type="html">&lt;p&gt;Save your future self from sleepless nights and learn to prevent sneaky concurrency bugs&lt;/p&gt;</summary><content type="html">&lt;ol&gt;
&lt;li&gt;&lt;a href="#introduction"&gt;Introduction&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;&lt;a href="#problem"&gt;The Problem: Lost Updates with a Django API Endpoint&lt;/a&gt;&lt;br&gt;
    2.1 &lt;a href="#demo-app"&gt;Demo App&lt;/a&gt;&lt;br&gt;
    2.2 &lt;a href="#simulating-concurrent-requests"&gt;Simulating Concurrent Requests&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;&lt;a href="#understanding"&gt;Understanding Read Committed Isolation Level&lt;/a&gt;&lt;br&gt;
    3.1 &lt;a href="#what-is-read-committed"&gt;What is Read Committed?&lt;/a&gt;&lt;br&gt;
    3.2 &lt;a href="#what-it-doesnt-guarantee"&gt;What it Doesn't Guarantee&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;&lt;a href="#solutions"&gt;Solutions to Avoid Lost Updates&lt;/a&gt;&lt;br&gt;
    4.1 &lt;a href="#solution1"&gt;Solution 1: Atomic Write with &lt;code&gt;F()&lt;/code&gt; Expressions&lt;/a&gt;&lt;br&gt;
    4.2 &lt;a href="#solution2"&gt;Solution 2: Change Isolation Level to Serializable&lt;/a&gt;&lt;br&gt;
    4.3 &lt;a href="#solution3"&gt;Solution 3: Use &lt;code&gt;select_for_update()&lt;/code&gt; for Complex Queries&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusion"&gt;Conclusion&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;&lt;a href="#references"&gt;References and Further Readings&lt;/a&gt; &lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;a name="introduction"&gt;&lt;/a&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Ignoring concepts like proper database transactions and handling race conditions might seem harmless at first, but it’s a recipe for concurrency bugs that are incredibly hard to debug. These bugs have a knack for showing up at the worst possible time—usually once your app has scaled—leading to massive headaches and sleepless nights. In this post, we’ll explore how to prevent these issues in Django, saving your future self from countless hours of frustration.&lt;/p&gt;
&lt;p&gt;Relational databases like PostgreSQL offer various levels of transaction isolation to manage concurrent data access. The most common (and often default settings) is &lt;strong&gt;Read Committed&lt;/strong&gt;, which can lead to &lt;strong&gt;lost updates&lt;/strong&gt; in certain scenarios. This tutorial demonstrates the problem and explores solutions using a Django application, but the problem and solutions are relevant for any framework.&lt;/p&gt;
&lt;p&gt;We'll:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define a basic Django app that increments a counter through an endpoint.&lt;/li&gt;
&lt;li&gt;Explain the &lt;strong&gt;Read Committed&lt;/strong&gt; isolation level and its limitations.&lt;/li&gt;
&lt;li&gt;Provide solutions:&lt;ol&gt;
&lt;li&gt;Using &lt;strong&gt;atomic write operations&lt;/strong&gt; with Django's &lt;code&gt;F&lt;/code&gt; expressions.&lt;/li&gt;
&lt;li&gt;Switching to &lt;strong&gt;Serializable&lt;/strong&gt; isolation.&lt;/li&gt;
&lt;li&gt;Leveraging &lt;code&gt;select_for_update()&lt;/code&gt; for complex cases.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The demo app and code samples can be found in this repository: &lt;a href="https://github.com/tartieret/django-isolation-levels"&gt;https://github.com/tartieret/django-isolation-levels&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a name="problem"&gt;&lt;/a&gt;1. The Problem: Lost Updates with a Django API Endpoint&lt;/h2&gt;
&lt;h3&gt;&lt;a name="demo-app"&gt;&lt;/a&gt;Demo app&lt;/h3&gt;
&lt;p&gt;Imagine a simple Django app where an endpoint increments a counter stored in a PostgreSQL database. Here's the basic implementation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# models.py&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&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="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&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;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PositiveIntegerField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# views.py&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.http&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JsonResponse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&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="nn"&gt;.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;

&lt;span class="nd"&gt;@transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atomic&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;increment_counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;This endpoint receives a GET request such as:&lt;/span&gt;
&lt;span class="sd"&gt;    GET http://localhost:8000/counters/increment/myCounter/&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="c1"&gt;# Retrieve the counter object&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Simulate a slow API endpoint&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Increment and save&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Return a JSON response&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;JsonResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# urls.py&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.urls&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;.&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;views&lt;/span&gt;

&lt;span class="n"&gt;urlpatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;increment/&amp;lt;str:name&amp;gt;/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;increment_counter&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="s1"&gt;&amp;#39;increment_counter&amp;#39;&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;p&gt;Here, when the &lt;code&gt;/counters/increment/&amp;lt;name&amp;gt;/&lt;/code&gt; endpoint is called, it retrieves the counter, increments its &lt;code&gt;value&lt;/code&gt;, and saves the updated value.Simulating Concurrent Requests&lt;/p&gt;
&lt;p&gt;Using the admin portal, let's create a first counter called "test," with initial value of zero:&lt;/p&gt;
&lt;p&gt;&lt;img alt="image.png" src="/images/blog/Avoiding%20Lost%20Updates%20in%20Django/3N_vqGmHSjWAMG-image.png"&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a name="simulating-concurrent-requests"&gt;&lt;/a&gt;Simulating Concurrent Requests&lt;/h3&gt;
&lt;p&gt;We'll now try to send multiple requests to this endpoint in parallel, as an example of what could happen in real life with multiple concurrent users.&lt;/p&gt;
&lt;p&gt;To simulate concurrent requests, we can use the &lt;code&gt;concurrent.futures&lt;/code&gt; module in Python to send multiple requests in parallel and build the following &lt;code&gt;run_test.py&lt;/code&gt; script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;

&lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;http://localhost:8000/counters/increment/test/&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Function to send a request&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;# Send 10 requests in parallel&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;futures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;send_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;URL&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;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;All requests completed.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The script above will send ten concurrent requests to our local web server. Intuitively, we would expect the final counter value to be equal to 10. Let's run it!&lt;/p&gt;
&lt;p&gt;&lt;img alt="image.png" src="/images/blog/Avoiding%20Lost%20Updates%20in%20Django/eYwVTT6oLsqT4Z-image.png"&gt;&lt;/p&gt;
&lt;p&gt;What's happening? Each concurrent request returns a value of 1 for the counter!!!&lt;/p&gt;
&lt;p&gt;Checking the counter in the admin portal, we can verify that the final value is indeed 1, in spite of having processed ten requests to increment it.&lt;/p&gt;
&lt;p&gt;&lt;img alt="image.png" src="/images/blog/Avoiding%20Lost%20Updates%20in%20Django/huO4fWk7ofQ4x4-image.png"&gt;&lt;/p&gt;
&lt;p&gt;The schematic below gives a visual representation of what happened:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The server receives request #1 (in blue), and a zero value is retrieved from the database. It takes a while for the transaction to finish and for the &lt;code&gt;counter = counter + 1&lt;/code&gt; operation to be committed.&lt;/li&gt;
&lt;li&gt;In the meantime, request #2 (in red) is received and processed similarly. It starts by reading the counter's current value (zero) in the database and incrementing it by one.&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;Similarly, request #n (in green) is received, reads the value of the counter, and commits a new value of &lt;code&gt;0 + 1 = 1&lt;/code&gt; to the database after about 5 seconds.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="image.png" src="/images/blog/Avoiding%20Lost%20Updates%20in%20Django/schema.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;This problem is known as a &lt;strong&gt;lost update.&lt;/strong&gt; Another transaction overwrites changes made by one transaction without the latter being aware of the former's change.&lt;/p&gt;
&lt;p&gt;This typically happens when two or more transactions access the same data and then perform an update based on what was read. If there is no locking mechanism in place, we are losing some of the updates as the defaut isolation level (read committed) only guarantees that each transaction is based on a value that was previously committed.&lt;/p&gt;
&lt;h2&gt;&lt;a name="understanding"&gt;&lt;/a&gt;2. Understanding Read Committed Isolation Level&lt;/h2&gt;
&lt;h3&gt;&lt;a name="what-is-read-committed"&gt;&lt;/a&gt;What is Read Committed?&lt;/h3&gt;
&lt;p&gt;Read Committed is the default isolation level in PostgreSQL, Microsoft SQL server, and other relational databases. It ensures that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When reading from the database, we only see data that has been committed &lt;strong&gt;(no dirty reads&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;When writing to the database, we only overwrite data that has been committed (&lt;strong&gt;no dirty writes&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a name="what-it-doesnt-guarantee"&gt;&lt;/a&gt;What it Doesn't Guarantee&lt;/h3&gt;
&lt;p&gt;Read Committed &lt;strong&gt;does not prevent non-repeatable reads or lost updates&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Non-repeatable reads&lt;/strong&gt;: The same query can return different results within a transaction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lost updates&lt;/strong&gt;: Two transactions can overwrite each other's updates because they operate on the same snapshot of the data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In our example, all transactions read the same value (&lt;code&gt;0&lt;/code&gt;), increment it, and overwrite each other's result.&lt;/p&gt;
&lt;p&gt;A full description of transaction guarantees and isolation levels is outside the scope of the present article, but I refer you to the following resources for more information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PostgreSQL transaction isolation: &lt;a href="https://www.postgresql.org/docs/current/transaction-iso.html"&gt;https://www.postgresql.org/docs/current/transaction-iso.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Designing Data-Intensive Applications, by Martin Kleppman (Chapter 7): &lt;a href="https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/"&gt;https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a name="solutions"&gt;&lt;/a&gt; 3. Solutions to Avoid Lost Updates&lt;/h2&gt;
&lt;h3&gt;&lt;a name="solution1"&gt;&lt;/a&gt;Solution 1: Atomic Write with &lt;code&gt;F()&lt;/code&gt; Expressions&lt;/h3&gt;
&lt;p&gt;The trivial example given above can be avoided by using an atomic update operation, which removes the need to read-modify-write. For example, the counter update can be performed using the following SQL query:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;counters&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;test&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Django's &lt;code&gt;F()&lt;/code&gt; Expressions allow you to perform atomic operations at the database level without retrieving and updating the value in Python, which avoids race conditions.&lt;/p&gt;
&lt;p&gt;Using this approach, we can add a new endpoint to increment our counter:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.db.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&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;increment_counter_atomic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="c1"&gt;# Atomically increment the counter&lt;/span&gt;
        &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;value&amp;#39;&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="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&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="n"&gt;name&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;JsonResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Testing again with ten concurrent requests, our counter now increases from 1 to 11, which is what we want:&lt;/p&gt;
&lt;p&gt;&lt;img alt="image.png" src="/images/blog/Avoiding%20Lost%20Updates%20in%20Django/YG7Ic2T0gseUp8-image.png"&gt;&lt;/p&gt;
&lt;p&gt;This is a simple and efficient solution, but it is limited to simple operations that can be expressed as an atomic update.&lt;/p&gt;
&lt;h3&gt;&lt;a name="solution2"&gt;&lt;/a&gt;Solution 2: Change Isolation Level to Serializable&lt;/h3&gt;
&lt;p&gt;A more radical option is to modify the Django settings to use a different isolation level, such as &lt;strong&gt;repeatable read&lt;/strong&gt; or &lt;strong&gt;serializable&lt;/strong&gt;. This can be done for the entire application directly &lt;a href="https://docs.djangoproject.com/en/5.1/ref/databases/#isolation-level"&gt;in the Django settings&lt;/a&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.db.backends.postgresql.psycopg_any&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;IsolationLevel&lt;/span&gt;

&lt;span class="n"&gt;DATABASES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;OPTIONS&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;isolation_level&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IsolationLevel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SERIALIZABLE&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;p&gt;The &lt;strong&gt;Serializable&lt;/strong&gt; isolation level prevents concurrent transactions from interfering with each other. It ensures that transactions are executed &lt;strong&gt;as if they ran sequentially&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;However, serializable transactions may fail with &lt;strong&gt;serialization errors&lt;/strong&gt; if concurrent transactions conflict, requiring retries. Stricter locking can also significantly impact performance.&lt;/p&gt;
&lt;p&gt;Alternatively, this can be done for specific raw queries by explicitly setting the isolation level, as shown below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;SELECT ... &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;&lt;a name="solution3"&gt;&lt;/a&gt;Solution 3: Use &lt;code&gt;select_for_update()&lt;/code&gt; for Complex Queries&lt;/h3&gt;
&lt;p&gt;Reading and updating an object through multiple steps is a very common use case and is supported by modern relational databases, thanks to the &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; statement. With this statement, the rows that are selected are locked, preventing any concurrent update until the current transaction is committed&lt;/p&gt;
&lt;p&gt;When we try to use this statement, the rows that we are trying to access may be already locked by another transaction. In that case, we have several options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;wait until the concurrent transaction finishes&lt;/li&gt;
&lt;li&gt;don't wait and return an error (&lt;code&gt;NOWAIT&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;skip any locked row (&lt;code&gt;SKIP LOCKED&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Django includes a &lt;code&gt;select_for_update&lt;/code&gt; function &lt;a href="https://docs.djangoproject.com/en/5.1/ref/models/querysets/#select-for-update"&gt;in its Queryset API&lt;/a&gt; , with the following signature:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;select_for_update(nowait=False, skip_locked=False, of=(), no_key=False)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Note that rows of selected objects specified in &lt;code&gt;select_for_update&lt;/code&gt; are locked in addition to rows of the queryset’s model.&lt;/p&gt;
&lt;p&gt;Using this function, we can build a new view that will lock the counter instance to prevent concurrent updates:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&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="nn"&gt;django.http&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JsonResponse&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;increment_counter_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="c1"&gt;# Lock the row to prevent concurrent access&lt;/span&gt;
        &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;select_for_update&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&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;JsonResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Running our test script again, we can see that the counter is properly incremented. However, even if the requests are sent concurrently, they are only executed sequentially due to the lock on the counter table. The screenshot below shows the timestamp when each response is received, with a clear 5-second delay between each response.&lt;/p&gt;
&lt;p&gt;&lt;img alt="image.png" src="/images/blog/Avoiding%20Lost%20Updates%20in%20Django/gyqaDFAPfbH3Dl-image.png"&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a name="conclusion"&gt;&lt;/a&gt;5. Conclusion&lt;/h2&gt;
&lt;p&gt;Lost updates are a common issue when using the default &lt;strong&gt;Read Committed&lt;/strong&gt; isolation level in relational databases. Ignoring this issue can lead to concurrency bugs that often manifest at higher scales or under heavy load.&lt;/p&gt;
&lt;p&gt;In a Django application, you can prevent lost updates using the following techniques:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Switch to Serializable Isolation&lt;/strong&gt; for strict guarantees, but with a performance trade-off.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;strong&gt;&lt;code&gt;F()&lt;/code&gt;&lt;/strong&gt; expressions&lt;/strong&gt; for atomic writes when operations are simple.&lt;/li&gt;
&lt;li&gt;Leverage &lt;code&gt;select_for_update()&lt;/code&gt; to lock rows during complex updates.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Choosing the right approach depends on your application's requirements for consistency and performance.&lt;/p&gt;
&lt;h1&gt;&lt;a name="references"&gt;&lt;/a&gt;6. References and Further Reading&lt;/h1&gt;
&lt;p&gt;Below are resources that provide more context and depth to the topics discussed in this document:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.djangoproject.com/en/stable/ref/models/querysets/"&gt;Django QuerySet API Reference&lt;/a&gt;: Detailed documentation on QuerySet methods, including &lt;code&gt;select_for_update&lt;/code&gt; and &lt;code&gt;F()&lt;/code&gt; expressions.  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/transaction-iso.html"&gt;PostgreSQL: Transaction Isolation&lt;/a&gt;: Explanation of the different isolation levels supported by PostgreSQL.  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html"&gt;MySQL: InnoDB Isolation Levels&lt;/a&gt;: Information on MySQL's implementation of isolation levels and their guarantees.  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://martin.kleppmann.com/"&gt;"Designing Data-Intensive Applications" by Martin Kleppmann, Chapter 7&lt;/a&gt;: An excellent overview of transaction concepts and their implications for developers.  &lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These resources will help you dive deeper into Django, database isolation levels, and concurrency handling techniques.&lt;/p&gt;</content><category term="django"/><category term="django"/><category term="python"/><category term="webdev"/></entry></feed>