← ProgrammingBack to top

Gunicorn + Uvicorn: Python Web service deployment

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

FeaturesWSGIASGI
Main programming modelssyncSupport synchronous and asynchronous
HTTPsupportsupport
WebSocketNot natively supportedProtocol layer support
long connectionThere are certain restrictionsmore suitable
async/awaitNot natively supportedNative support
Djangosupportsupport
FastAPINot usually usedNative 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:

parametersfunction
config.asgi:applicationDjango ASGI application entrance
-kSpecify Worker type
--workersWorker Number of processes
--bindListening 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

TypeExampleCommon optimization methods
CPU intensiveImage processing, massive Python calculationsMulti-process, task queue, specialized computing service
I/O intensiveHTTP request, database waitingAsynchronous I/O, appropriate concurrency
hybridData processing + network callsOptimize 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

parametersfunction
workersWorker number of processes
bindListen on IP/port
timeoutWorker No response timeout
graceful_timeoutWait time for graceful shutdown or restart
max_requestsWorker Restart after processing a certain number of requests
max_requests_jitterAdd random offset to restart threshold
accesslogHTTP access log location
errorlogError log location
loglevelLog level
preload_appPreload apps in Master
worker_tmp_dirWorker 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

sceneCommon choices
Traditional sync Django applicationGunicorn + WSGI
Django ASGI ApplicationUvicorn or Gunicorn + Uvicorn
FastAPIUvicorn or Gunicorn + Uvicorn
Single unit VPS + Docker ComposeGunicorn + Uvicorn is very reasonable
AWS ECS / Ordinary container serviceBoth methods are available
KubernetesCan use single process or less Worker + more Pod
small personal projectDirect Uvicorn is simpler
A large number of asynchronous I/OASGI + true asynchronous processing
WebSocketASGI Server + Applications that support WebSocket
Normal synchronization DRF APIGunicorn + WSGI can also meet the needs

Summary

Gunicorn and Uvicorn respectively solve problems at different levels:

componentsCore responsibilities
NginxReverse proxy, HTTPS, static resources
GunicornWorker Process management and life cycle
UvicornASGI protocol, HTTP processing, asynchronous event loop
Django / FastAPIAPI, business logic, database access
CeleryBackground asynchronous task processing
DockerContainer operation is isolated from the environment
KubernetesContainer 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.

Reference documentation