Fix a Slow First Request After a Django Deploy
Fix slow first requests after a Django deploy or restart: measure cold versus warm, find heavy imports with -X importtime, precompile bytecode, preload, warm workers, and avoid cold starts.
Problem statement
Right after a deploy or restart, the first request to each page takes several seconds, sometimes long enough to hit a proxy or load balancer timeout. Subsequent requests are fast. Monitoring shows a latency spike on every deploy, and users who happen to arrive first see a spinner or a 504.
The first request in each worker process pays one-time costs: importing modules that were not needed at startup, compiling Python bytecode, loading and compiling templates, building URL resolvers, opening database connections, and filling empty caches. Each Gunicorn worker pays them separately. This guide measures where the time goes and moves that work out of user requests.
- Measure: time the first and second request to the same URL on a freshly started worker.
- Profile imports with
python -X importtime -c "import config.wsgi"and move heavy imports out of request paths or into startup. - Precompile bytecode during the build (
python -m compileall) so workers do not compile.pyfiles at runtime. - Use Gunicorn
--preload(or warm-up inwsgi.py) so shared work happens once before forking. - Send warm-up requests to new instances before they receive traffic, via readiness checks that hit real pages.
- On platforms that scale to zero, keep a minimum number of instances running.
Step-by-step solution
1. Measure the cold penalty
sudo systemctl restart gunicorn
sleep 2
for i in 1 2 3; do
curl -s -o /dev/null -w "request $i: %{time_total}s\n" -H "Host: example.com" \
--unix-socket /run/gunicorn/gunicorn.sock http://localhost/products/
done
With several workers, consecutive requests may land on different cold workers, so run more requests than you have workers and look at the pattern. A large gap between the first requests and the rest confirms a warm-up cost.
2. Find heavy imports
DJANGO_SETTINGS_MODULE=config.settings.production \
python -X importtime -c "import config.wsgi" 2> /tmp/imports.txt
sort -t'|' -k2 -n /tmp/imports.txt | tail -20
The second column is cumulative import time in microseconds. Then import the modules the first request needs:
python -X importtime -c "
import django, os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.production')
django.setup()
import config.urls
" 2> /tmp/urls-imports.txt
sort -t'|' -k2 -n /tmp/urls-imports.txt | tail -20
Typical heavy imports are data science libraries (pandas, numpy, scikit-learn), PDF and image libraries, cloud SDKs, and large generated API clients. If only a few views need them, import them inside those functions. If most requests need them, import them at startup (step 4) so no user waits.
URL resolution is lazy: Django imports every module referenced by urls.py on the first request that resolves a URL. Large URL trees pull in many view modules at that moment.
3. Precompile bytecode
Python compiles .py files to bytecode on first import and caches it in __pycache__. If the cache directory is not writable (read-only filesystems, or images built with PYTHONDONTWRITEBYTECODE=1), every new process compiles everything again. Compile during the build:
RUN python -m compileall -q /app /usr/local/lib/python3.12/site-packages
For non-container deploys, run python -m compileall -q . in the release directory before starting Gunicorn.
4. Do shared work once with --preload
gunicorn config.wsgi:application --preload --workers 4 --bind unix:/run/gunicorn/gunicorn.sock
With --preload, the master imports the application (and whatever wsgi.py imports) before forking workers, so each worker starts with those modules already loaded, and memory pages are shared copy-on-write. You can add explicit warm-up to wsgi.py:
# config/wsgi.py
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
application = get_wsgi_application()
# Warm-up: import URLconf and view modules before workers fork
from django.urls import get_resolver # noqa: E402
get_resolver().url_patterns
Do not open database connections or network clients in the master: forked workers would share the same sockets. Django opens database connections lazily per worker, which is safe. With --preload, a code change requires a full restart instead of a HUP reload.
5. Warm templates and caches
With DEBUG = False, Django uses the cached template loader, so each template is compiled once per process, on first use. That is fast for most templates; complex template trees can take tens of milliseconds per page type. Warm-up requests (step 6) cover this naturally.
Empty caches after a deploy are a separate cold start: if you change cache key prefixes or KEY_PREFIX/VERSION on every release, every page is recomputed at once. Change cache versions only when the cached data format changes, and precompute expensive fragments in a background job.
6. Warm new instances before traffic
Point readiness checks, or a post-start step in your deploy, at pages that exercise the application, not only a trivial health view:
# after starting the new release, before switching the proxy or load balancer
for path in / /products/ /accounts/login/; do
for i in $(seq 1 8); do
curl -s -o /dev/null -H "Host: example.com" --unix-socket /run/gunicorn/gunicorn.sock "http://localhost$path"
done
done
Send enough requests to reach every worker. In Kubernetes, a startupProbe or readinessProbe hitting a warm-up endpoint, together with maxUnavailable: 0, keeps traffic on old pods until new ones are warm.
7. Avoid platform cold starts
On Cloud Run, App Engine, Azure Container Apps, and similar platforms, instances scaled to zero must start a container, boot Python, and import Django before serving. Set a minimum instance count for user-facing services, and keep images small so they pull quickly.
Verification check:
Restart or deploy, then repeat the measurement from step 1 immediately after the warm-up step. The first user-facing request should now be close to the warm timing, and deploy-time latency spikes should flatten in your monitoring.
Explanation
A Python process only does work when asked: modules are imported on first use, templates compiled on first render, URL patterns loaded on first resolve, connections opened on first query. Gunicorn multiplies this by the number of workers, because each is a separate process. The fixes all move the same work earlier: into the build (bytecode), into the master before forking (preload), or into synthetic requests before real ones (warm-up).
Edge cases / notes
Worker recycling
--max-requests restarts workers periodically to contain memory growth; each new worker is cold. Use --max-requests-jitter so workers do not all restart at once, and combine with --preload so restarted workers start warm.
Slow first request to external services
TLS handshakes and DNS lookups to payment or email APIs happen on first use per process. Reusing HTTP sessions (a module-level requests.Session created lazily per process) avoids paying that on every request.
When to automate this
Make the warm-up step part of the deploy script or readiness probe, and track "first request after deploy" latency as a metric. A regression in that number usually means someone added a heavy import to a common path.
Next steps
- Size and configure workers with How to Optimize Gunicorn Workers for Django.
- Find slow code paths with Profile Slow Django Endpoints in Production with py-spy.
- Keep old workers serving until new ones are ready with How to Set Up Zero-Downtime Deployments for Django.
- Build a meaningful readiness check with How to Build a Django Health Check Endpoint for Deployments.
FAQ
Why is the first request after restarting Django so slow?
Each worker imports modules, compiles templates, loads URL patterns, and opens connections on its first request. Those one-time costs make the first response slow.
Does Gunicorn --preload make startup faster?
It moves application import into the master process, so workers start with modules already loaded. It speeds up worker readiness and saves memory.
Should I precompile Python bytecode in Docker images?
Yes, especially if the runtime filesystem is read-only or bytecode writing is disabled. Run python -m compileall during the build.
How do I warm up Django after a deploy?
Send requests to key pages on the new instances before switching traffic to them, enough to reach every worker.