14 Jun 2026

feedDjango community aggregator: Community blog posts

Understanding Memory Usage in Django Webserver Workers

If you are coming from the PHP world, you might be used to thinking that when a request reaches the web server, everything is parsed and processed from scratch. In Python, however, the behavior is a little different.

A Python web server (for example, Gunicorn) starts one or more worker processes and then continuously accepts and processes requests as a running server application. In this article, I will explore how memory is managed in that environment.

Startup time vs. request execution

When you run a Django web server (either the development server or a Gunicorn-based deployment), there are two phases of execution: startup, when the server application is initialized, and the request/response cycle, when a request is received and processed to return a response.

During startup, wsgi.py and get_wsgi_application() are executed once per worker at boot time, Django settings are evaluated, applications listed in INSTALLED_APPS are imported and registered, ready() methods of app configs are called, and URL patterns are compiled.

During request execution, the middleware chain processes the request, the URL is resolved, the view is executed, context managers run within the view, the template is rendered, the middleware chain processes the response, and the response is returned.

The Django development server runs a single worker process but uses threads to handle concurrent requests. Gunicorn runs multiple worker processes (separate copies of the server application). A common rule of thumb for Gunicorn configuration is (2 × number_of_CPU_cores) + 1 workers.

Little demo of shared memory between requests

If you have mutable globals in your Django code, they will be shared within the same worker process, even across threads.

Here's a simple example for demonstration purposes (note that this is an antipattern and should never be used in production):

from django.http import JsonResponse
from django.utils.timezone import now

TIMESTAMPS = []

def show_timestamps(request):
    TIMESTAMPS.append(now())
    return JsonResponse({"timestamps": TIMESTAMPS})

The output would be (manually indented for readability):

{"timestamps": [
  "2026-06-08T19:48:42.044Z", 
  "2026-06-08T19:48:43.875Z", 
  "2026-06-08T19:48:44.776Z"
]}

If you serve this application with Gunicorn using multiple workers and open the view in several tabs, refreshing them a few times, you will see that the timestamp list keeps growing. However, because each request may be served by a different worker, the timestamps returned by each worker will differ.

How RAM is managed

1. Compilation/setup state is persistent

When Django starts and the application server is initialized, the following objects live in process memory for the lifetime of the worker process:

This memory normally remains allocated until the worker process restarts.

2. Execution state lives per-request

Each request creates objects that are local to that request's call stack-views, forms, querysets, serializers, and so on. Once the response is sent:

Common bugs to avoid

1. Mutable module-level state

A mutable object defined at the module level persists across all requests handled by that worker process and will accumulate data from every user.

BAD:

_recent_users = []

def dashboard(request):
    # leaks across requests!
    _recent_users.append(request.user.id)
    return render(
        request, 
        "dashboard.html", 
        {"recent": _recent_users}
    )

GOOD - state belongs in the DB, cache, or session:

def dashboard(request):
    recent_users = get_recent_users_from_db()
    return render(
        request, 
        "dashboard.html", 
        {"recent": recent_users}
    )

2. Storing per-request data on a shared instance

Middleware instances live for the lifetime of the worker process. Storing anything request-specific on self means it will be shared across every request handled by that worker.

BAD:

class AuditMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        # shared across all requests!
        self.current_user = None

    def __call__(self, request):
        # User A's data visible when User B's request runs
        self.current_user = request.user
        return self.get_response(request)

GOOD - attach data to the request object, which is scoped to a single request:

class AuditMiddleware:
    def __init__(self, get_response):
        # only immutable config on self
        self.get_response = get_response

    def __call__(self, request):
        # scoped to this request's lifetime
        request.current_user = request.user
        return self.get_response(request)

3. Class-level cache shared across instances

A class attribute is shared across all instances of that class and therefore across all requests. The first user's data may be cached and returned to everyone else.

BAD:

class ReportGenerator:
    # class attribute, shared across all instances and all requests!
    _cache = {}

    def get_data(self, user):
        if "data" not in self._cache:
            # first user's data cached for everyone
            self._cache["data"] = expensive_query(user)  
        return self._cache["data"]

GOOD - use the cache framework with a user-scoped key:

from django.core.cache import cache

class ReportGenerator:
    def get_data(self, user):
        key = f"report:{user.id}"
        return cache.get_or_set(
            key, 
            lambda: expensive_query(user), 
            timeout=300,
        )

Mental model

Before writing module-level code, ask yourself:

"If 1000 different users hit this worker, and this variable exists for all of them, is that safe?"

Location Lifetime Safe for user data?
Local variable in a view/function Single request ✅ Yes
request object attributes Single request ✅ Yes
threading.local() Single thread ✅ Yes
Database / cache with scoped keys Persistent but keyed ✅ Yes
self on a middleware/class instance Worker lifetime ⚠️ Only immutable config
Module-level mutable variable Worker lifetime ❌ Never user data
Class-level mutable attribute Worker lifetime ❌ Never user data

Rule of thumb: immutable configuration (numbers, booleans, strings, bytes, tuples, frozensets, and None) belongs at module level; anything that varies by request, user, or time belongs in the request cycle.

Conclusion

A Django worker process has two separate memory areas: a startup state that lives for the lifetime of the worker process (app registry, URL resolvers, middleware instances, template engines) and a request state that exists only while handling a request (views, querysets, forms) and is reclaimed by Python's garbage collector afterward.

The main rule is that immutable configuration can live at the module level, but anything that depends on the current user or request should stay inside the request cycle, such as local variables or the request object.

That's because module-level variables, class attributes, and middleware instance state are shared by all requests handled by the same worker, making them unsafe for user-specific data and a common cause of memory leaks and data exposure bugs.


Cover Picture by Jon Tyson

14 Jun 2026 5:00pm GMT

12 Jun 2026

feedDjango community aggregator: Community blog posts

Issue 341: Django 2026 Fundraising Goals

News

DSF 2026 Fundraising Goals

The Django Software Foundation is raising its 2026 annual fundraising goal from $300,000 to $500,000. The money supports the Django Fellows program, legal and trademark work, community grants and events, ongoing infrastructure, and progress toward hiring an Executive Director.

Announcing Our DjangoCon US 2026 Talks!

DjangoCon US 2026 released its tutorial and talk lineup for Aug 24 to Aug 26, with live access for online-only ticket holders and free YouTube uploads after the conference. Expect sessions spanning Django 6.1 and modern ORM patterns, performance testing, Wagtail routing, Postgres updates, and deployment topics, with the final schedule to follow soon.

Vulnerability and malware checks in uv

uv introduces uv audit to scan locked dependencies for known vulnerabilities and adverse statuses, positioned as a faster uv-native alternative to pip-audit.


Updates to Django

Today, "Updates to Django" is presented by Hwayoung from Djangonaut Space! 🚀

Last week we had 16 pull requests merged into Django by 11 different contributors - including 5 first-time contributors! Congratulations to jodizzle, Bankai, Chris Rose, esperonus-karolis and Wes P. for having their first commits merged into Django - welcome on board!

This week's Django highlights: 🦄


Releases

Python 3.14.6 and 3.13.14 are now available!

Python 3.14.6 is out as the sixth 3.14 maintenance release, with about 179 bugfixes plus build and documentation updates since 3.14.5. Python 3.13.14 follows as the fourteenth 3.13 maintenance release, adding around 240 bugfixes along with build and documentation changes since 3.13.13.

Core Dispatch #5

Python 3.15.0 beta 2 landed June 2, with another round of milestones due June 9 and June 23. Expect the practical stuff in 3.15 including a fixed O(n^2) blowup in unicodedata.normalize, XML multi-byte encoding support, and fresh deprecation warnings around ast and abc abstract* helpers, alongside an initial documented Python security policy in the Devguide.

Sponsored Link

Middleware, but for AI agents

Django middleware composes request handlers. Harnesses do the same for AI agents - Claude Code, Codex, Gemini in one coordinated system. Learn what a harness actually is, why it's a new primitive, and how to engineer one that holds in production. Apache 2.0, open source.


Articles

My Local LLM Setup - Fast Agentic Development with No Token Bill

From Peter Grandstaff, a write-up of his maxxed-out local LLM setup using an Nvidia GeForce RTX 4090 video card. A very cool setup and one which many of us will likely be using some version of in the future.

Logical optimizations

Nested if blocks can often be rewritten as a single conjunction, but only when each if fully owns the body with no else or trailing code.

Browser Push Notifications for a Django Website

Set up browser push notifications end to end: store push subscriptions in Django, register/unregister via authenticated endpoints, and send notifications from a Huey background task using pywebpush with VAPID.

Anything new?

Maintainer Matthias Kestenholz explains why stepping away from django-mptt is hard, and how entitlement in issue trackers turns "free labor" into a burnout trap.

PyCon US 2026 - Open Source Community in Long Beach - Peter Grandstaff

Another one from Peter Grandstaff, a day-by-day PyCon US 2026 trip focused on building Django connections, sponsorship work for DjangoCon US, and catching talks on developer experience and open source community support.


Django Forum

Switch to Playwright tests for integration testing

This project aims to modernize Django's integration testing by introducing Playwright as an alternative to Selenium.

Proposal: Leverage Oracle Test Pilot for Django CI

A proposal from the Oracle Test Pilot for Third-Party Software program building Oracle Test Pilot to provide access to the latest versions of the Oracle databases to Django on GitHub, for free.


Django Fellow Reports

Natalia Bidart

This week was quite intense, with most of the focus 🔍 on getting the security release out the door 🚪. Issuing the release for the 5 CVEs took a fair amount of coordination and attention to detail, and definitely consumed a good chunk of brain power 🧠 ⚡.

Alongside that, there were a number of meetings throughout the week, so overall it was a mix of high-focus release work and keeping in sync with the different groups 🤝. Bonus: the final DEP 0018 for MAILERS was approved, moved to the accepted folder, and merged ✅.

Jacob Walls

A highlight this week was landing the listurls command modeled on django-extensions. Many tickets triaged, reviewed, authored, and discussed.


Events

Announcing Our DjangoCon US 2026 Talks!

The complete lineup of talks, August 24-26, is now live! So many great talks coming up.

Django Meetup Vol. 78 / Beyond Boilerplate: Building Maintainable CRUD in Django

Django Meetup Cologne Vol. 78 will take place on the 16th of June 2026, online and in person.


Django Job Board

Founding ML/Data Scientist (Remote, UK) at MyDataValue 🆕


Projects

django-helpdesk/django-helpdesk

A Django application to manage tickets for an internal helpdesk. Formerly known as Jutda Helpdesk.

emmett-framework/granian

A Rust HTTP server for Python applications.

12 Jun 2026 3:00pm GMT

10 Jun 2026

feedDjango community aggregator: Community blog posts

Running Fallout London on Bazzite

I'm a huge Fallout fan, and Fallout London is one of the most impressive mods I've seen in years: a full DLC-sized expansion set in post-apocalyptic London, made by a community team. Running it on Bazzite (my gaming OS of choice) wasn't completely straightforward, so here's what actually worked for me. Consider this a note to future me, but hopefully it saves someone else an afternoon of trial and error.

What you'll need

The steps

1. Install Heroic Games Launcher

If you don't have it yet, install Heroic from the Bazzite app store or via Flatpak. It's a fantastic open-source launcher that handles GOG, Epic, and Amazon games, and it plays very nicely with Proton.

After you install it, login with your GOG account

Heroic Games Launcher main screen

2. Install the Fallout London One Click Mod

Search for "Fallout London" in Heroic and install the One Click Mod version. This bundles everything together so you don't have to manually manage mod files. Let it do its thing.

Fallout London in Heroic Games Launcher

3. Disable UMU (yes, it needs to be disabled)

This is the counterintuitive part. Once the mod is installed, go to its settings in Heroic, then the Advanced tab. You'll see an option called "Disable UMU". Enable it (meaning: check the checkbox to disable UMU). I know, "enable the disable" is a confusing way to phrase it, but that's what it says.

Without this, the game won't launch correctly on Bazzite.

Heroic advanced settings showing Disable UMU option

4. Run it once in Desktop Mode

Before adding it to Steam, launch the game once directly from Heroic while you're in Desktop Mode. This lets everything install and configure properly: shaders, redistributables, the works. Wait until you're actually in the game and confirmed it runs without issues, then close it.

5. Add to Steam

Click the three-dot menu on the game in Heroic and select "Add to Steam". From this point on you can launch it from Game Mode like any other game in your library.

Adding Fallout London to Steam via Heroic

Play!

That's it. Boot into Game Mode, find Fallout London in your library, and enjoy one of the best Fallout experiences made outside of Bethesda.

Fallout in Steam
Fallout London title screen

See you in the next one!

10 Jun 2026 5:00am GMT