Use Gunicorn preload_app With Django

Decide whether to enable Gunicorn preload_app for Django: memory savings from copy-on-write, faster worker boot, fail-fast startup, the database and client connections you must close after fork, and why reloads need a full restart.

Problem statement

By default each Gunicorn worker imports Django and your project after it forks, so four workers import everything four times and each holds its own copy of the code and data in memory. With preload_app = True, the master imports the application once and then forks workers that share those memory pages until they modify them. That can save hundreds of megabytes on small servers and makes broken code fail immediately at startup. It also changes how connections and reloads behave, which catches teams by surprise.

preload_app in GunicornMaster imports apponcefork() workersshare pagesCopy-on-writeuntil modifiedClose connectionsafter forkPreloading saves memory and boot time but shared sockets must be closed in each worker.
  1. Enable with preload_app = True in gunicorn.conf.py (or --preload).
  2. Expect lower total memory and faster worker boot; measure with PSS (smem), not RSS.
  3. Do not open database, Redis, or HTTP connections at import time; if anything does, close or recreate it in a post_fork hook.
  4. Deploy with a full restart (or USR2 binary upgrade), because HUP reloads do not re-import preloaded code.
  5. Skip preloading if your deploy process relies on HUP reloads to pick up code, or if libraries misbehave after fork.

Step-by-step solution

1. Enable it

# gunicorn.conf.py
preload_app = True
workers = 4

On startup, the master imports config.wsgi, which runs django.setup(). If the import fails (syntax error, missing setting), Gunicorn exits immediately instead of starting workers that crash in a loop.

2. Measure the memory effect

RSS counts shared pages in every process and overstates total use. Use proportional set size:

sudo smem -k -P gunicorn

Compare total PSS with and without preloading. Savings are largest for apps with big imported libraries; they shrink over time as workers touch memory (Python's reference counting writes to objects, so many pages are eventually copied).

four workers, total PSS (illustrative)no preload820 MBpreload, just started430 MBpreload, after a day610 MB0 MB450 MB900 MBSavings are real but shrink as workers touch shared pagesdjango-deployment.com
Illustrative numbers; recycle workers with max_requests to keep the benefit.

3. Close inherited connections

Anything opened in the master is inherited by every worker as the same socket. Two workers sharing one PostgreSQL connection corrupt each other's traffic. Django opens database connections lazily per request, so a clean project is safe, but code that queries at import (or in ready()) leaves a connection open in the master. Close everything after fork:

def post_fork(server, worker):
    from django.db import connections
    for conn in connections.all():
        conn.close()
    from django.core.cache import caches
    for cache in caches.all():
        try:
            cache.close()
        except Exception:
            pass

Also recreate module-level clients that hold sockets or threads (Redis clients, gRPC channels, some SDKs, Sentry transports are fork-aware). Random seeds are another subtlety: libraries that seed once at import produce identical sequences in every worker; Python's random and secrets are reseeded safely by the standard library after fork.

4. Change how you deploy

With preloading, a HUP signal starts new workers by forking the existing master, which still has the old code in memory. To deploy new code:

  • systemctl restart gunicorn (brief gap, mitigated by Nginx retries or a load balancer), or
  • send USR2 to start a new master with new code, then WINCH and QUIT the old one for a zero-downtime binary upgrade, or
  • use symlinked releases with a full restart.

5. When not to preload

Skip it if your deploy tooling depends on HUP reloads, if a library is known to break after fork (some C extensions with threads), or with --reload in development. Uvicorn workers under Gunicorn can also be preloaded, but event loops must not be created before fork.

preload_app trade-offsBenefits✓less memory via copy-on-write✓workers boot almost instantly✓import errors stop startupCosts✕HUP no longer loads new code✕import-time connections shared✕fork-unsafe libraries breakWorth it on memory-tight servers if deploys use full restartsdjango-deployment.com
Test a deploy end to end after enabling it.

Verification check: after enabling, smem shows lower total PSS, the Gunicorn log shows workers booting quickly, a deploy with a full restart serves the new code (check a version endpoint), and no database errors such as "SSL error: decryption failed" or "server closed the connection unexpectedly" appear.

Explanation

Unix fork() creates child processes that share the parent's memory pages, copying a page only when either process writes to it. Preloading moves the expensive import of Django and libraries into the master so that workers start as copies of an already-initialised process. Sockets and file descriptors are also duplicated by fork, which is why any connection opened before fork must be closed in each child. Because the master keeps the imported code, reloading by forking new workers cannot pick up changes, so deploys need a new master.

Edge cases / notes

  • Python's garbage collector writes to object headers, causing pages to be copied; gc.freeze() before fork (in pre_fork or after import) can keep more pages shared.
  • max_requests recycling forks fresh workers from the master, restoring sharing.
  • Celery has a similar concept: prefork pool workers import the app in the parent.
  • Containers: memory limits count unique pages, so preloading can let more workers fit under a limit.

Next steps

FAQ

What does Gunicorn preload_app do?

It imports the application in the master before forking workers, so workers share memory and start faster.

Does preload_app break database connections?

Only if a connection is opened before fork. Close connections in a post_fork hook to be safe.

Why are code changes not picked up after reload?

With preloading, HUP forks from the old master. Restart Gunicorn or use a USR2 upgrade.

How much memory does preloading save?

It depends on the app; often a third or more at start, decreasing as workers modify shared pages.

Keep going