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:
$ pip install uvicornThis will install uvicorn with minimal (pure Python) dependencies.
$ 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
uvloopwill be installed and used if possible. - the http protocol will be handled by
httptoolsif possible.
Moreover, "optional extras" means that:
- the websocket protocol will be handled by
websockets(should you want to usewsprotoyou'd need to install it manually) if possible. - the
--reloadflag in development mode will usewatchfiles. python-dotenvwill be installed should you want to use the--env-fileoption.PyYAMLwill be installed to allow you to provide a.yamlfile to--log-config, if desired.
Create an application, in example.py:
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:
$ uvicorn example:appWhy 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.
$ pip install daphne
$ daphne app:AppHypercorn
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.
$ pip install hypercorn
$ hypercorn app:AppMangum
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:
async def app(scope, receive, send):
assert scope['type'] == 'http'
...Or instance-based applications:
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:
{
'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.
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:
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.
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.
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.
pip install daphne
daphne app:AppHypercorn
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.
pip install hypercorn
hypercorn app:AppASGI 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:
uvicorn main:app --loop <auto|asyncio|uvloop>By default, Uvicorn uses --loop auto, which automatically selects:
- uvloop - If uvloop is installed, Uvicorn will use it for maximum performance
- 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:
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:
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:
uvicorn main:app --loop winloop:new_event_loopFile: docs/concepts/http-protocols.md
HTTP Protocols
Uvicorn provides three HTTP/1.1 protocol implementations that you can choose from using the --http option:
uvicorn main:app --http <auto|h11|httptools|zttp>By default, Uvicorn uses --http auto, which automatically selects:
- httptools - If httptools is installed, Uvicorn will use it for maximum performance
- 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:
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:
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 stoppedHaving 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:
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:
uvicorn main:app --lifespan offBy 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 followingdictConfig()
configuration:
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:
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: falseThen pass it to Uvicorn:
=== "CLI"
```bash
uvicorn main:app --log-config log_config.yaml
```=== "Programmatic"
```python
uvicorn.run("main:app", log_config="log_config.yaml")
```JSON Example
Create a file named log_config.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:
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:
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: falseIn 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 theuvicorn.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 standardlogging.Formatter instead:
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:
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
endInitial HTTP Request: The client sends a regular HTTP GET request with special headers indicating it wants to upgrade to WebSocket:
Upgrade: websocketConnection: UpgradeSec-WebSocket-Key: A base64-encoded random keySec-WebSocket-Version: 13
Server Processing: Uvicorn receives the request and creates a WebSocket scope, sending a
websocket.connectevent to the ASGI application.Application Decision: The ASGI app decides whether to accept or reject the connection based on authentication, authorization, or other logic.
Handshake Completion: If accepted, the server responds with HTTP 101 status and the computed
Sec-WebSocket-Acceptheader.Full-Duplex Communication: Once upgraded, both client and server can send messages at any time using WebSocket frames.
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 upgradewebsocket.receive: Sent when a message is received from the clientwebsocket.disconnect: Sent when the connection is closed
The ASGI app can respond with:
websocket.accept: Accept the connection upgrade with an optional subprotocolwebsocket.send: Send a message to the clientwebsocket.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 thewsproto 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 anduv.
Then, let's create a new project with uv:
uv init appThis will create a new project with a basic structure:
app/
βββ main.py
βββ pyproject.toml
βββ README.mdOn main.py, let's create a simple ASGI application:
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:
uv add uvicornThis will also create a uv.lock file. :sunglasses:
??? tip "What is uv.lock?"
`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:
uv run uvicorn main:appYou should see the following output:
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.
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:
docker build -t my-app .
docker run -p 8000:8000 my-appFor 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:
services:
backend:
build: .
ports:
- "8000:8000"
environment:
- UVICORN_RELOAD=true
volumes:
- .:/app
tty: trueYou 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 --reloadfrom the command line for local development. - Run
gunicorn -k uvicorn.workers.UvicornWorkerfor 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.
$ uvicorn main:app --reload --port 5000The 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:
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.
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.
$ uvicorn main:app --workers 4Unlike 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.
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:
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 thefcgi-programsection. - Or use a UNIX domain socket for each
uvicornprocess.
A simple supervisor configuration might look something like this:
[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=0Then 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.
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.
$ uvicorn main:app --port 5000 --ssl-keyfile=./key.pem --ssl-certfile=./cert.pemRunning gunicorn worker
It's also possible to use certificates with uvicorn's worker for gunicorn.
$ gunicorn --keyfile=./key.pem --certfile=./cert.pem -k uvicorn.workers.UvicornWorker main:appCustomizing 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.
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:
X-Forwarded-For(MDN Reference)X-Forwarded-Proto(MDN Reference)
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 theX-Forwarded-Forheader. - 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:
- Using Uvicorn on your stack and reporting bugs/issues you find
- Implementing new features and fixing bugs
- Review Pull Requests of others
- Write documentation
- Participate in discussions
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
Multiprocessor 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:
$ git clone https://github.com/YOUR-USERNAME/uvicornYou can now install the project and its dependencies using:
$ cd uvicorn
$ scripts/installTesting and Linting
We use custom shell scripts to automate testing, linting,
and documentation building workflow.
To run the tests, use:
$ scripts/testAny additional arguments will be passed to pytest. See the pytest documentation for more information.
For example, to run a single test script:
$ scripts/test tests/test_cli.pyTo run the code auto-formatting:
$ scripts/lintLastly, to run code checks separately (they are also run as part of scripts/test), run:
$ scripts/checkDocumenting
Documentation pages are located under the docs/ folder.
To run the documentation site locally (useful for previewing changes), use:
$ scripts/docs serveResolving 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:
$ scripts/checkIt 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
mainwith 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.