Django WSGI vs ASGI: Which One Should You Deploy?
When you deploy Django in production, you will see both WSGI and ASGI recommended.
Problem statement
When you deploy Django in production, you will see both WSGI and ASGI recommended. The confusing part is that both are valid, both are supported by Django, and both can sit behind the same reverse proxy.
The real deployment question is not “which one is newer?” It is:
- do you need a simple and stable request/response stack, or
- do you need async features like websockets, long-lived connections, or an async-first service model?
Your choice affects:
- which app server you run
- how you configure process supervision with
systemd - what Nginx must proxy
- what failure modes you need to monitor
- how hard rollback will be if you switch later
Quick answer
For most traditional Django apps, deploy WSGI first.
Use ASGI when you clearly need:
- websockets
- long-lived connections
- Django Channels
- streaming or realtime features
- async views backed by truly async dependencies
A practical default is:
- WSGI stack:
Nginx -> Gunicorn -> Django wsgi.py - ASGI stack:
Nginx -> UvicornorNginx -> Gunicorn with Uvicorn workers -> Django asgi.py
What does not change: you still need TLS, secrets management, static files, migrations, health checks, logging, and rollback steps.
Step-by-step solution
1. Identify what your app actually needs
Use this decision rule before changing anything in production:
- Choose WSGI if your app is mostly:
- admin
- CRUD
- normal HTML pages
- standard REST APIs
- background tasks handled separately by Celery or cron
- Choose ASGI if your app needs:
- websockets
- chat or live notifications
- collaborative editing
- long polling
- streaming responses
- async endpoints that depend on async libraries
If you are unsure and reliability matters more than future flexibility, start with WSGI.
2. Confirm the Django entrypoint you will deploy
Django ships with both entrypoints in most projects.
Typical files:
# project/wsgi.py
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
application = get_wsgi_application()
# project/asgi.py
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
application = get_asgi_application()
Your app server loads one of these:
- WSGI server loads
project.wsgi:application - ASGI server loads
project.asgi:application
3. Run the correct app server command
WSGI with Gunicorn
gunicorn --bind 127.0.0.1:8000 project.wsgi:application
This is the common production default for standard Django deployments.
ASGI with Uvicorn
uvicorn project.asgi:application --host 127.0.0.1 --port 8000
ASGI with Gunicorn and Uvicorn workers
gunicorn project.asgi:application -k uvicorn.workers.UvicornWorker --bind 127.0.0.1:8000
This hybrid setup is common when teams want Gunicorn-style process management but need an ASGI app.
4. Put process management under systemd
Do not run these commands manually in production.
Example WSGI service
# /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn for Django project
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/srv/project/current
EnvironmentFile=/etc/project/project.env
ExecStart=/srv/project/venv/bin/gunicorn \
--workers 3 \
--bind 127.0.0.1:8000 \
project.wsgi:application
Restart=always
RestartSec=5
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target
Example ASGI service with Uvicorn
# /etc/systemd/system/uvicorn.service
[Unit]
Description=Uvicorn for Django project
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/srv/project/current
EnvironmentFile=/etc/project/project.env
ExecStart=/srv/project/venv/bin/uvicorn \
project.asgi:application \
--host 127.0.0.1 \
--port 8000
Restart=always
RestartSec=5
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target
For production, a single Uvicorn process may be enough for small apps, but higher traffic usually needs multiple workers or a Gunicorn + Uvicorn worker setup.
Store secrets in an environment file, not in the service unit. That file should be outside your repository, root-owned, and readable only by the service group your app runs under.
Example using Django-native database variables:
# /etc/project/project.env
DJANGO_SETTINGS_MODULE=project.settings
DJANGO_SECRET_KEY=replace-me
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=example.com
DB_NAME=project
DB_USER=projectuser
DB_PASSWORD=replace-me
DB_HOST=127.0.0.1
DB_PORT=5432
If your settings module parses a DSN with a helper such as dj-database-url, a DATABASE_URL variable is also valid, but Django does not read that automatically by itself.
Protect the env file:
sudo chown root:www-data /etc/project/project.env
sudo chmod 640 /etc/project/project.env
Then enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable gunicorn
sudo systemctl start gunicorn
Or for ASGI:
sudo systemctl daemon-reload
sudo systemctl enable uvicorn
sudo systemctl start uvicorn
5. Put Nginx in front of the app server
For both WSGI and ASGI, bind the app server to localhost and expose only Nginx publicly.
Basic Nginx reverse proxy
server {
listen 80;
server_name example.com;
location /static/ {
alias /srv/project/current/staticfiles/;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
}
}
For ASGI apps using websockets, use a safer upgrade configuration:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
}
}
If you terminate TLS at Nginx, Django also needs the matching proxy-aware settings:
# settings.py
DEBUG = False
ALLOWED_HOSTS = ["example.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
If you accept cross-origin HTTPS POSTs or use trusted external admin origins, set CSRF_TRUSTED_ORIGINS explicitly as well:
CSRF_TRUSTED_ORIGINS = ["https://example.com"]
Test and reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
6. Run release tasks that do not depend on WSGI or ASGI
Before switching traffic, run the normal Django production tasks:
source /srv/project/venv/bin/activate
cd /srv/project/current
python manage.py migrate
python manage.py collectstatic --noinput
Then verify your app locally first and through the reverse proxy:
# Replace /health/ with your actual health endpoint if you expose one
curl -I http://127.0.0.1:8000/health/
curl -I https://example.com/health/
7. Verify the deployment after startup
Check service health:
systemctl status gunicorn
or:
systemctl status uvicorn
Check logs:
journalctl -u gunicorn -n 50 --no-pager
or:
journalctl -u uvicorn -n 50 --no-pager
Watch for:
- startup import errors
- database connection failures
- bad
DJANGO_SETTINGS_MODULE - missing environment variable parsing in your settings module
- permission errors on env files or static files
DisallowedHosterrors fromALLOWED_HOSTS502errors from Nginx
8. Keep a rollback path if you switch interfaces
If you move from WSGI to ASGI, do not combine that change with a risky schema migration.
A safe rollback plan is:
- keep the old
systemdunit file - keep the previous app server command
- keep the previous Nginx config
- switch the service back
- restart the previous service
- reload Nginx
- verify health and logs
Example rollback sequence:
sudo systemctl stop uvicorn
sudo systemctl start gunicorn
sudo systemctl reload nginx
curl -I https://example.com/health/
journalctl -u gunicorn -n 50 --no-pager
If the ASGI rollout also changed Nginx websocket proxying or introduced Channels/Redis, include those components in the rollback plan as well.
Explanation
What WSGI handles well
WSGI is the right default for most Django production apps because it is simple and mature. If your traffic is standard HTTP request/response and your app logic is mostly synchronous, WSGI gives you a predictable process model and easier debugging.
A typical stack is:
- Nginx
- Gunicorn sync workers
- Django via
wsgi.py - PostgreSQL
- optional Redis for cache or task queue
This is enough for many admin systems, client portals, APIs, and content sites.
What ASGI adds
ASGI adds support for async protocols and long-lived connections. It matters when you need:
- websockets
- realtime browser updates
- Django Channels
- async-first APIs
- streaming behavior that benefits from an event loop
A typical stack is:
- Nginx
- Uvicorn, or Gunicorn with Uvicorn workers
- Django via
asgi.py - Redis if Channels or coordination requires it
ASGI is not automatically faster. If your code still spends most of its time in synchronous database calls or blocking libraries, ASGI may add complexity without meaningful benefit.
Worker model and failure modes
With WSGI, a sync worker can get tied up by slow requests. Capacity planning is usually about worker count, memory use, and timeout tuning.
With ASGI, you also need to care about:
- blocking sync code inside async paths
- websocket proxy configuration
- connection counts
- event loop stalls
- timeout mismatches between Nginx and the app server
For both models, monitor:
502and504rates- worker restarts
- response times by endpoint
- memory growth
- connection failures in logs
When to automate this
Once you are repeating the same setup across multiple servers or environments, convert the manual steps into reusable templates. The best first targets are systemd service generation, Nginx config generation, environment file checks, health verification, and rollback commands. Keep the WSGI and ASGI variants modular so you can switch app server type without rewriting the whole release process.
Edge cases or notes
- Async views under WSGI: Django can run them, but you do not get the full benefit of an ASGI-native deployment.
- One project with both sync and async behavior: possible, but production should still choose one primary serving interface.
- Static files: neither WSGI nor ASGI should serve production static files directly; let Nginx or a CDN do it.
- Media files: plan media storage separately from static files. Do not assume the static file path layout is appropriate for user uploads.
- TLS: terminate TLS at Nginx or another reverse proxy, not at the Django app server.
- Security: keep
DEBUG=False, setALLOWED_HOSTS, and bind Gunicorn/Uvicorn to127.0.0.1or a private interface only. - Channels: if you use Django Channels, ASGI is required, and Redis is commonly added for the channel layer.
- Do not switch interfaces during a risky release: change the serving layer separately from large migrations so rollback stays simple.
Internal links
If you need the wider production picture, start with Django deployment architecture explained.
For implementation details, see:
For production settings and release safety, also review:
- Django Deployment Checklist for Production
- Django Production Settings Checklist (DEBUG, ALLOWED_HOSTS, CSRF)
If the app stops responding after the change, use How to fix 502 Bad Gateway in Django behind Nginx.
FAQ
Is WSGI or ASGI better for most Django apps?
For most Django apps, WSGI is the better default because it is simpler to operate and fully supports normal request/response workloads. Use ASGI when you have a clear async requirement such as websockets or realtime features.
Do I need ASGI to use async views in Django?
Not always, but ASGI is the correct deployment target if async behavior is a real part of your production design. Running async views under WSGI works, but it does not provide the full ASGI execution model.
Should I switch an existing Django app from WSGI to ASGI?
Only if your requirements justify it. If the app is stable on WSGI and does not need websockets, streaming, or async-first behavior, switching may add complexity without operational benefit.
Can I run Django Channels without ASGI?
No. Django Channels requires an ASGI deployment path.
Does Gunicorn work with both WSGI and ASGI?
Yes. Gunicorn works directly with WSGI apps, and it can also run ASGI apps using Uvicorn workers:
gunicorn project.asgi:application -k uvicorn.workers.UvicornWorker --bind 127.0.0.1:8000
That makes it a practical option for teams that already use Gunicorn in production.