keyboard-shortcut
d

WSGI and ASGI

9min read

an image

WSGI and ASGI

If you have deployed a Python web application, you have probably seen names like Gunicorn, uWSGI, Daphne, Uvicorn, WSGI, and ASGI.

At first this can feel like a pile of random web server jargon. But the core idea is actually quite small:

WSGI and ASGI are specifications that describe how a Python web server talks to a Python web application.

The server calls your application in a standard way, and your application returns a response in a standard way. That shared contract is what lets one server run many different Python frameworks.

What WSGI Is

WSGI stands for Web Server Gateway Interface.

It is defined in PEP 3333 and describes a standard way for a web server to call a Python web application.

A WSGI application is just a callable object. In its simplest form, it looks like this:

def application(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [b"Hello from WSGI"]

That function receives:

  • environ: a dictionary containing request information, environment variables, HTTP headers, path information, and server details.
  • start_response: a function the application calls to begin the HTTP response.

The application returns an iterable of bytes, which becomes the response body.

That is the basic shape of WSGI.

What Happens When a Request Hits a WSGI App

In production, a Python web app usually sits behind a real HTTP server or reverse proxy such as Nginx, Apache, or a cloud load balancer:

Browser -> Reverse Proxy -> Gunicorn / uWSGI / mod_wsgi -> Django WSGI application -> Django view

Your load balancer or reverse proxy handles the public HTTP connection, then forwards dynamic requests to a Python application server. Gunicorn, uWSGI, or another WSGI server then:

  1. Build the environ dictionary from the HTTP request.
  2. Call the Python application using the WSGI interface.
  3. Let the application produce a status, headers, and body.
  4. Send that response back to the client.

Django as a WSGI App

Django projects include a file called wsgi.py.

It usually looks something like this:

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")

application = get_wsgi_application()

That application object is the WSGI callable.

When you run Django behind Gunicorn, you commonly point Gunicorn at that object:

gunicorn myproject.wsgi:application

That command means:

Import myproject.wsgi, find the object called application, and use it as the WSGI app.

Once Gunicorn has loaded the app, it assigns each incoming request to a worker process or thread, creates the WSGI environ, calls Django's application, and sends Django's response back to the client.

The important point is that Django is not directly accepting raw internet traffic in this setup. It is being called by a WSGI server, using the WSGI contract.

WSGI Is Synchronous

WSGI was designed around a synchronous request/response model:

One worker handles one request until that request is finished.

If a Django view performs a slow database query, calls an external API, or waits for a file to upload, that worker is occupied while it waits.

This model is straightforward and reliable. It is still a good fit for traditional CRUD apps where most work is short-lived and request/response based.

But WSGI was not designed for long-lived connections or asynchronous communication: WebSockets, server-sent events, long polling, chat apps, live dashboards, or high-concurrency IO-heavy services. For those, Python needed a different interface.

What ASGI Is

ASGI stands for Asynchronous Server Gateway Interface.

It is the spiritual successor to WSGI, designed to support both traditional HTTP and long-lived asynchronous protocols like WebSockets.

An ASGI application is also a callable, but it has a different shape:

async def application(scope, receive, send):
    ...

It receives three things:

  • scope: information about the connection, such as the protocol, path, headers, and client.
  • receive: an async function the app calls to receive events.
  • send: an async function the app calls to send events.

WSGI mostly thinks in terms of one HTTP request and one HTTP response. ASGI thinks in terms of a connection and a stream of events.

That model works for normal HTTP, but it also works for long-lived WebSocket conversations:

Client connects
  -> ASGI server sends websocket.connect event
  -> App accepts the connection
  -> Client and app exchange websocket.receive and websocket.send events
  -> Connection eventually closes

Django as an ASGI App

Modern Django projects also include an asgi.py file.

It usually looks like this:

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")

application = get_asgi_application()

That application object is the ASGI callable.

You can run it with an ASGI server such as Uvicorn or Daphne:

uvicorn myproject.asgi:application

or:

daphne myproject.asgi:application

The request path now looks like the WSGI version, but with an ASGI server:

Browser
  -> Nginx
  -> Uvicorn / Daphne / Hypercorn
  -> Django ASGI application
  -> Django view or async view

For a normal HTTP request, this may look very similar to WSGI from the outside. The difference is that ASGI can also handle asynchronous events and long-lived connections.

ASGI Does Not Automatically Make Everything Faster

It is tempting to hear "async" and assume ASGI is automatically faster than WSGI. That is not quite right.

ASGI can handle certain kinds of concurrency more efficiently, especially when an app spends a lot of time waiting on network IO. But if your code is synchronous, CPU-heavy, or blocked on synchronous database calls, putting it behind an ASGI server does not magically remove those bottlenecks.

For example, this still blocks while requests.get() waits for the remote server:

def slow_view(request):
    result = requests.get("https://example.com/api")
    return JsonResponse(result.json())

To benefit from async, the code in the path also needs to be async-friendly:

async def async_view(request):
    async with httpx.AsyncClient() as client:
        result = await client.get("https://example.com/api")
    return JsonResponse(result.json())

Your framework, middleware, database driver, HTTP client, and deployment setup all need to fit the async model.

WSGI vs ASGI

The simplest comparison is:

Feature WSGI ASGI
Sync/Async Synchronous callable Asynchronous callable
Main protocol HTTP HTTP, WebSockets, server-sent events, and lifespan events
Request model One request, one response One connection can produce many receive/send events over time
Good for Traditional web apps Async apps and long-lived connections
Common servers Gunicorn, uWSGI, mod_wsgi Uvicorn, Daphne, Hypercorn
Django entrypoint wsgi.py asgi.py

WSGI is still a solid default for many Django applications. ASGI is the better fit when you need async views, WebSockets, or many concurrent connections that spend most of their time waiting on IO.

How to Think About It

If you are building a standard Django app with pages, forms, admin screens, and JSON endpoints, WSGI is still completely reasonable.

If you need WebSockets, live updates, async views, or a framework like FastAPI or Starlette, ASGI is usually the right interface.

The important thing is to separate the layers:

  • Django, Flask, FastAPI: your web framework.
  • WSGI or ASGI: the interface your app exposes.
  • Gunicorn, uWSGI, Uvicorn, Daphne: the server that calls your app.
  • Nginx, Apache, load balancers: the outer HTTP infrastructure.

Once you see those layers clearly, the terminology becomes much less mysterious. WSGI is the classic Python web contract. ASGI is the newer async-capable contract. Both exist so your Python application and your Python server can agree on how to talk to each other.