Python WSGI/ASGI #
Just like the PHP ecosystem, the Python runtime isn’t designed to handle thousands of simultaneous HTTP connections directly from the internet. To serve Python web applications (like Django, Flask, FastAPI, or Starlette) in large-scale production environments, we place Nginx at the front line as a reverse proxy. Nginx acts as the party responsible for SSL/TLS termination security, static file serving, and request buffering, while Python code execution is delegated to an intermediate application server.
In the Python ecosystem, communication between the web server (Nginx) and the web application is bridged by two interface standards: WSGI (Web Server Gateway Interface) for traditional synchronous applications, and ASGI (Asynchronous Server Gateway Interface) for modern asynchronous applications. In this article, we’ll dissect the WSGI vs ASGI difference, compare uWSGI vs Gunicorn, configure ASGI with Uvicorn, optimize Django/FastAPI static file serving, and put together production-ready configurations.
Python Web Server Integration Architecture #
Nginx acts as the front filter. If a client request is for a static file (like image files or compiled CSS/JS files), Nginx serves it directly from the local storage folder. If the request is for a dynamic page or API, Nginx forwards it to the application server (Gunicorn/uWSGI/Uvicorn) running our Python code.
Here’s a diagram of the Python request processing flow on the server:
flowchart TD
Client["Client Browser"] -->|"HTTPS (Port 443)"| Nginx["Nginx Web Server"]
Nginx -->|"1. Static File Request"| Static{"Static Asset / Media?"}
Static -->|"Yes"| StaticDir["Serve Directly from the /static/ or /media/ Folder"]
Static -->|"No (Dynamic Request)"| ProtoCheck{"Backend Protocol Type?"}
ProtoCheck -->|"WSGI (Synchronous - Django/Flask)"| WSGIApp["uWSGI / Gunicorn Server"]
ProtoCheck -->|"ASGI (Asynchronous - FastAPI)"| ASGIApp["Uvicorn / Hypercorn Server"]
WSGIApp -->|"WSGI Interface"| Django["Django / Flask Application"]
ASGIApp -->|"ASGI Interface"| FastAPI["FastAPI / Starlette Application"]
Django --> Nginx
FastAPI --> Nginx
Nginx -->|"Return the HTTP Response"| Client
classDef default fill:#f9f9f9,stroke:#d1d5db,stroke-width:1px,color:#111827;
classDef nginxStyle fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a;
classDef pythonStyle fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f;
class Nginx,StaticDir nginxStyle;
class WSGIApp,ASGIApp,Django,FastAPI pythonStyle;Understanding WSGI vs ASGI in the Python Ecosystem #
Before putting together the Nginx configuration, we must recognize the protocol type supported by our Python application framework:
1. WSGI (Web Server Gateway Interface) #
WSGI is the old standard (since 2003) designed for synchronous applications.
- How It Works: Each client request is served by one Python worker process/thread exclusively. The worker is blocked while waiting for I/O operations (like database queries or external API calls).
- Frameworks: Django (default), Flask, Bottle.
- Application Servers: Gunicorn or uWSGI.
2. ASGI (Asynchronous Server Gateway Interface) #
ASGI is the evolution of WSGI designed to natively support asynchronous features, WebSockets, and long-lived persistent connections.
- How It Works: Uses a non-blocking event loop model (similar to Node.js). One worker process can handle thousands of requests simultaneously without being blocked by slow I/O operations.
- Frameworks: FastAPI, Sanic, Starlette, Django Channels.
- Application Servers: Uvicorn, Hypercorn, or Daphne.
uWSGI Native Protocol vs Gunicorn HTTP Proxy #
For WSGI applications, we can connect Nginx to the application server using the regular HTTP protocol (Gunicorn) or using the native uWSGI binary protocol (uWSGI Server).
1. uWSGI Native Protocol (uwsgi_pass)
#
uWSGI uses a special binary protocol called uwsgi that has smaller header overhead than standard HTTP, making it slightly faster and more efficient.
- Nginx Configuration Snippet:
location / { include uwsgi_params; # Loads standard uwsgi parameters uwsgi_pass unix:/run/uwsgi/myapp.sock; # Forward to the uWSGI unix socket }
2. Gunicorn HTTP Proxy (proxy_pass)
#
Gunicorn acts as a standalone local HTTP server. Nginx communicates with Gunicorn using the standard HTTP reverse proxy module. This is the most popular choice because its configuration is very easy to understand and debug.
- Nginx Configuration Snippet:
location / { proxy_pass http://unix:/run/gunicorn/myapp.sock; # Forward to the Gunicorn unix socket # Or if using a TCP port: # proxy_pass http://127.0.0.1:8000; }
Forwarding the SSL Security Context to the Python Application #
One of the most common obstacles when putting a Python application behind Nginx SSL Termination is that the Python application doesn’t realize the outside client connection is secure (HTTPS). If Django or FastAPI tries to generate absolute URLs (like page redirects after login), they’ll generate http:// URLs instead of https://. This triggers Mixed Content Errors in the client browser or CSRF Token Validation failures.
We must send the X-Forwarded-Proto header from Nginx, then configure our Python framework to honor that header.
1. Configuration in Nginx #
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Tell the backend that the client is using HTTPS on the outside
proxy_set_header X-Forwarded-Proto $scheme;
}
2. Configuration on the Django Side (settings.py)
#
Add the following lines to the Django configuration file so it’s willing to trust headers from Nginx:
# Tell Django to read the X-Forwarded-Proto header
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Additional HTTPS security hardening
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
3. Configuration on the FastAPI Side #
FastAPI / Starlette provides built-in middleware to handle this HTTPS proxy scenario automatically:
from fastapi import FastAPI
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
app = FastAPI()
# If we want to force all FastAPI requests to redirect to HTTPS at the python level
# app.add_middleware(HTTPSRedirectMiddleware)
Django Static Files Offloading (collectstatic) #
Django separates Python code files from static asset files (admin panel images, admin JS/CSS files). In production environments, we run the following Python command to collect all static files from various application modules into one central directory:
python manage.py collectstatic
After the files are collected in one folder (e.g., /var/www/myproject/static/), we configure Nginx to serve that folder directly without ever involving Gunicorn/Django.
server {
listen 80;
server_name app.unisbadri.com;
# 1. Central Static Asset Location (Django collectstatic)
location /static/ {
alias /var/www/myproject/static/;
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
}
# 2. User Uploaded Files Location (Django Media Files)
location /media/ {
alias /var/www/myproject/media/;
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
}
# 3. Dynamic Request Location
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Complete Production Server Block Configuration Examples #
1. Django + Gunicorn Production Template (HTTPS) #
upstream django_app_server {
# Use a Unix Domain Socket for optimal local performance
server unix:/var/run/gunicorn.sock fail_timeout=0;
}
server {
listen 80;
server_name django.unisbadri.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name django.unisbadri.com;
ssl_certificate /etc/letsencrypt/live/django.unisbadri.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/django.unisbadri.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Global file upload size limit
client_max_body_size 20m;
# Offload Django collectstatic
location /static/ {
alias /var/www/django-project/static/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Offload Django User Uploaded Files
location /media/ {
alias /var/www/django-project/media/;
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
}
# Main Gunicorn Reverse Proxy Routing
location / {
# Check static file availability first (optional)
try_files $uri @proxy_to_app;
}
location @proxy_to_app {
proxy_pass http://django_app_server;
# Standard header configuration
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Proxy buffer and timeout settings
proxy_redirect off;
proxy_read_timeout 90s;
proxy_connect_timeout 90s;
# Hide technology information
proxy_hide_header X-Powered-By;
}
}
2. FastAPI + Uvicorn Production Template (HTTPS + WebSocket) #
ASGI applications like FastAPI often handle real-time traffic (WebSockets). Here’s an Nginx configuration designed to support both HTTP reverse proxying and WebSocket tunnels dynamically:
# Map the Connection header based on the client's Upgrade header
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream fastapi_asgi_server {
# Uvicorn runs on port 8000
server 127.0.0.1:8000 max_fails=3 fail_timeout=10s;
keepalive 32;
}
server {
listen 80;
server_name api.unisbadri.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name api.unisbadri.com;
ssl_certificate /etc/letsencrypt/live/api.unisbadri.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.unisbadri.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Offload Static Files (if FastAPI serves frontend static files)
location /static/ {
alias /var/www/fastapi-project/static/;
expires 30d;
access_log off;
}
# Proxying HTTP & WebSockets to Uvicorn
location / {
proxy_pass http://fastapi_asgi_server;
# Enable HTTP/1.1 for Keepalive and WebSockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Standard proxy headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Prevent socket timeouts from cutting off WebSockets mid-way
proxy_read_timeout 86400s; # Set to 24 hours (or use a keepalive ping)
proxy_send_timeout 86400s;
}
}
Tuning Gunicorn & Uvicorn Worker Classes in Production #
When running Django (WSGI) or FastAPI (ASGI) applications behind Nginx, performance and request capacity are heavily influenced by the number and type of workers we use in the application server.
1. Determining the Gunicorn / Uvicorn Worker Count #
In general, the standard formula for determining the number of worker processes on a Python application server so the CPU is optimally utilized without context switching overhead is: [\text{Worker Count} = (2 \times \text{Number of CPU Cores}) + 1]
For example, if our VPS has 2 CPU Cores: [\text{Worker Count} = (2 \times 2) + 1 = 5 \text{ workers}]
We run Gunicorn with the following syntax:
gunicorn --workers 5 --bind unix:/run/gunicorn/myapp.sock myproject.wsgi:application
2. Choosing the Worker Class Type (Gunicorn/Uvicorn) #
Gunicorn supports various worker class types tailored to our application load:
sync(Default): A simple synchronous worker. Very suitable for CPU-bound applications (heavy calculations) that don’t have many external I/O calls. Each worker only serves one connection at a time.geventoreventlet: Greenlet (coroutine)-based workers. Very optimal for I/O-bound applications (many slow database queries or external HTTP API fetches) because they can suspend thread execution while waiting for data without blocking the CPU.eggry/tornado: Alternative asynchronous workers.uvicorn.workers.UvicornWorker: A special worker class for running ASGI (FastAPI) applications inside the Gunicorn wrapper. This combines Gunicorn’s process management capabilities (auto-restart, cluster management) with Uvicorn’s very fast uvloop-based asynchronous event loop performance.
# Running FastAPI with Gunicorn process management behind Nginx
gunicorn myfastapiapp.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 127.0.0.1:8000
By integrating Nginx with Gunicorn using UvicornWorker, we get high-level server resilience because Gunicorn monitors Uvicorn worker health and automatically restarts workers if they’re detected dead or experiencing memory leaks.
Troubleshooting Production Cases #
Here are some of the most commonly encountered issues when pairing Nginx with Python backends in production:
502 Bad GatewayError on Unix Sockets:- Cause: Nginx doesn’t have permissions to read the
/var/run/gunicorn.socksocket file because it was created by Gunicorn running as therootorpythonuser, while Nginx runs aswww-data. - Solution: Run Gunicorn with the
--umask 007flag or set the socket access rights manually withchmod 660 /var/run/gunicorn.sockso thewww-datagroup has write-read access.
- Cause: Nginx doesn’t have permissions to read the
- Django Admin Panel CSS Not Appearing (404):
- Cause: We forgot to run the
collectstaticcommand in Django, or thealiasdirectory in the Nginx/static/location block points to the wrong local folder. - Solution: Run
python manage.py collectstatic, make sure the folder path in the Nginx configuration ends with the same trailing slash/as the alias target (e.g.,alias /path/to/static/;).
- Cause: We forgot to run the
- Infinite Redirect Loop:
- Cause: Django is set to force HTTPS (
SECURE_SSL_REDIRECT = True), but Nginx calls Gunicorn over local HTTP without including theproxy_set_header X-Forwarded-Proto $scheme;header. Django thinks the request is still plain HTTP, then responds with a redirect to HTTPS continuously. - Solution: Add the
X-Forwarded-Protoheader directive in Nginx and make sureSECURE_PROXY_SSL_HEADERis declared in Django’ssettings.py.
- Cause: Django is set to force HTTPS (
Summary and Best Practices #
- Must Set SECURE_PROXY_SSL_HEADER: Never skip this configuration in Django settings if our server is behind Nginx SSL Termination to prevent CSRF authentication problems.
- Do Static Asset Offloading: Make sure Django static assets (
/static/and/media/) are served directly by Nginx. This cuts server response time by up to 4x.- Choose Gunicorn for Release Speed: If our developer team is less familiar with complex uWSGI parameter tuning, use the friendlier and more stable Gunicorn HTTP proxy.
- Use HTTP/1.1 for Uvicorn: Uvicorn (FastAPI) requires the HTTP/1.1 protocol so WebSocket Upgrade handshakes and asynchronous connection mapping can run smoothly.
← Previous: PHP-FPM (Laravel/WordPress) Next: Single Page Application (React/Vue) →