uvicorn Documentation

An ASGI web server, for Python. πŸ¦„

10,801 stars Python Python 1 file Β· ~14,337 tokens #asgi#asyncio#http#http-server#python
RAW Doc

File: README.md






Documentation: https://uvicorn.dev

Source Code: https://www.github.com/Kludex/uvicorn


Uvicorn is an ASGI web server implementation for Python.

Until recently Python has lacked a minimal low-level server/application interface for
async frameworks. The ASGI specification fills this gap, and means we're now able to
start building a common set of tooling usable across all async frameworks.

Uvicorn supports HTTP/1.1 and WebSockets.

Quickstart

Install using pip:

shell
$ pip install uvicorn

This will install uvicorn with minimal (pure Python) dependencies.

shell
$ pip install 'uvicorn[standard]'

This will install uvicorn with "Cython-based" dependencies (where possible) and other "optional extras".

In this context, "Cython-based" means the following:

  • the event loop uvloop will be installed and used if possible.
  • the http protocol will be handled by httptools if possible.

Moreover, "optional extras" means that:

  • the websocket protocol will be handled by websockets (should you want to use wsproto you'd need to install it manually) if possible.
  • the --reload flag in development mode will use watchfiles.
  • python-dotenv will be installed should you want to use the --env-file option.
  • PyYAML will be installed to allow you to provide a .yaml file to --log-config, if desired.

Create an application, in example.py:

python
async def app(scope, receive, send):
    assert scope['type'] == 'http'

    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            (b'content-type', b'text/plain'),
        ],
    })
    await send({
        'type': 'http.response.body',
        'body': b'Hello, world!',
    })

Run the server:

shell
$ uvicorn example:app

Why ASGI?

Most well established Python Web frameworks started out as WSGI-based frameworks.

WSGI applications are a single, synchronous callable that takes a request and returns a response.
This doesn’t allow for long-lived connections, like you get with long-poll HTTP or WebSocket connections,
which WSGI doesn't support well.

Having an async concurrency model also allows for options such as lightweight background tasks,
and can be less of a limiting factor for endpoints that have long periods being blocked on network
I/O such as dealing with slow HTTP requests.


Alternative ASGI servers

A strength of the ASGI protocol is that it decouples the server implementation
from the application framework. This allows for an ecosystem of interoperating
webservers and application frameworks.

Daphne

The first ASGI server implementation, originally developed to power Django Channels, is the Daphne webserver.

It is run widely in production, and supports HTTP/1.1, HTTP/2, and WebSockets.

Any of the example applications given here can equally well be run using daphne instead.

text
$ pip install daphne
$ daphne app:App

Hypercorn

Hypercorn was initially part of the Quart web framework, before
being separated out into a standalone ASGI server.

Hypercorn supports HTTP/1.1, HTTP/2, and WebSockets.

It also supports the excellent trio async framework, as an alternative to asyncio.

text
$ pip install hypercorn
$ hypercorn app:App

Mangum

Mangum is an adapter for using ASGI applications with AWS Lambda & API Gateway.

Granian

Granian is an ASGI compatible Rust HTTP server which supports HTTP/2, TLS and WebSockets.



File: docs/concepts/asgi.md

ASGI

Uvicorn uses the ASGI specification for interacting with an application.

The application should expose an async callable which takes three arguments:

  • scope - A dictionary containing information about the incoming connection.
  • receive - A channel on which to receive incoming messages from the server.
  • send - A channel on which to send outgoing messages to the server.

Two common patterns you might use are either function-based applications:

python
async def app(scope, receive, send):
    assert scope['type'] == 'http'
    ...

Or instance-based applications:

python
class App:
    async def __call__(self, scope, receive, send):
        assert scope['type'] == 'http'
        ...

app = App()

It's good practice for applications to raise an exception on scope types
that they do not handle.

The content of the scope argument, and the messages expected by receive and send depend on the protocol being used.

The format for HTTP messages is described in the ASGI HTTP Message format.

HTTP Scope

An incoming HTTP request might have a connection scope like this:

python
{
    'type': 'http',
    'scheme': 'http',
    'root_path': '',
    'server': ('127.0.0.1', 8000),
    'http_version': '1.1',
    'method': 'GET',
    'path': '/',
    'headers': [
        (b'host', b'127.0.0.1:8000'),
        (b'user-agent', b'curl/7.51.0'),
        (b'accept', b'*/*')
    ]
}

HTTP Messages

The instance coroutine communicates back to the server by sending messages to the send coroutine.

python
await send({
    'type': 'http.response.start',
    'status': 200,
    'headers': [
        [b'content-type', b'text/plain'],
    ]
})
await send({
    'type': 'http.response.body',
    'body': b'Hello, world!',
})

Requests & responses

Here's an example that displays the method and path used in the incoming request:

python
async def app(scope, receive, send):
    """
    Echo the method and path back in an HTTP response.
    """
    assert scope['type'] == 'http'

    body = f'Received {scope["method"]} request to {scope["path"]}'
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            [b'content-type', b'text/plain'],
        ]
    })
    await send({
        'type': 'http.response.body',
        'body': body.encode('utf-8'),
    })

Reading the request body

You can stream the request body without blocking the asyncio task pool,
by fetching messages from the receive coroutine.

python
async def read_body(receive):
    """
    Read and return the entire body from an incoming ASGI message.
    """
    body = b''
    more_body = True

    while more_body:
        message = await receive()
        body += message.get('body', b'')
        more_body = message.get('more_body', False)

    return body


async def app(scope, receive, send):
    """
    Echo the request body back in an HTTP response.
    """
    body = await read_body(receive)
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            (b'content-type', b'text/plain'),
            (b'content-length', str(len(body)).encode())
        ]
    })
    await send({
        'type': 'http.response.body',
        'body': body,
    })

Streaming responses

You can stream responses by sending multiple http.response.body messages to
the send coroutine.

python
import asyncio


async def app(scope, receive, send):
    """
    Send a slowly streaming HTTP response back to the client.
    """
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            [b'content-type', b'text/plain'],
        ]
    })
    for chunk in [b'Hello', b', ', b'world!']:
        await send({
            'type': 'http.response.body',
            'body': chunk,
            'more_body': True
        })
        await asyncio.sleep(1)
    await send({
        'type': 'http.response.body',
        'body': b'',
    })

Why ASGI?

Most well established Python Web frameworks started out as WSGI-based frameworks.

WSGI applications are a single, synchronous callable that takes a request and returns a response.
This doesn’t allow for long-lived connections, like you get with long-poll HTTP or WebSocket connections,
which WSGI doesn't support well.

Having an async concurrency model also allows for options such as lightweight background tasks,
and can be less of a limiting factor for endpoints that have long periods being blocked on network
I/O such as dealing with slow HTTP requests.


Alternative ASGI servers

A strength of the ASGI protocol is that it decouples the server implementation
from the application framework. This allows for an ecosystem of interoperating
webservers and application frameworks.

Daphne

The first ASGI server implementation, originally developed to power Django Channels, is
the Daphne webserver.

It is run widely in production, and supports HTTP/1.1, HTTP/2, and WebSockets.

Any of the example applications given here can equally well be run using daphne instead.

shell
pip install daphne
daphne app:App

Hypercorn

Hypercorn was initially part of the Quart web framework,
before being separated out into a standalone ASGI server.

Hypercorn supports HTTP/1.1, HTTP/2, HTTP/3 and WebSockets.

shell
pip install hypercorn
hypercorn app:App

ASGI frameworks

You can use Uvicorn, Daphne, or Hypercorn to run any ASGI framework.

For small services you can also write ASGI applications directly.

Starlette

Starlette is a lightweight ASGI framework/toolkit.

It is ideal for building high performance asyncio services, and supports both HTTP and WebSockets.

Django Channels

The ASGI specification was originally designed for use with Django Channels.

Channels is a little different to other ASGI frameworks in that it provides
an asynchronous frontend onto a threaded-framework backend. It allows Django
to support WebSockets, background tasks, and long-running connections,
with application code still running in a standard threaded context.

Quart

Quart is a Flask-like ASGI web framework.

FastAPI

FastAPI is an API framework based on Starlette and Pydantic, heavily inspired by previous server versions of APIStar.

You write your API function parameters with Python 3.6+ type declarations and get automatic data conversion, data validation, OpenAPI schemas (with JSON Schemas) and interactive API documentation UIs.

BlackSheep

BlackSheep is a web framework based on ASGI, inspired by Flask and ASP.NET Core.

Its most distinctive features are built-in support for dependency injection, automatic binding of parameters by request handler's type annotations, and automatic generation of OpenAPI documentation and Swagger UI.

Falcon

Falcon is a minimalist REST and app backend framework for Python, with a focus on reliability, correctness, and performance at scale.

Muffin

Muffin is a fast, lightweight and asynchronous ASGI web-framework for Python 3.

Litestar

Litestar is a powerful, lightweight and flexible ASGI framework.

It includes everything that's needed to build modern APIs - from data serialization and validation to websockets, ORM integration, session management, authentication and more.

Panther

Panther is a fast & friendly web framework for building async APIs with Python 3.10+.

It has built-in Document-oriented Database, Caching System, Authentication and Permission Classes, Visual API Monitoring and also supports Websocket, Throttling, Middlewares.


File: docs/concepts/event-loop.md

Event Loop

Uvicorn provides two event loop implementations that you can choose from using the --loop option:

bash
uvicorn main:app --loop <auto|asyncio|uvloop>

By default, Uvicorn uses --loop auto, which automatically selects:

  1. uvloop - If uvloop is installed, Uvicorn will use it for maximum performance
  2. asyncio - If uvloop is not available, Uvicorn falls back to Python's built-in asyncio event loop

Since uvloop is not compatible with Windows or PyPy, it is not available on these platforms.

On Windows, the asyncio implementation uses the standard [ProactorEventLoop][asyncio.ProactorEventLoop] in single-process mode.
When running with --reload or multiple workers, it uses [SelectorEventLoop][asyncio.SelectorEventLoop] instead.

??? info "Why can ProactorEventLoop fail with multiple processes on Windows?"
If you want to know more about it, you can read the issue #cpython/122240.

Custom Event Loop

You can use custom event loop implementations by specifying a module path and function name using the colon notation:

bash
uvicorn main:app --loop <module>:<function>

The function should return a callable that creates a new event loop instance.

rloop

rloop is an experimental AsyncIO event loop implemented in Rust on top of the mio crate. It aims to provide high performance through Rust's systems programming capabilities.

You can install it with:

=== "pip"
bash pip install rloop
=== "uv"
bash uv add rloop

You can run uvicorn with rloop with the following command:

bash
uvicorn main:app --loop rloop:new_event_loop

!!! warning "Experimental"
rloop is currently experimental and not suited for production usage. It is only available on Unix systems.

Winloop

Winloop is an alternative library that brings uvloop-like performance to Windows. Since uvloop is based on libuv and doesn't support Windows, Winloop provides a Windows-compatible implementation with significant performance improvements over the standard Windows event loop policies.

You can install it with:

=== "pip"
bash pip install winloop
=== "uv"
bash uv add winloop

You can run uvicorn with Winloop with the following command:

bash
uvicorn main:app --loop winloop:new_event_loop

File: docs/concepts/http-protocols.md

HTTP Protocols

Uvicorn provides three HTTP/1.1 protocol implementations that you can choose from using the --http option:

bash
uvicorn main:app --http <auto|h11|httptools|zttp>

By default, Uvicorn uses --http auto, which automatically selects:

  1. httptools - If httptools is installed, Uvicorn will use it for maximum performance
  2. h11 - If httptools is not available, Uvicorn falls back to h11

h11

h11 is a pure Python HTTP/1.1 implementation. It is a required dependency of Uvicorn, so it is always available, and it is the only implementation compatible with PyPy.

httptools

httptools is a Python binding for the Node.js HTTP parser. It is installed as part of the uvicorn[standard] optional extras, and provides greater performance than h11, but is not compatible with PyPy.

zttp

zttp is a sans-IO HTTP parser for Python with a core written in Zig. Prebuilt wheels are available for CPython on Linux, macOS and Windows.

You can install it with:

=== "pip"
bash pip install zttp
=== "uv"
bash uv add zttp

You can run uvicorn with zttp with the following command:

bash
uvicorn main:app --http zttp

!!! warning "Experimental"
zttp support is currently experimental and not suited for production usage. If you try it out, please report any issues or feedback on the issue tracker.


File: docs/concepts/lifespan.md

Since Uvicorn is an ASGI server, it supports the
ASGI lifespan protocol.
This allows you to run startup and shutdown events for your application.

The lifespan protocol is useful for initializing resources that need to be available throughout
the lifetime of the application, such as database connections, caches, or other services.

Keep in mind that the lifespan is executed only once per application instance. If you have
multiple workers, each worker will execute the lifespan independently.

Lifespan Architecture

The lifespan protocol runs as a sibling task alongside your main application, allowing both to execute concurrently.

Let's see how Uvicorn handles the lifespan and main application tasks:

mermaid
sequenceDiagram
    participant Server as Uvicorn Server
    participant LifespanTask as Lifespan Task
    participant AppTask as Application Task
    participant UserApp as User Application

    Note over Server: βœ… Server starts

    Server->>+LifespanTask: spawn_task(lifespan_handler)

    LifespanTask->>UserApp: {"type": "lifespan.startup"}

    Note over UserApp: Initialize databases, caches, etc.

    UserApp-->>LifespanTask: {"type": "lifespan.startup.complete"}
    LifespanTask->>Server: βœ… Startup complete

    Server->>+AppTask: spawn_task(application_handler)
    Note over AppTask: βœ… Ready for requests

    rect rgb(240, 248, 255)
        Note over LifespanTask, AppTask: Both tasks running concurrently

        par Lifespan maintains state
            LifespanTask->>LifespanTask: Keep lifespan connection alive
        and Application serves requests
            AppTask->>UserApp: HTTP/WebSocket requests
            UserApp-->>AppTask: Responses
        end
    end

    Note over Server: Shutdown signal received

    Server->>AppTask: Stop accepting new connections
    AppTask->>AppTask: Complete pending requests

    LifespanTask->>UserApp: {"type": "lifespan.shutdown"}

    Note over UserApp: Cleanup databases, caches, etc.

    UserApp-->>LifespanTask: {"type": "lifespan.shutdown.complete"}

    LifespanTask->>-Server: Lifespan task complete
    AppTask->>-Server: Application task complete

    Note over Server: βœ… Server stopped

Having the lifespan task run as a sibling task is a deliberate design choice. It could have been implemented as a parent task that spawns the
application task. This decision has the implication that if you create a [ContextVar][contextvars.ContextVar]
in the lifespan task, it will not be available in the application task.

Usage

Let's see an example of a minimal (but complete) ASGI application that implements the lifespan protocol:

python title="asgi application with lifespan" hl_lines="3-11"
async def app(scope, receive, send):
    if scope['type'] == 'lifespan':
        while True:
            message = await receive()
            if message['type'] == 'lifespan.startup':
                print("Application is starting up...")
                await send({'type': 'lifespan.startup.complete'})
            elif message['type'] == 'lifespan.shutdown':
                print("Application is shutting down...")
                await send({'type': 'lifespan.shutdown.complete'})
                return
    elif scope['type'] == 'http':
        await send({
            'type': 'http.response.start',
            'status': 200,
            'headers': [(b'content-type', b'text/plain')],
        })
        await send({'type': 'http.response.body', 'body': b'Hello, World!'})
    else:
        raise RuntimeError("This server doesn't support WebSocket.")

You can run the above application with uvicorn main:app. Then you'll see the print statements when the
application starts. You can also try to send some HTTP requests to it, and it will respond with "Hello, World!".
And if you stop the server (CTRL + C), it will print "Application is shutting down...".

Disabling Lifespan

If you want to disable the lifespan protocol, you can do so by setting the lifespan option to off when running Uvicorn:

bash
uvicorn main:app --lifespan off

By default, Uvicorn will automatically enable the lifespan protocol if the application supports it.


File: docs/concepts/logging.md

Uvicorn uses Python's built-in logging
module, and provides three loggers out of the box:

Logger name Purpose
uvicorn Parent logger (rarely used directly)
uvicorn.error Server-level messages (startup, shutdown, errors)
uvicorn.access Per-request access log lines

!!! note
Despite its name, uvicorn.error is not limited to error messages.
It is the general-purpose server logger, similar to how Gunicorn names its
main logger. See #562 for
background.

Default Configuration

By default, Uvicorn applies the following
dictConfig()
configuration:

python
LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "default": {
            "()": "uvicorn.logging.DefaultFormatter",
            "fmt": "%(levelprefix)s %(message)s",
            "use_colors": None,
        },
        "access": {
            "()": "uvicorn.logging.AccessFormatter",
            "fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',
        },
    },
    "handlers": {
        "default": {
            "formatter": "default",
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stderr",
        },
        "access": {
            "formatter": "access",
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stdout",
        },
    },
    "loggers": {
        "uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
        "uvicorn.error": {"level": "INFO"},
        "uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False},
    },
}

Custom Logging Configuration

You can supply a custom logging configuration file with the --log-config
option (or log_config when calling uvicorn.run()).

Uvicorn supports three file formats:

Extension Loader Notes
.json logging.config.dictConfig Standard JSON dictConfig schema.
.yaml/.yml logging.config.dictConfig Requires PyYAML (uvicorn[standard]).
Any other logging.config.fileConfig Classic INI-style format.

YAML Example

Create a file named log_config.yaml:

yaml
version: 1
disable_existing_loggers: false
formatters:
  default:
    "()": uvicorn.logging.DefaultFormatter
    fmt: "%(asctime)s - %(levelprefix)s %(message)s"
    datefmt: "%Y-%m-%d %H:%M:%S"
    use_colors: null
  access:
    "()": uvicorn.logging.AccessFormatter
    fmt: '%(asctime)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s'
    datefmt: "%Y-%m-%d %H:%M:%S"
handlers:
  default:
    formatter: default
    class: logging.StreamHandler
    stream: ext://sys.stderr
  access:
    formatter: access
    class: logging.StreamHandler
    stream: ext://sys.stdout
loggers:
  uvicorn:
    handlers:
      - default
    level: INFO
    propagate: false
  uvicorn.error:
    level: INFO
  uvicorn.access:
    handlers:
      - access
    level: INFO
    propagate: false

Then pass it to Uvicorn:

=== "CLI"

text
```bash
uvicorn main:app --log-config log_config.yaml
```

=== "Programmatic"

text
```python
uvicorn.run("main:app", log_config="log_config.yaml")
```

JSON Example

Create a file named log_config.json:

json
{
  "version": 1,
  "disable_existing_loggers": false,
  "formatters": {
    "default": {
      "()": "uvicorn.logging.DefaultFormatter",
      "fmt": "%(asctime)s - %(levelprefix)s %(message)s",
      "datefmt": "%Y-%m-%d %H:%M:%S",
      "use_colors": null
    },
    "access": {
      "()": "uvicorn.logging.AccessFormatter",
      "fmt": "%(asctime)s - %(levelprefix)s %(client_addr)s - \"%(request_line)s\" %(status_code)s",
      "datefmt": "%Y-%m-%d %H:%M:%S"
    }
  },
  "handlers": {
    "default": {
      "formatter": "default",
      "class": "logging.StreamHandler",
      "stream": "ext://sys.stderr"
    },
    "access": {
      "formatter": "access",
      "class": "logging.StreamHandler",
      "stream": "ext://sys.stdout"
    }
  },
  "loggers": {
    "uvicorn": {
      "handlers": ["default"],
      "level": "INFO",
      "propagate": false
    },
    "uvicorn.error": {
      "level": "INFO"
    },
    "uvicorn.access": {
      "handlers": ["access"],
      "level": "INFO",
      "propagate": false
    }
  }
}

Programmatic `dictConfig`

You can also pass a dictionary directly when running programmatically:

python
import uvicorn

log_config = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "default": {
            "()": "uvicorn.logging.DefaultFormatter",
            "fmt": "%(asctime)s - %(levelprefix)s %(message)s",
            "datefmt": "%Y-%m-%d %H:%M:%S",
        },
        "access": {
            "()": "uvicorn.logging.AccessFormatter",
            "fmt": '%(asctime)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',
            "datefmt": "%Y-%m-%d %H:%M:%S",
        },
    },
    "handlers": {
        "default": {
            "formatter": "default",
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stderr",
        },
        "access": {
            "formatter": "access",
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stdout",
        },
    },
    "loggers": {
        "uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
        "uvicorn.error": {"level": "INFO"},
        "uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False},
    },
}

uvicorn.run("main:app", log_config=log_config)

Common Recipes

Writing Logs to a File

To write Uvicorn's server logs to a file in addition to the console, add a FileHandler to the uvicorn logger:

yaml
version: 1
disable_existing_loggers: false
formatters:
  default:
    "()": uvicorn.logging.DefaultFormatter
    fmt: "%(asctime)s - %(levelprefix)s %(message)s"
    datefmt: "%Y-%m-%d %H:%M:%S"
    use_colors: false
  access:
    "()": uvicorn.logging.AccessFormatter
    fmt: '%(asctime)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s'
    datefmt: "%Y-%m-%d %H:%M:%S"
handlers:
  default:
    formatter: default
    class: logging.StreamHandler
    stream: ext://sys.stderr
  access:
    formatter: access
    class: logging.StreamHandler
    stream: ext://sys.stdout
  file:
    formatter: default
    class: logging.FileHandler
    filename: uvicorn.log
loggers:
  uvicorn:
    handlers:
      - default
      - file
    level: INFO
    propagate: false
  uvicorn.error:
    level: INFO
  uvicorn.access:
    handlers:
      - access
    level: INFO
    propagate: false

In this example, uvicorn.access still writes to stdout only. To write access
logs to the file as well, add file to the uvicorn.access.handlers list.

Disabling Access Logs

Use the --no-access-log CLI flag, or set access_log=False programmatically.
This removes all handlers from uvicorn.access without affecting the
uvicorn.error logger.

Disabling Colors

Pass --no-use-colors on the command line, or set use_colors=False
programmatically. When using a custom --log-config, set use_colors: false
on each formatter that extends uvicorn.logging.ColourizedFormatter.

Using a Standard Formatter

If you do not need Uvicorn's colorized output, you can use the standard
logging.Formatter instead:

yaml
version: 1
disable_existing_loggers: false
formatters:
  default:
    format: "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
    datefmt: "%Y-%m-%d %H:%M:%S"
handlers:
  default:
    formatter: default
    class: logging.StreamHandler
    stream: ext://sys.stderr
loggers:
  uvicorn:
    handlers:
      - default
    level: INFO
    propagate: false
  uvicorn.error:
    level: INFO
  uvicorn.access:
    handlers:
      - default
    level: INFO
    propagate: false

!!! warning
When using a standard logging.Formatter for the access logger, the
%(client_addr)s, %(request_line)s, and %(status_code)s placeholders
are not available. The access log line will be formatted using only the
standard %(message)s field.


File: docs/concepts/websockets.md

Uvicorn supports the WebSocket protocol as defined in RFC 6455.

Upgrade Process

The WebSocket protocol starts as an HTTP connection that gets "upgraded" to a WebSocket connection
through a handshake process. Here's how it works:

mermaid
sequenceDiagram
    participant Client
    participant Server
    participant ASGI App

    Note over Client,ASGI App: WebSocket Handshake Process

    Client->>Server: HTTP GET Request
    Note right of Client: Headers:Upgrade: websocketConnection: UpgradeSec-WebSocket-Key: [key]Sec-WebSocket-Version: 13

    Server->>ASGI App: websocket.connect event
    Note right of Server: Scope type: "websocket"

    alt Connection Accepted
        ASGI App->>Server: {"type": "websocket.accept"}
        Server->>Client: HTTP 101 Switching Protocols
        Note right of Server: Headers:Upgrade: websocketConnection: UpgradeSec-WebSocket-Accept: [hash]

        Note over Client,ASGI App: WebSocket Connection Established

        loop Message Exchange
            Client->>Server: WebSocket Frame
            Server->>ASGI App: websocket.receive event
            ASGI App->>Server: {"type": "websocket.send", "text": "..."}
            Server->>Client: WebSocket Frame
        end

        alt Client Closes
            Client->>Server: Close Frame
            Server->>ASGI App: websocket.disconnect event
        else Server Closes
            ASGI App->>Server: {"type": "websocket.close"}
            Server->>Client: Close Frame
        end

    else Connection Rejected
        ASGI App->>Server: {"type": "websocket.http.response.start", "status": 403}
        Server->>Client: HTTP 403 Forbidden
    end
  1. Initial HTTP Request: The client sends a regular HTTP GET request with special headers indicating it wants to upgrade to WebSocket:

    • Upgrade: websocket
    • Connection: Upgrade
    • Sec-WebSocket-Key: A base64-encoded random key
    • Sec-WebSocket-Version: 13
  2. Server Processing: Uvicorn receives the request and creates a WebSocket scope, sending a websocket.connect event to the ASGI application.

  3. Application Decision: The ASGI app decides whether to accept or reject the connection based on authentication, authorization, or other logic.

  4. Handshake Completion: If accepted, the server responds with HTTP 101 status and the computed Sec-WebSocket-Accept header.

  5. Full-Duplex Communication: Once upgraded, both client and server can send messages at any time using WebSocket frames.

  6. Connection Termination: Either side can initiate closing the connection with a close frame.

ASGI WebSocket Events

Uvicorn translates WebSocket protocol messages into ASGI events:

  • websocket.connect: Sent when a client requests a WebSocket upgrade
  • websocket.receive: Sent when a message is received from the client
  • websocket.disconnect: Sent when the connection is closed

The ASGI app can respond with:

  • websocket.accept: Accept the connection upgrade with an optional subprotocol
  • websocket.send: Send a message to the client
  • websocket.close: Close the connection with an optional status code

You can read more about it on the ASGI documentation.

Protocol Implementations

Uvicorn has three implementations of the WebSocket protocol.

WSProto Protocol

This implementation was the first implemented. It uses the
wsproto package underneath.

You can choose this protocol by setting the --ws option to wsproto.

WebSocket Protocol

This implementation uses the websockets package as dependency.

By default, if you have websockets installed, Uvicorn will use this protocol.

WebSockets SansIO Protocol

Since websockets deprecated the API Uvicorn uses to run the previous protocol, we had to create this new
protocol that uses the websockets SansIO API.

You can choose this protocol by setting the --ws option to websockets-sansio.

!!! note
The SansIO implementation was released in Uvicorn version 0.35.0 in June 2025.


File: docs/deployment/docker.md

Dockerfile

Docker is a popular choice for modern application deployment. However, creating a good Dockerfile from scratch can be challenging. This guide provides a solid foundation that works well for most Python projects.

While the example below won't fit every use case, it offers an excellent starting point that you can adapt to your specific needs.

Quickstart

For this example, we'll need to install docker,
docker-compose and
uv.

Then, let's create a new project with uv:

bash
uv init app

This will create a new project with a basic structure:

bash
app/
β”œβ”€β”€ main.py
β”œβ”€β”€ pyproject.toml
└── README.md

On main.py, let's create a simple ASGI application:

python title="main.py"
async def app(scope, receive, send):
    body = "Hello, world!"
    await send(
        {
            "type": "http.response.start",
            "status": 200,
            "headers": [
                [b"content-type", b"text/plain"],
                [b"content-length", len(body)],
            ],
        }
    )
    await send(
        {
            "type": "http.response.body",
            "body": body.encode("utf-8"),
        }
    )

We need to include uvicorn in the dependencies:

bash
uv add uvicorn

This will also create a uv.lock file. :sunglasses:

??? tip "What is uv.lock?"

text
`uv.lock` is a `uv` specific lockfile. A lockfile is a file that contains the exact versions of the dependencies
that were installed when the `uv.lock` file was created.

This allows for deterministic builds and consistent deployments.

Just to make sure everything is working, let's run the application:

bash
uv run uvicorn main:app

You should see the following output:

bash
INFO:     Started server process [62727]
INFO:     Waiting for application startup.
INFO:     ASGI 'lifespan' protocol appears unsupported.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Dockerfile

We'll create a cache-aware Dockerfile that optimizes build times. The key strategy is to install dependencies first, then copy the project files. This approach leverages Docker's caching mechanism to significantly speed up rebuilds.

dockerfile title="dockerfile"
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# Change the working directory to the `app` directory
WORKDIR /app

# Install dependencies
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --frozen --no-install-project

# Copy the project into the image
ADD . /app

# Sync the project
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen

# Run with uvicorn
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

A common question is "how many workers should I run?". The image above uses a single Uvicorn worker.
The recommended approach is to let your orchestration system manage the number of deployed containers rather than
relying on the process manager inside the container.

You can read more about this in the
Decouple applications section
of the Docker documentation.

!!! warning "For production, create a non-root user!"
When running in production, you should create a non-root user and run the container as that user.

To make sure it works, let's build the image and run it:

bash
docker build -t my-app .
docker run -p 8000:8000 my-app

For more information on using uv with Docker, refer to the
official uv Docker integration guide.

Docker Compose

When running in development, it's often useful to have a way to hot-reload the application when code changes.

Let's create a docker-compose.yml file to run the application:

yaml title="docker-compose.yml"
services:
  backend:
    build: .
    ports:
      - "8000:8000"
    environment:
      - UVICORN_RELOAD=true
    volumes:
      - .:/app
    tty: true

You can run the application with docker compose up and it will automatically rebuild the image when code changes.

Now you have a fully working development environment! :tada:


File: docs/deployment/index.md

Server deployment is a complex area, that will depend on what kind of service you're deploying Uvicorn onto.

As a general rule, you probably want to:

  • Run uvicorn --reload from the command line for local development.
  • Run gunicorn -k uvicorn.workers.UvicornWorker for production.
  • Additionally run behind Nginx for self-hosted deployments.
  • Finally, run everything behind a CDN for caching support, and serious DDOS protection.

Running from the command line

Typically you'll run uvicorn from the command line.

bash
$ uvicorn main:app --reload --port 5000

The ASGI application should be specified in the form path.to.module:instance.path.

When running locally, use --reload to turn on auto-reloading.

The --reload and --workers arguments are mutually exclusive.

To see the complete set of available options, use uvicorn --help.

See the settings documentation for more details on the supported options for running uvicorn.

Running programmatically

To run directly from within a Python program, you should use uvicorn.run(app, **config). For example:

py title="main.py"
import uvicorn

class App:
    ...

app = App()

if __name__ == "__main__":
    uvicorn.run("main:app", host="127.0.0.1", port=5000, log_level="info")

The set of configuration options is the same as for the command line tool.

Note that the application instance itself can be passed instead of the app
import string.

python
uvicorn.run(app, host="127.0.0.1", port=5000, log_level="info")

However, this style only works if you are not using multiprocessing (workers=NUM)
or reloading (reload=True), so we recommend using the import string style.

Also note that in this case, you should put uvicorn.run into if __name__ == '__main__' clause in the main module.

!!! note
The reload and workers parameters are mutually exclusive.

Using a process manager

Running Uvicorn using a process manager ensures that you can run multiple processes in a resilient manner, and allows you to perform server upgrades without dropping requests.

A process manager will handle the socket setup, start-up multiple server processes, monitor process aliveness, and listen for signals to provide for processes restarts, shutdowns, or dialing up and down the number of running processes.

Built-in

Uvicorn includes a --workers option that allows you to run multiple worker processes.

bash
$ uvicorn main:app --workers 4

Unlike gunicorn, uvicorn does not use pre-fork, but uses spawn, which allows uvicorn's multiprocess manager to still work well on Windows.

The default process manager monitors the status of child processes and automatically restarts child processes that die unexpectedly. Not only that, it will also monitor the status of the child process through the pipeline. When the child process is accidentally stuck, the corresponding child process will be killed through an unstoppable system signal or interface.

You can also manage child processes by sending specific signals to the main process. (Not supported on Windows.)

  • SIGHUP: Gracefully restart the workers one at a time with no dropped requests. Fresh workers pick up new code on disk.
  • SIGTTIN: Increase the number of worker processes by one.
  • SIGTTOU: Decrease the number of worker processes by one.

Gunicorn

!!! warning
The uvicorn.workers module is deprecated and will be removed in a future release.

text
You should use the [`uvicorn-worker`](https://github.com/Kludex/uvicorn-worker) package instead.

```bash
python -m pip install uvicorn-worker
```

Gunicorn is probably the simplest way to run and manage Uvicorn in a production setting. Uvicorn includes a gunicorn worker class that means you can get set up with very little configuration.

The following will start Gunicorn with four worker processes:

gunicorn -w 4 -k uvicorn.workers.UvicornWorker

The UvicornWorker implementation uses the uvloop and httptools implementations. To run under PyPy you'll want to use pure-python implementation instead. You can do this by using the UvicornH11Worker class.

gunicorn -w 4 -k uvicorn.workers.UvicornH11Worker

Gunicorn provides a different set of configuration options to Uvicorn, so some options such as --limit-concurrency are not yet supported when running with Gunicorn.

If you need to pass uvicorn's config arguments to gunicorn workers then you'll have to subclass UvicornWorker:

python
from uvicorn.workers import UvicornWorker

class MyUvicornWorker(UvicornWorker):
    CONFIG_KWARGS = {"loop": "asyncio", "http": "h11", "lifespan": "off"}

Supervisor

To use supervisor as a process manager you should either:

  • Hand over the socket to uvicorn using its file descriptor, which supervisor always makes available as 0, and which must be set in the fcgi-program section.
  • Or use a UNIX domain socket for each uvicorn process.

A simple supervisor configuration might look something like this:

ini title="supervisord.conf"
[supervisord]

[fcgi-program:uvicorn]
socket=tcp://localhost:8000
command=venv/bin/uvicorn --fd 0 main:App
numprocs=4
process_name=uvicorn-%(process_num)d
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0

Then run with supervisord -n.

Running behind Nginx

Using Nginx as a proxy in front of your Uvicorn processes may not be necessary, but is recommended for additional resilience. Nginx can deal with serving your static media and buffering slow requests, leaving your application servers free from load as much as possible.

In managed environments such as Heroku, you won't typically need to configure Nginx, as your server processes will already be running behind load balancing proxies.

The recommended configuration for proxying from Nginx is to use a UNIX domain socket between Nginx and whatever the process manager that is being used to run Uvicorn. If using Uvicorn directly you can bind it to a UNIX domain socket using uvicorn --uds /path/to/socket.sock <...>.

When running your application behind one or more proxies you will want to make sure that each proxy sets appropriate headers to ensure that your application can properly determine the client address of the incoming connection, and if the connection was over http or https. For more information see Proxies and Forwarded Headers below.

Here's how a simple Nginx configuration might look. This example includes setting proxy headers, and using a UNIX domain socket to communicate with the application server.

It also includes some basic configuration to forward websocket connections.
For more info on this, check Nginx recommendations.

conf
http {
  server {
    listen 80;
    client_max_body_size 4G;

    server_name example.com;

    location / {
      proxy_set_header Host $http_host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection $connection_upgrade;
      proxy_redirect off;
      proxy_buffering off;
      proxy_pass http://uvicorn;
    }

    location /static {
      # path for static files
      root /path/to/app/static;
    }
  }

  map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
  }

  upstream uvicorn {
    server unix:/tmp/uvicorn.sock;
  }

}

Uvicorn's --proxy-headers behavior may not be sufficient for more complex proxy configurations that use different combinations of headers, or where the application is running behind more than one intermediary proxying service.

In those cases, you might want to use an ASGI middleware to set the client and scheme dependent on the request headers.

Running behind a CDN

Running behind a content delivery network, such as Cloudflare or Cloud Front, provides a serious layer of protection against DDoS attacks. Your service will be running behind huge clusters of proxies and load balancers that are designed for handling huge amounts of traffic, and have capabilities for detecting and closing off connections from DDoS attacks.

Proper usage of cache control headers can mean that a CDN is able to serve large amounts of data without always having to forward the request on to your server.

Content Delivery Networks can also be a low-effort way to provide HTTPS termination.

Running with HTTPS

To run uvicorn with https, a certificate and a private key are required.
The recommended way to get them is using Let's Encrypt.

For local development with https, it's possible to use mkcert
to generate a valid certificate and private key.

bash
$ uvicorn main:app --port 5000 --ssl-keyfile=./key.pem --ssl-certfile=./cert.pem

Running gunicorn worker

It's also possible to use certificates with uvicorn's worker for gunicorn.

bash
$ gunicorn --keyfile=./key.pem --certfile=./cert.pem -k uvicorn.workers.UvicornWorker main:app

Customizing the SSL context

For TLS scenarios that the --ssl-* flags don't cover (e.g., mutual TLS, custom SSLContext.options, bumping minimum_version, loading certificates from memory), pass an ssl_context_factory to uvicorn.run() or Config.

The factory receives the Config instance and a default_ssl_context_factory callable that builds the standard context from the ssl_* settings on Config. Use it to start from uvicorn's default and mutate it, or ignore it and build your own context from scratch - the ssl_* settings are only consumed by the default factory, so if you don't call it they're effectively unused.

python
import ssl
from collections.abc import Callable

import uvicorn
from uvicorn.config import Config


def ssl_context_factory(config: Config, default_ssl_context_factory: Callable[[], ssl.SSLContext]) -> ssl.SSLContext:
    context = default_ssl_context_factory()
    context.minimum_version = ssl.TLSVersion.TLSv1_3
    return context


uvicorn.run(
    "main:app",
    ssl_keyfile="key.pem",
    ssl_certfile="cert.pem",
    ssl_context_factory=ssl_context_factory,
)

The factory is called inside each worker process, so it works with --reload and --workers > 1. The factory itself must be picklable in those modes (a top-level function is fine; lambdas and local closures are not). The ssl_* settings on Config are only consumed by default_ssl_context_factory(); if you build the context yourself without calling it, those settings are ignored.

Proxies and Forwarded Headers

When running an application behind one or more proxies, certain information about the request is lost.
To avoid this most proxies will add headers containing this information for downstream servers to read.

Uvicorn currently supports the following headers:

Uvicorn can use these headers to correctly set the client and protocol in the request.
However as anyone can set these headers you must configure which "clients" you will trust to have set them correctly.

Uvicorn can be configured to trust IP Addresses (e.g. 127.0.0.1), IP Networks (e.g. 10.100.0.0/16),
or Literals (e.g. /path/to/socket.sock). When running from CLI these are configured using --forwarded-allow-ips.

!!! Warning "Only trust clients you can actually trust!"
Incorrectly trusting other clients can lead to malicious actors spoofing their apparent client address to your application.

A proxy chain may send a header once per hop rather than as a single comma-separated value. Repeated X-Forwarded-For fields are combined in order (as the equivalent comma-separated list, RFC 9110, 5.3), while for X-Forwarded-Proto the last field is used.

For more information, check ProxyHeadersMiddleware.

Client Port

Currently if the ProxyHeadersMiddleware is able to retrieve a trusted client value then the client's port will be set to 0.
This is because port information is lost when using these headers.

UNIX Domain Sockets (UDS)

Although it is common for UNIX Domain Sockets to be used for communicating between various HTTP servers, they can mess with some of the expected received values as they will be various non-address strings or missing values.

For example:

  • when NGINX itself is running behind a UDS it will add the literal unix: as the client in the X-Forwarded-For header.
  • When Uvicorn is running behind a UDS the initial client will be None.

Trust Everything

Rather than specifying what to trust, you can instruct Uvicorn to trust all clients using the literal "*".
You should only set this when you know you can trust all values within the forwarded headers (e.g. because
your proxies remove the existing headers before setting their own).


File: docs/contributing.md

Contributing

Thank you for being interested in contributing to Uvicorn.
There are many ways you can contribute to the project:

Reporting Bugs, Issues or Feature Requests

Found something that Uvicorn should support?
Stumbled upon some unexpected behaviour?
Need a missing functionality?

Contributions should generally start out from a previous discussion.
You can reach out someone at the community chat
or at the github discussions tab.

When creating a new topic in the discussions tab, possible bugs may be raised
as a "Potential Issue" discussion, feature requests may be raised as an
"Ideas" discussion. We can then determine if the discussion needs
to be escalated into an "Issue" or not, or if we'd consider a pull request.

Try to be more descriptive as you can and in case of a bug report,
provide as much information as possible like:

  • OS platform
  • Python version
  • Installed dependencies and versions (python -m pip freeze)
  • Code snippet
  • Error traceback

You should always try to reduce any examples to the simplest possible case
that demonstrates the issue.

Some possibly useful tips for narrowing down potential issues...

  • Does the issue exist with a specific supervisor like Multiprocess or more than one?
  • Does the issue exist on asgi, or wsgi, or both?
  • Are you running Uvicorn in conjunction with Gunicorn, others, or standalone?

Development

To start developing Uvicorn create a fork of the
Uvicorn repository on GitHub.

Then clone your fork with the following command replacing YOUR-USERNAME with
your GitHub username:

shell
$ git clone https://github.com/YOUR-USERNAME/uvicorn

You can now install the project and its dependencies using:

shell
$ cd uvicorn
$ scripts/install

Testing and Linting

We use custom shell scripts to automate testing, linting,
and documentation building workflow.

To run the tests, use:

shell
$ scripts/test

Any additional arguments will be passed to pytest. See the pytest documentation for more information.

For example, to run a single test script:

shell
$ scripts/test tests/test_cli.py

To run the code auto-formatting:

shell
$ scripts/lint

Lastly, to run code checks separately (they are also run as part of scripts/test), run:

shell
$ scripts/check

Documenting

Documentation pages are located under the docs/ folder.

To run the documentation site locally (useful for previewing changes), use:

shell
$ scripts/docs serve

Resolving Build / CI Failures

Once you've submitted your pull request, the test suite will
automatically run, and the results will show up in GitHub.
If the test suite fails, you'll want to click through to the
"Details" link, and try to identify why the test suite failed.

Here are some common ways the test suite can fail:

Check Job Failed

This job failing means there is either a code formatting issue or type-annotation issue.
You can look at the job output to figure out why it's failed or within a shell run:

shell
$ scripts/check

It may be worth it to run $ scripts/lint to attempt auto-formatting the code
and if that job succeeds commit the changes.

Docs Job Failed

This job failing means the documentation failed to build. This can happen for
a variety of reasons like invalid markdown or missing configuration within mkdocs.yml.

Python 3.X Job Failed

This job failing means the unit tests failed or not all code paths are covered by unit tests.

If tests are failing you will see this message under the coverage report:

=== 1 failed, 354 passed, 1 skipped, 1 xfailed in 37.08s ===

If tests succeed but coverage doesn't reach 100%, you will see this
message under the coverage report:

Coverage failure: total of 98 is less than fail-under=100

Releasing

This section is targeted at Uvicorn maintainers.

Before releasing a new version, create a pull request that includes:

  • An update to the changelog:
    • We follow the format from keepachangelog.
    • Compare main with the tag of the latest release, and list all entries that are of interest to our users:
      • Things that must go in the changelog: added, changed, deprecated or removed features, and bug fixes.
      • Things that should not go in the changelog: changes to documentation, tests or tooling.
      • Try sorting entries in descending order of impact / importance.
      • Keep it concise and to-the-point. 🎯
  • A version bump: see __init__.py.

For an example, see #1006.

Once the release PR is merged, create a
new release including:

  • Tag version like 0.13.3.
  • Release title Version 0.13.3
  • Description copied from the changelog.

Once created this release will be automatically uploaded to PyPI.