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.
- Enable with
preload_app = Trueingunicorn.conf.py(or--preload). - Expect lower total memory and faster worker boot; measure with PSS (
smem), not RSS. - Do not open database, Redis, or HTTP connections at import time; if anything does, close or recreate it in a
post_forkhook. - Deploy with a full restart (or
USR2binary upgrade), becauseHUPreloads do not re-import preloaded code. - Skip preloading if your deploy process relies on
HUPreloads 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).
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
USR2to start a new master with new code, thenWINCHandQUITthe 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.
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 (inpre_forkor 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
- Choose worker counts with How to Optimize Gunicorn Workers for Django.
- Put the settings in one place with Write a gunicorn.conf.py for Django.
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.