Deployment
#django
#gunicorn
#uvicorn

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 -> Uvicorn or Nginx -> 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
  • DisallowedHost errors from ALLOWED_HOSTS
  • 502 errors 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:

  1. keep the old systemd unit file
  2. keep the previous app server command
  3. keep the previous Nginx config
  4. switch the service back
  5. restart the previous service
  6. reload Nginx
  7. 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:

  • 502 and 504 rates
  • 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, set ALLOWED_HOSTS, and bind Gunicorn/Uvicorn to 127.0.0.1 or 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.

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:

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.

2026 · django-deployment.com - Django Deployment knowledge base