Gunicorn + Uvicorn is a common production deployment combination for the Python Web project.
Among them:
- Gunicorn: Responsible for process management, Worker life cycle, signal handling and process-level concurrency.
- Uvicorn: Responsible for running the ASGI application and processing HTTP, asynchronous I/O and ASGI protocol communications.
- Django / FastAPI: Responsible for application functions such as routing, business logic, and database access.
- Nginx: Usually used as a reverse proxy, responsible for HTTPS, static resources, request forwarding, etc.
Typical architecture:
Client
↓
Nginx (Reverse Proxy)
↓
Gunicorn (Master Process)
│
├── Uvicorn Worker 1 ──→ Django ASGI
├── Uvicorn Worker 2 ──→ Django ASGI
├── Uvicorn Worker 3 ──→ Django ASGI
└── Uvicorn Worker 4 ──→ Django ASGI
Gunicorn manages the Worker process, while actual ASGI request processing is done by Uvicorn running in Worker.
1. WSGI and ASGI
WSGI
WSGI (Web Server Gateway Interface) is the standard interface between Python Web servers and applications.
Traditional Django usually uses WSGI:
Client
↓
Nginx
↓
Gunicorn (WSGI Worker)
↓
Django WSGI Application
Start:
gunicorn config.wsgi:application
WSGI is mainly designed around the synchronous request response model.
ASGI
ASGI (Asynchronous Server Gateway Interface) is an interface standard that supports asynchronous communication.
ASGI supports:
- HTTP request.
- Asynchronous processing.
- WebSocket protocol interface.
- Long connection scenario.
- Protocol extensions such as ASGI Lifespan.
Typical architecture:
Client
↓
Nginx
↓
Uvicorn (ASGI Server)
↓
Django / FastAPI
Start:
uvicorn config.asgi:application
WSGI vs. ASGI
| Features | WSGI | ASGI |
|---|---|---|
| Main programming models | sync | Support synchronous and asynchronous |
| HTTP | support | support |
| WebSocket | Not natively supported | Protocol layer support |
| long connection | There are certain restrictions | more suitable |
| async/await | Not natively supported | Native support |
| Django | support | support |
| FastAPI | Not usually used | Native ASGI |
Note: ASGI server supports WebSocket, which does not mean that the ASGI application that comes with Django can directly handle WebSocket.
Django projects typically require Django Channels or other ASGI routing components to implement WebSocket services.
2. Uvicorn
Uvicorn is Python ASGI Server.
Mainly responsible for:
- Receive HTTP connection.
- Parse HTTP request.
- Call ASGI Application.
- Manage asynchronous event loops.
- Handle ASGI HTTP/WebSocket protocol.
- Send a HTTP response to the client.
Working principle
HTTP Request
↓
Uvicorn
↓
ASGI Scope / Receive / Send
↓
Django ASGI Application
↓
View / Business Logic
↓
ASGI Response
↓
Uvicorn
↓
HTTP Response
Uvicorn uses an event loop to handle asynchronous tasks and can continue processing other requests while waiting for the network or other asynchronous I/O.
Start the development environment
uvicorn config.asgi:application --reload
--reload will monitor code changes and automatically restart the application, mainly used in development environments.
Start production environment
uvicorn config.asgi:application \
--host 0.0.0.0 \
--port 8000 \
--workers 2
--workers can start multiple Uvicorn Worker processes.
Note: --reload and --workers cannot be used at the same time.
Therefore, Uvicorn is not only a single-process server, but also has certain multi-process management capabilities.
3. Gunicorn
Gunicorn (Green Unicorn) is Python WSGI HTTP Server and also provides mature process management capabilities.
ASGI Server can also be managed via Gunicorn when using the compatible Worker type.
Main functions:
- Start multiple Worker processes.
- Monitor Worker status.
- Worker automatically restarts after crash.
- Graceful shutdown and restart.
- Unix signal processing.
- Request timeouts and Worker health monitoring.
- Log management.
- Controls the maximum number of Worker requests.
Master / Worker model
Gunicorn Master
│
├── Worker 1
├── Worker 2
├── Worker 3
└── Worker 4
Master is responsible for managing the process.
Worker is responsible for handling the actual request.
Different Worker types have different concurrency models, for example:
- sync: Synchronize Worker.
- gthread: Thread Worker.
- gevent: Collaborative concurrency Worker.
- UvicornWorker:ASGI Worker。
Gunicorn itself does not determine whether the application is necessarily synchronous or asynchronous, depending on the type of Worker used.
4. Gunicorn + Uvicorn Worker
For Django ASGI applications:
Gunicorn Master
│
├── Uvicorn Worker 1
├── Uvicorn Worker 2
├── Uvicorn Worker 3
└── Uvicorn Worker 4
Each Uvicorn Worker is an independent operating system process, running its own event loop and ASGI application instance.
Installation
pip install gunicorn uvicorn uvicorn-worker
start
gunicorn config.asgi:application \
-k uvicorn_worker.UvicornWorker \
--workers 4 \
--bind 0.0.0.0:8000
Parameter explanation:
| parameters | function |
|---|---|
config.asgi:application | Django ASGI application entrance |
-k | Specify Worker type |
--workers | Worker Number of processes |
--bind | Listening address and port |
Deprecation of uvicorn.workers
Old way of writing:
gunicorn config.asgi:application \
-k uvicorn.workers.UvicornWorker
uvicorn.workers has been officially deprecated by Uvicorn.
It is recommended to use the standalone uvicorn-worker package:
gunicorn config.asgi:application \
-k uvicorn_worker.UvicornWorker
This reduces dependencies on deprecated modules.
5. Synchronous and asynchronous concurrency models
Sync Worker
Traditional Gunicorn sync Worker usually handles only one request at a time.
Worker 1 → Request A
Worker 2 → Request B
Worker 3 → Request C
If Request A is performing the time-consuming synchronization of I/O, the Worker responsible for that request may not be able to handle other requests at the same time.
Asynchronous Worker
Uvicorn Using the event loop, the asynchronous task can yield execution rights while waiting for I/O.
Uvicorn Worker
│
├── Request A → await DB
├── Request B → await HTTP
└── Request C → Processing
A process can handle multiple requests concurrently under appropriate conditions.
But it needs to be distinguished:
- Concurrency (Concurrency): Multiple tasks advance alternately in time.
- Parallel (Parallelism): Multiple tasks are actually executed at the same time.
Asynchronous mainly improves the concurrency capability of I/O waiting scenarios, but does not automatically accelerate CPU intensive calculations.
CPU intensive and I/O intensive
| Type | Example | Common optimization methods |
|---|---|---|
| CPU intensive | Image processing, massive Python calculations | Multi-process, task queue, specialized computing service |
| I/O intensive | HTTP request, database waiting | Asynchronous I/O, appropriate concurrency |
| hybrid | Data processing + network calls | Optimize based on bottleneck combinations |
For example:
async def get_data():
response = await async_client.get(
"https://example.com"
)
return response.json()
If HTTP Client supports truly asynchronous I/O, the event loop can continue processing other tasks while waiting for a network response.
However:
async def get_data():
response = requests.get(
"https://example.com"
)
return response.json()
requests.get() here is a synchronous blocking call that may block the event loop.
Therefore, declaring a function as async def does not mean that the operations within it automatically become non-blocking operations.
6. Django ASGI and asynchronous support
Django supports synchronous View and asynchronous View.
Sync View
def get_data(request):
result = calculate_data()
return JsonResponse(result)
Asynchronous View
async def get_data(request):
result = await fetch_remote_data()
return JsonResponse(result)
But when Django is run through ASGI, synchronous View still needs to be run through a synchronous execution mechanism, which usually involves thread adaptation.
Django ORM
Django provides partially asynchronous ORM methods:
user = await User.objects.aget(id=1)
and:
async for user in User.objects.all():
print(user.username)
But not all features of Django ORM have full asynchronous support.
In particular, transaction operations and certain database connection management behaviors require additional attention.
Asynchronous support for DRF
The usage of standard DRF APIView and GenericAPIView is mainly designed around synchronization processing.
Don’t just include:
def get(self, request):
...
Change to:
async def get(self, request):
...
Just assume that the complete DRF request processing link already supports asynchronous.
If asynchronous API is required, compatibility should be confirmed based on the DRF version, extension library, and concrete view implementation.
For ordinary Django REST API, using synchronized View with ASGI Server is an acceptable deployment method.
But this does not mean that synchronous requests can directly obtain all the concurrency advantages of native asynchronous View.
7. Gunicorn common configuration
Typical startup command:
gunicorn config.asgi:application \
-k uvicorn_worker.UvicornWorker \
--workers 2 \
--bind 0.0.0.0:8000 \
--timeout 60 \
--graceful-timeout 30 \
--max-requests 1000 \
--max-requests-jitter 100 \
--access-logfile - \
--error-logfile -
Core parameters
| parameters | function |
|---|---|
workers | Worker number of processes |
bind | Listen on IP/port |
timeout | Worker No response timeout |
graceful_timeout | Wait time for graceful shutdown or restart |
max_requests | Worker Restart after processing a certain number of requests |
max_requests_jitter | Add random offset to restart threshold |
accesslog | HTTP access log location |
errorlog | Error log location |
loglevel | Log level |
preload_app | Preload apps in Master |
worker_tmp_dir | Worker temporary file directory |
timeout and graceful_timeout
timeout:
--timeout 60
It mainly means that Worker may be terminated and restarted if it does not respond normally to the health check of Master for more than the specified time.
For an asynchronous Worker, it is not equivalent to a single HTTP request that must complete within 60 seconds.
graceful_timeout:
--graceful-timeout 30
Indicates the waiting time allowed for Worker to complete existing work after receiving a graceful restart or shutdown command.
Worker that has not exited after the specified time may be forcibly terminated.
max_requests
--max-requests 1000
Indicates that Worker automatically restarts after processing a certain number of requests.
Mainly used to mitigate the impact of potential memory leaks.
Recommended combination:
--max-requests-jitter 100
Prevent multiple Worker from restarting at the same time at similar times.
NOTE: This is an optional defensive measure that is not required for all apps and does not replace fixing the actual memory leak.
8. Worker quantity and performance
Gunicorn can utilize multiple processes to improve the usage of multi-core CPU.
CPU Core 1 ← Worker 1
CPU Core 2 ← Worker 2
CPU Core 3 ← Worker 3
CPU Core 4 ← Worker 4
But this is just an indication. The actual process is scheduled by the operating system and will not be permanently bound to a CPU Core.
The influence of Worker quantity
Add Worker:
- More CPU resources can be utilized.
- Can improve process-level concurrency capabilities.
- Increased memory consumption.
- Possibly increase the number of database connections.
- May increase process scheduling costs.
Worker The more the better.
Actual needs based on:
CPU
RAM
Average request time
sync / Asynchronous ratio
Number of database connections
external I/O
Request concurrency
Make adjustments.
Worker connects to database
Multiple Worker have independent in-process state and database connection management mechanisms.
Worker 1 → Database Connections
Worker 2 → Database Connections
Worker 3 → Database Connections
Worker 4 → Database Connections
Need to pay attention to:
- Django does not provide a general in-process database connection pool by default.
- The creation, closing and reuse of database connections are affected by the Django configuration and the database driver used.
- Multi-process deployment may increase the pressure on the maximum number of database connections.
- Additional threads, background tasks, and database connection pools will also affect the total number of connections.
For Django asynchronous mode, special attention should be paid to the configuration of persistent connections and the use of external connection pools.
9. Nginx + Gunicorn + Uvicorn
Typical deployment architecture:
Internet
↓
Cloudflare(Optional)
↓
Nginx
│
├── HTTPS / TLS
├── Static Assets
├── Reverse Proxy
├── gzip / Brotli
└── Cache-Control
↓
Gunicorn Master
↓
Uvicorn Workers
↓
Django ASGI
↓
PostgreSQL
Nginx example
server {
listen 80;
location / {
proxy_pass http://backend: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;
proxy_http_version 1.1;
}
}
It is assumed here that backend is the service name in Docker Compose.
HTTPS can be processed by another Nginx Server Block.
If you need the WebSocket proxy, you also need to correctly configure the related Upgrade and Connection Header; the specific writing method depends on the Nginx version and deployment method.
Proxy Header and security
Both Uvicorn and Django are required to correctly identify the protocol of the original request, Host, and client IP.
But you cannot unconditionally trust what the client provides:
X-Forwarded-For
X-Forwarded-Proto
Forwarded
These Header should be set by a trusted reverse proxy and properly scoped to the trusted proxy.
Otherwise, it may cause problems such as forgery of IP or incorrect judgment of HTTPS on the client side.
10. Docker Compose deployment
Dependencies can be installed in Django Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD [
"gunicorn",
"config.asgi:application",
"-k",
"uvicorn_worker.UvicornWorker",
"--workers",
"2",
"--bind",
"0.0.0.0:8000",
"--access-logfile",
"-",
"--error-logfile",
"-"
]
Need to ensure that requirements.txt contains:
Django
gunicorn
uvicorn
uvicorn-worker
and other dependencies actually used by the project.
Compose example
services:
backend:
build: ./backend
expose:
- "8000"
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
depends_on:
- backend
Only configurations related to request forwarding between containers are shown here.
Actual projects also need to configure:
- environment variables.
- database.
- Volume。
- Nginx configuration file.
- TLS certificate.
- Health Check。
- Migration execution process.
Logs in Docker
It is recommended that access logs and error logs be written to stdout/stderr.
Gunicorn:
--access-logfile -
--error-logfile -
In this way, Docker can collect logs directly:
docker compose logs -f backend
For containerized deployments, there is usually no need for the application to maintain a complex local log file rotation mechanism.
11. Gunicorn + Uvicorn and directly Uvicorn
Gunicorn + Uvicorn
Container
↓
Gunicorn Master
↓
Multiple Uvicorn Workers
↓
Django
Advantages:
- Mature process management.
- Run multiple processes.
- Worker automatically restarts.
- Graceful restart and signal management.
- Rich Worker life cycle parameters.
Disadvantages:
- There are more process levels.
- The configuration is relatively complex.
- It is necessary to maintain Gunicorn and Uvicorn Worker related dependencies at the same time.
- You need to pay attention to the number of Worker processes and resource limitations.
Direct Uvicorn
Container
↓
Uvicorn
↓
Django
Start:
uvicorn config.asgi:application \
--host 0.0.0.0 \
--port 8000
Advantages:
- Simple structure.
- Less configuration.
- One less layer of process management abstraction.
- Suitable for process restart and horizontal expansion through container platforms.
Disadvantages:
- Some process management features are not as rich as Gunicorn.
- If you only run one Worker, you can only utilize the execution capabilities of a single process.
However, Uvicorn also supports:
uvicorn config.asgi:application --workers 4
Therefore, we cannot simply think that using Uvicorn directly can only run a single process.
12. Docker / Kubernetes deployment strategy
Single unit VPS + Docker Compose
VPS
↓
Docker Compose
↓
Gunicorn Master
↓
2–4 Uvicorn Workers
↓
Django
Suitable for managing multiple processes through one container.
For stand-alone service, Gunicorn + Uvicorn is a reasonable solution.
Kubernetes
Kubernetes can achieve horizontal expansion through multiple Pod:
Load Balancer
↓
Kubernetes Service
│
├── Pod 1 → Uvicorn
├── Pod 2 → Uvicorn
├── Pod 3 → Uvicorn
└── Pod 4 → Uvicorn
Kubernetes is mainly responsible for:
- Pod schedule.
- Container health checks.
- Recreate Pod after failure.
- load distribution.
- Horizontal expansion.
Therefore, in Kubernetes, you can consider using one Uvicorn process for each Pod, and then expand the capacity by increasing the number of Pod.
But Kubernetes does not require that each Pod can only run one Worker.
The actual choice requires comprehensive consideration:
- Pod resource limit.
- Application startup time.
- Number of database connections.
- memory usage.
- Automatic expansion strategy.
- Traffic and fault recovery needs.
13. Gunicorn FAQ
Q1: Do Gunicorn and Uvicorn have to be used together?
Not required.
You can choose:
Gunicorn + WSGI
Uvicorn + ASGI
Gunicorn + Uvicorn Worker + ASGI
Depends on framework, protocol, and deployment needs.
Q2: Is ASGI necessarily faster than WSGI?
uncertain.
ASGI may be more advantageous in high concurrent I/O waiting scenarios.
However, if the main business is synchronization Django ORM, synchronization DRF View or CPU intensive processing, switching to ASGI does not guarantee performance improvement.
It should be judged through actual load testing.
Q3: Can true multi-core parallelism be achieved using Gunicorn?
This can be achieved through multiple Worker processes.
But multiple Worker will increase memory and database connection overhead.
Q4: Should the number of Worker be set to twice the number of CPU cores?
Fixed formulas should not be applied mechanically.
The number of Worker needs to be combined with the actual request time, memory, CPU, I/O ratio and database stress test.
Q5: Do you need to use Celery?
Gunicorn and Uvicorn mainly handle Web requests.
Task queues such as Celery are used to perform background tasks that do not require blocking HTTP requests.
For example:
Client
↓
Django API
↓
Celery Queue
↓
Celery Worker
↓
Image Processing / Long-running Job
Therefore Gunicorn Worker is not Celery Worker.
For time-consuming tasks, the problem cannot be solved simply by increasing the number of Gunicorn Worker.
14. Deployment plan selection
| scene | Common choices |
|---|---|
| Traditional sync Django application | Gunicorn + WSGI |
| Django ASGI Application | Uvicorn or Gunicorn + Uvicorn |
| FastAPI | Uvicorn or Gunicorn + Uvicorn |
| Single unit VPS + Docker Compose | Gunicorn + Uvicorn is very reasonable |
| AWS ECS / Ordinary container service | Both methods are available |
| Kubernetes | Can use single process or less Worker + more Pod |
| small personal project | Direct Uvicorn is simpler |
| A large number of asynchronous I/O | ASGI + true asynchronous processing |
| WebSocket | ASGI Server + Applications that support WebSocket |
| Normal synchronization DRF API | Gunicorn + WSGI can also meet the needs |
Summary
Gunicorn and Uvicorn respectively solve problems at different levels:
| components | Core responsibilities |
|---|---|
| Nginx | Reverse proxy, HTTPS, static resources |
| Gunicorn | Worker Process management and life cycle |
| Uvicorn | ASGI protocol, HTTP processing, asynchronous event loop |
| Django / FastAPI | API, business logic, database access |
| Celery | Background asynchronous task processing |
| Docker | Container operation is isolated from the environment |
| Kubernetes | Container orchestration and scale-out |
The most critical understanding is:
Gunicorn solves the process management problem, Uvicorn solves the ASGI service running problem, and whether the application itself truly has asynchronous advantages depends on the business code and its dependencies.
For Django + DRF projects on a single VPS, you can continue to use Gunicorn + Uvicorn Worker.
On this basis, priority should be given to:
- Worker quantity and memory usage.
- Number of database connections.
- Request timeouts and logs.
- Nginx reverse proxy configuration.
- Health checks and automatic restarts.
- Is Celery required for long running tasks.