WSGI (Web Server Gateway Interface) is the standard, synchronous interface between Python web servers and Python web applications or frameworks. Defined in PEP 3333, it lets any WSGI-compliant server (Gunicorn, uWSGI, mod_wsgi) talk to any WSGI-compliant framework (Django, Flask, Pyramid) without either side knowing the other's internals. You write your app once and run it on any compatible server.
WSGI has been the backbone of Python web deployment since 2003. But in 2026 it tells only half the story: modern async frameworks like FastAPI and async-capable Django speak ASGI instead, the newer interface built for async I/O, WebSockets, and long-lived connections. This guide explains WSGI from first principles with runnable code, then contrasts it with ASGI so you know which to reach for.
What problem does WSGI solve?
Before WSGI, every Python web framework shipped its own way of plugging into a web server. A Zope app could not easily run under a server written for a different framework, and server authors had to support each framework individually. The result was an N x M integration mess: every framework had to be glued to every server.
WSGI fixes this by defining one contract in the middle:
- Servers (the "WSGI server" / gateway) handle raw HTTP, networking, and process management, then call your app in a standard way.
- Applications / frameworks receive a normalised request and return a response, without caring what server is running them.
Because both sides agree on the same calling convention, you can swap Gunicorn for uWSGI, or move a Flask app behind Apache's mod_wsgi, with little more than configuration changes. This server-to-framework decoupling is the entire point of WSGI.
The WSGI spec in one callable
At its core, a WSGI application is just a callable that accepts two arguments and returns an iterable of bytes:
application(environ, start_response) -> iterable of bytes
The two arguments are:
environ— a plain dict holding the request. It merges CGI-style variables (REQUEST_METHOD,PATH_INFO,QUERY_STRING,CONTENT_TYPE, HTTP headers asHTTP_*) with WSGI-specific keys (wsgi.inputfor the request body stream,wsgi.url_scheme, etc.).start_response— a callable you invoke once to send the HTTP status line and response headers, e.g.start_response('200 OK', [('Content-Type', 'text/plain')]).
Your callable then returns an iterable of byte strings (the response body). HTTP moves bytes, so you return bytes, never str. That single, small contract is all WSGI is.
A minimal WSGI app from scratch
You don't need any framework to write a WSGI app — the standard library's wsgiref ships a reference server perfect for learning. Save this as helloworld_wsgi.py (works on Python 3.12+):
# helloworld_wsgi.py
from wsgiref.simple_server import make_server
def application(environ, start_response):
"""A minimal WSGI app: greets using the request path."""
path = environ.get("PATH_INFO", "/")
method = environ.get("REQUEST_METHOD", "GET")
body = f"Hello, WSGI! You sent {method} {path}".encode("utf-8")
status = "200 OK"
headers = [
("Content-Type", "text/plain; charset=utf-8"),
("Content-Length", str(len(body))),
]
start_response(status, headers)
return [body] # an iterable of byte strings
if __name__ == "__main__":
with make_server("", 8000, application) as server:
print("Serving on http://localhost:8000 ...")
server.serve_forever()Run it and open the URL in a browser:
python helloworld_wsgi.py
# then visit http://localhost:8000/anythingThat is a complete, spec-compliant WSGI application. wsgiref.simple_server is single-threaded and meant for development and testing only — never production. In production you swap make_server for a real WSGI server (next section), but your application callable stays exactly the same. That portability is WSGI's whole promise.
How Django and Flask expose application
You almost never hand-write the environ/start_response plumbing — frameworks do it for you and expose a ready-made WSGI callable, conventionally named application (or app).
Django generates a wsgi.py in every project. A WSGI server points at the application object inside it:
# myproject/wsgi.py (generated by django-admin)
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
application = get_wsgi_application()
Flask apps are themselves WSGI callables, so the Flask instance you create is the application:
# app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Hello from Flask over WSGI"
In both cases the server just needs the import path to that object — for example myproject.wsgi:application or app:app. The framework handles routing, request parsing, and building the byte response; WSGI is only the handshake at the edge. For deeper Django and Python work, see our Django development services and Python development services.
WSGI servers: Gunicorn, uWSGI, mod_wsgi
A WSGI server is the process that actually listens on a socket, parses HTTP, and calls your application. The common production options:
- Gunicorn — "Green Unicorn", the most popular choice. Pure-Python, simple to configure, pre-fork worker model. The default for most Django and Flask deployments.
- uWSGI — extremely feature-rich and fast (C-based), with knobs for almost everything. Powerful but heavier to configure; often paired with Nginx.
- mod_wsgi — an Apache module that embeds Python directly in the Apache web server. A good fit when you're already standardised on Apache.
Running a Django app under Gunicorn is a one-liner — point it at the module path of your application object:
# install
pip install gunicorn
# run: module_path:callable_name
gunicorn myproject.wsgi:application --workers 4 --bind 0.0.0.0:8000
# Flask example (app object named "app" in app.py)
gunicorn app:app --workers 4 --bind 0.0.0.0:8000A common rule of thumb for CPU-bound sync workloads is (2 x CPU cores) + 1 workers, then tune from real traffic. We walk through a full production setup in Django hosting on Nginx with uWSGI for high performance.
WSGI vs ASGI: the sync/async divide
WSGI's one limitation is baked into its design: it is synchronous. Each worker handles one request at a time, blocking until that request finishes. That is fine for typical request/response web apps, but it cannot natively handle WebSockets, server-sent events, long-polling, or thousands of concurrent slow connections without burning a worker on each one.
ASGI (Asynchronous Server Gateway Interface) is the spiritual successor, created to support async/await and long-lived connections. Instead of a single blocking call, an ASGI app is an async callable taking three arguments — scope (connection metadata), receive (await incoming events), and send (push events out) — which lets one worker juggle many concurrent connections via the event loop.
ASGI is broader in scope than WSGI: it handles HTTP and WebSockets, and frameworks like Django can run their existing sync views under ASGI too. It's the standard for FastAPI, Starlette, and async Django.
When each one matters
- Reach for WSGI when your app is classic request/response (server-rendered pages, CRUD APIs, admin dashboards) and your stack is sync Django, Flask, or Pyramid. It's mature, simple, and battle-tested.
- Reach for ASGI when you need real-time features (chat, live dashboards, notifications), high-concurrency I/O-bound work (lots of waiting on databases or external APIs), or you're building on FastAPI / async Django.
async defviews only pay off under ASGI.
You can also run a sync Django app under ASGI today and adopt async views incrementally — you are not forced to choose forever on day one.
WSGI vs ASGI at a glance
| Aspect | WSGI | ASGI |
|---|---|---|
| Spec | PEP 3333 | ASGI specification (asgi.readthedocs.io) |
| Model | Synchronous, one request per worker | Asynchronous, many concurrent connections per worker |
| Concurrency | Processes / threads (pre-fork workers) | asyncio event loop (+ worker processes) |
| Callable | application(environ, start_response) |
async application(scope, receive, send) |
| Frameworks | Django (sync), Flask, Pyramid, Bottle | FastAPI, Starlette, async Django, Sanic |
| Servers | Gunicorn, uWSGI, mod_wsgi | Uvicorn, Hypercorn, Daphne |
| WebSockets / SSE | Not supported natively | First-class support |
| Best for | Classic request/response apps, CRUD, admin | Real-time, streaming, high-concurrency I/O-bound APIs |
ASGI servers and a minimal ASGI app
Just as WSGI has Gunicorn and uWSGI, ASGI has its own servers:
- Uvicorn — lightning-fast, built on
uvloopandhttptools; the default for FastAPI. Often run as Gunicorn workers in production viauvicorn.workers.UvicornWorker. - Hypercorn — supports HTTP/2 and HTTP/3 in addition to HTTP/1.1 and WebSockets.
- Daphne — the reference ASGI server from the Django Channels project, great for WebSocket-heavy Django apps.
Here's the ASGI equivalent of our hello-world, written against the raw spec so you can see the three-argument async contract (Python 3.12+):
# helloworld_asgi.py
async def application(scope, receive, send):
"""Minimal raw ASGI app. Handles a single HTTP request/response."""
assert scope["type"] == "http"
body = b"Hello, ASGI! (async)"
await send({
"type": "http.response.start",
"status": 200,
"headers": [
(b"content-type", b"text/plain; charset=utf-8"),
(b"content-length", str(len(body)).encode()),
],
})
await send({
"type": "http.response.body",
"body": body,
})
# Run with an ASGI server:
# pip install uvicorn
# uvicorn helloworld_asgi:application --port 8000Compare this to the WSGI version: same idea, but the async scope/receive/send model is what unlocks WebSockets and concurrency. For production FastAPI you typically run Uvicorn workers under Gunicorn:
# FastAPI / async app in production (Uvicorn workers managed by Gunicorn)
pip install "uvicorn[standard]" gunicorn
gunicorn main:app \
--worker-class uvicorn.workers.UvicornWorker \
--workers 4 \
--bind 0.0.0.0:8000Middleware: layering logic around your app
Both WSGI and ASGI support middleware — wrappers that sit between the server and your application to add cross-cutting behaviour (logging, auth, compression, CORS, error handling) without touching app code.
A WSGI middleware is simply a callable that takes the app, intercepts the environ/start_response cycle, and returns a new WSGI-compatible callable:
class TimingMiddleware:
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
import time
start = time.perf_counter()
response = self.app(environ, start_response)
# ... measure / log elapsed time after the response iterates ...
return response
Frameworks layer their own middleware stacks on top of this idea — Django's MIDDLEWARE setting and Flask's before_request hooks both build on the same wrap-the-app pattern. For Django's request/response pipeline specifically, see understanding middleware functionality in Django.
Production deployment notes
A typical Python web deployment puts a WSGI (or ASGI) server behind a reverse proxy:
- App server — Gunicorn/uWSGI (WSGI) or Uvicorn/Hypercorn (ASGI) runs your application across multiple worker processes.
- Reverse proxy — Nginx (or a load balancer) sits in front to terminate TLS, serve static files, buffer slow clients, and load-balance across workers. App servers like Gunicorn are deliberately not hardened to face the raw internet alone.
- Process supervision — systemd, Supervisor, or a container orchestrator keeps workers alive and restarts them on failure.
Practical tips: size sync workers from your CPU count ((2 x cores) + 1 is a starting point) and load-test before locking it in; use --worker-class uvicorn.workers.UvicornWorker to serve async apps under Gunicorn; set sensible timeouts; and run several workers so a single slow request can't stall the whole site.
Over 12+ years and 50+ delivered projects, MicroPyramid has shipped and operated Python apps across exactly these stacks — Gunicorn/uWSGI with Nginx for Django, and Uvicorn-based deployments for FastAPI and async services.
Frequently Asked Questions
What is WSGI in simple terms?
WSGI (Web Server Gateway Interface) is a standard, defined in PEP 3333, that describes how a Python web server and a Python web application talk to each other. It's a simple contract: the server calls your application as application(environ, start_response), your app reads the request from the environ dict, calls start_response with a status and headers, and returns the body as bytes. Because both sides follow the same rule, any WSGI server can run any WSGI app.
WSGI vs ASGI — what's the difference?
WSGI is synchronous: each worker handles one request at a time and blocks until it finishes, which is fine for classic request/response apps but cannot natively do WebSockets or high-concurrency long-lived connections. ASGI is the asynchronous successor built for async/await; its app is async application(scope, receive, send), letting one worker juggle many concurrent connections and support WebSockets and streaming. Use WSGI for sync Django and Flask, ASGI for FastAPI, async Django, and real-time features.
Do I need WSGI for FastAPI?
No. FastAPI is an ASGI framework, so you run it with an ASGI server such as Uvicorn (often as Uvicorn workers managed by Gunicorn), not a plain WSGI server like Gunicorn's default worker or mod_wsgi. FastAPI's async def endpoints rely on the ASGI event loop, which WSGI doesn't provide. Reach for WSGI only with sync frameworks like classic Django or Flask.
What WSGI server should I use in production?
Gunicorn is the most common and easiest choice for Django and Flask, with a simple pre-fork worker model. uWSGI is more feature-rich and very fast but heavier to configure, and mod_wsgi suits teams already on Apache. Whichever you pick, run it behind Nginx (or a load balancer) for TLS termination, static files, and buffering — app servers shouldn't face the internet directly. For async apps, use Uvicorn (often under Gunicorn) instead.
Can Django run on ASGI?
Yes. Modern Django supports both WSGI and ASGI — django-admin generates both wsgi.py and asgi.py. Run Django under an ASGI server (Uvicorn, Daphne, Hypercorn) when you want async def views, WebSockets via Channels, or better concurrency for I/O-bound work. Your existing sync views keep working under ASGI, so you can adopt async incrementally rather than rewriting everything at once.
Is WSGI still used in 2026?
Yes, widely. WSGI remains the standard for the huge ecosystem of synchronous Python apps — most Django and Flask deployments still run on Gunicorn or uWSGI over WSGI, and it's mature, stable, and well understood. ASGI is growing fast and is the right choice for new async, real-time, and high-concurrency projects, but it complements rather than replaces WSGI. In practice, both interfaces are first-class parts of the Python web stack today.