Understanding WSGI

Difficulty: Beginner

WSGI stands for Web Server Gateway Interface, defined in PEP 3333. It is the standard interface between Python web servers and web applications. Understanding WSGI gives you a deeper appreciation of how Flask works under the hood and is a common topic in Python web development interviews.

Before WSGI existed, every Python web framework had its own way of communicating with web servers. This meant that if you wrote an application for one framework, you could not easily run it on a different server. WSGI solved this by defining a universal interface. Any WSGI-compliant application can run on any WSGI-compliant server, giving you freedom to choose servers independently of frameworks.

A WSGI application is simply a callable (function or object with a __call__ method) that accepts two arguments: environ and start_response. The environ dictionary contains all the information about the incoming HTTP request: the request method, URL path, headers, query string, request body, and server information. The start_response callable is used to begin the HTTP response by specifying the status code and response headers. The application then returns an iterable of byte strings that form the response body.

Flask's app object is itself a WSGI application. When you call app.run(), Flask starts Werkzeug's development server, which calls the Flask app as a WSGI callable for each request. In production, you replace this development server with a production WSGI server like Gunicorn, uWSGI, or Waitress. These servers handle concurrent connections, process management, and other production concerns.

WSGI middleware is a powerful concept. A middleware is a component that sits between the server and the application, wrapping the WSGI app to add functionality. Middleware can log requests, handle authentication, compress responses, manage CORS, or rewrite URLs. The middleware receives the request, can modify it, passes it to the inner application, receives the response, can modify it, and passes it back. Flask extensions often use middleware under the hood.

It is worth knowing about ASGI (Asynchronous Server Gateway Interface), which is the async evolution of WSGI. While WSGI handles one request at a time per worker, ASGI supports async/await, WebSockets, and HTTP/2 server push. Frameworks like FastAPI and Starlette use ASGI. Flask 2.0+ supports async views, but it still runs on a WSGI server by default. For fully async applications, consider using Quart, which is an ASGI re-implementation of the Flask API.

The WSGI layer is also where the environ dictionary provides important request information. Keys like REQUEST_METHOD, PATH_INFO, QUERY_STRING, CONTENT_TYPE, and wsgi.input (the request body stream) are all standardized. Werkzeug parses this environ dictionary and wraps it in the friendly request object that you use in Flask views.

Code examples

A Raw WSGI Application

def application(environ, start_response):
    """The simplest possible WSGI application."""
    status = '200 OK'
    headers = [('Content-Type', 'text/plain')]
    start_response(status, headers)
    return [b'Hello from raw WSGI!']

# Run with a WSGI server:
# gunicorn myapp:application
# or with werkzeug:
if __name__ == '__main__':
    from werkzeug.serving import run_simple
    run_simple('127.0.0.1', 5000, application)

This is a WSGI application without any framework. The function takes environ (request data) and start_response (to set status/headers), then returns the response body as an iterable of bytes. Flask wraps this pattern behind a friendly API.

Flask as a WSGI App

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, World!'

# Flask's app object IS a WSGI callable.
# Under the hood, app.__call__(environ, start_response)
# is called for every request.

# This is equivalent to:
def flask_wsgi_call(environ, start_response):
    # Flask parses environ into request object
    # Matches URL to route
    # Calls view function
    # Converts return value to Response
    # Calls start_response and returns body
    pass

# In production:
# gunicorn app:app

Flask's application object implements the WSGI interface via its __call__ method. When Gunicorn or another WSGI server calls app(environ, start_response), Flask handles routing, context creation, view dispatch, and response formatting.

Simple WSGI Middleware

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, World!'

class LoggingMiddleware:
    """WSGI middleware that logs each request."""
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        method = environ['REQUEST_METHOD']
        path = environ['PATH_INFO']
        print(f'{method} {path}')
        return self.app(environ, start_response)

# Wrap the Flask app with middleware
app.wsgi_app = LoggingMiddleware(app.wsgi_app)

if __name__ == '__main__':
    app.run(debug=True)

Middleware wraps the WSGI app to add cross-cutting concerns. Flask exposes app.wsgi_app for middleware wrapping. Note we wrap app.wsgi_app (the WSGI callable), not app itself, so Flask's routing and context management still work correctly.

The Environ Dictionary

from flask import Flask, request

app = Flask(__name__)

@app.route('/debug')
def debug_environ():
    # Flask's request object wraps the WSGI environ
    info = {
        'method': request.method,          # environ['REQUEST_METHOD']
        'path': request.path,              # environ['PATH_INFO']
        'query': request.query_string.decode(),  # environ['QUERY_STRING']
        'host': request.host,              # environ['HTTP_HOST']
        'user_agent': request.user_agent.string,
        'remote_addr': request.remote_addr,
    }
    return info

Flask's request object is a friendly wrapper around the raw WSGI environ dictionary. It provides parsed and typed access to request data that would otherwise require manual parsing from the environ.

Key points

Concepts covered

WSGI, PEP 3333, Application Callable, Middleware, Werkzeug, ASGI