Header

Python Application Servers in 2025: From WSGI to Modern ASGI Solutions

Devops & Infrastructure, Open Source, Python, and Tips & Tricks

Post Image

The Python web server landscape has evolved significantly, particularly with the shift from WSGI to ASGI. Let's explore the various options available for serving Python web applications, from traditional to cutting-edge solutions.

Traditional WSGI Servers

Gunicorn

The tried-and-true Python WSGI server:

Pros:

  • Production-proven reliability
  • Easy configuration
  • Pre-fork worker model
  • Extensive documentation
  • Great process management
# gunicorn.conf.py
bind = "0.0.0.0:8000"
workers = 4
worker_class = "sync"
max_requests = 1000
max_requests_jitter = 50

Cons:

  • No async support
  • Limited WebSocket support
  • Traditional architecture
  • Performance limitations

uWSGI

Full-featured application server:

Pros:

  • Multiple protocol support
  • Rich feature set
  • Process management
  • Caching capabilities
  • Load balancing
# uwsgi.ini
[uwsgi]
http = :8000
processes = 4
threads = 2
master = true
vacuum = true
die-on-term = true

Cons:

  • Complex configuration
  • Steep learning curve
  • Higher memory usage

Modern ASGI Servers

Uvicorn

Lightning-fast ASGI server:

Pros:

  • High performance
  • WebSocket support
  • HTTP/1.1, HTTP/2
  • Low memory footprint
  • Simple configuration
# uvicorn configuration
import uvicorn

if __name__ == "__main__":
    uvicorn.run(
        "app:app",
        host="0.0.0.0",
        port=8000,
        workers=4,
        log_level="info",
        reload=True
    )

Cons:

  • Less production tooling
  • Newer ecosystem
  • Limited process management

Hypercorn

Modern ASGI server with HTTP/3:

Pros:

  • HTTP/3 support
  • WebSocket support
  • Multiple worker types
  • TLS configuration
  • QUIC support
# hypercorn config
from hypercorn.config import Config
from hypercorn.asyncio import serve

config = Config()
config.bind = ["0.0.0.0:8000"]
config.workers = 4

Cons:

  • Smaller community
  • Less documentation
  • Performance trade-offs

Performance-Focused Solutions

Granian

Rust-based Python web server:

Pros:

  • Extremely fast
  • Low resource usage
  • ASGI support
  • Multi-protocol
  • Native performance
# granian server
from granian import Granian

server = Granian(
    "app:app",
    workers=4,
    threads=4,
    http_port=8000
)
server.serve()

Cons:

  • New to the ecosystem
  • Limited production examples
  • Less community support

Performance Comparisons

Request Handling (req/sec)

Hello World benchmark:

Gunicorn:  10,000
uWSGI:     12,000
Uvicorn:   45,000
Hypercorn: 35,000
Granian:   50,000

Memory Usage

Base memory footprint per worker:

Gunicorn:  ~30MB
uWSGI:     ~40MB
Uvicorn:   ~20MB
Hypercorn: ~25MB
Granian:   ~15MB

Modern Features Implementation

WebSocket Support (FastAPI Example)

from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    while True:
        data = await websocket.receive_text()
        await websocket.send_text(f"Message received: {data}")

HTTP/2 Configuration

# Hypercorn HTTP/2 config
config = Config()
config.h2_enabled = True
config.alpn_protocols = ["h2", "http/1.1"]
config.certfile = "cert.pem"
config.keyfile = "key.pem"

Production Deployment

Docker Configuration

FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: python-app
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: python-app
        image: python-app:1.0
        resources:
          limits:
            memory: "512Mi"
            cpu: "500m"
        readinessProbe:
          httpGet:
            path: /health
            port: 8000

If you are considering Docker or Kubernetes you will need to have your own Shell Server in order to be able to manage this, at least, for the time being.

Performance Optimization

Worker Configuration

# Gunicorn worker optimization
import multiprocessing

# Workers = 2 * CPU cores + 1
workers = multiprocessing.cpu_count() * 2 + 1
threads = 4
worker_class = "uvicorn.workers.UvicornWorker"

Connection Pooling

# Database connection pooling
from databases import Database

database = Database('postgresql://user:pass@localhost/db')

async def startup():
    await database.connect()

async def shutdown():
    await database.disconnect()

Monitoring and Observability

Prometheus Metrics

from prometheus_client import Counter, Histogram
from fastapi import FastAPI
from prometheus_fastapi_instrumentator import Instrumentator

app = FastAPI()
Instrumentator().instrument(app).expose(app)

REQUEST_COUNT = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'status']
)

OpenTelemetry Integration

from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.trace import TracerProvider

trace.set_tracer_provider(TracerProvider())
FastAPIInstrumentor.instrument_app(app)

Making the Right Choice

Choose Gunicorn if:

  • Need proven reliability
  • Traditional WSGI apps
  • Simple deployment needs
  • Production stability priority

Choose Uvicorn if:

  • Building async applications
  • Need WebSocket support
  • Modern ASGI framework usage
  • Performance is important

Choose Modern Solutions if:

  • Maximum performance required
  • Using latest Python features
  • Need HTTP/3 support
  • Resource optimization priority

Deployment Considerations

Process Management

# Supervisor config
[program:python-app]
command=uvicorn app:app --host 0.0.0.0 --port 8000
directory=/app
user=www-data
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true

Load Balancing

# Nginx configuration
upstream python_servers {
    server 127.0.0.1:8000;
    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
}

server {
    listen 80;
    location / {
        proxy_pass http://python_servers;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Conclusion

The Python server landscape offers various options for different use cases. While Gunicorn remains a solid choice for traditional applications, modern ASGI servers like Uvicorn and Granian provide significant performance improvements and modern protocol support.

With DeployHQ, you can easily manage deployments for any of these server solutions, ensuring smooth and reliable deployments regardless of your choice.


Want to learn more about deploying Python applications? Check out our Python deployment guide or contact our support team for assistance.

#Python #WebDevelopment #Performance #DevOps #ASGI

A little bit about the author

Facundo | CTO | DeployHQ | Continuous Delivery & Software Engineering Leadership - As CTO at DeployHQ, Facundo leads the software engineering team, driving innovation in continuous delivery. Outside of work, he enjoys cycling and nature, accompanied by Bono 🐶.

Tree

Proudly powered by Katapult. Running on 100% renewable energy.