Should You Use AsyncIO for Your Next Python Web Application?
Python’s AsyncIO web ecosystem continues to mature, but should you build your next production application with one of these shiny new frameworks such as FastAPI, Starlette, or Quart?
Table of Contents
A Brief History of Python Web Server Interfaces
Prior to PEP 333, Python web application frameworks, such as Zope, Quixote, and Twisted Web would each be written against specific web server APIs such as CGI or mod_python. PEP 333 created the v1.0 implementation of WSGI to give Python a standard similar to Java’s servlet API. WSGI created a common interface that promoted interchangeability between web frameworks and web servers. For example, Frameworks such as Django, Flask, and Pyramid are compatible with servers such as uWSGI and gunicorn.
Between 2003, when PEP 333 was published, and now, in 2022, WSGI has seen near universal adoption with all popular Python
web frameworks and servers using WSGI. Since 2003, web application development and network protocols have evolved and changed.
The RFC for the WebSocket protocol was finalized in 2013. The release of Python
3.6, in 2016, added the async
and await
keywords, which enable non-blocking I/O. WSGI accepts a request and returns
a response, which doesn’t work for newer protocols such as WebSocket. ASGI continues
the legacy of WSGI by allowing the same interchangeability in the new world of asynchronous Python. ASGI is a
fundamental redesign of WSGI that enables support for newer protocols such as HTTP/2, HTTP/3, and WebSocket.
AsyncIO Package Ecosystem Overview
Servers
Uvicorn
Uvicorn supports the HTTP and WebSocket protocols. Encode OSS, the company started by Tom Christie, the creator of Django REST Framework, maintains Uvicorn. Uvicorn tends to be close to the top in popular performance benchmarks due to its use of uvloop, an alternative event loop, and httptools, Python bindings for the NodeJS HTTP parser.
Uvicorn implements a gunicorn worker, that allows you to turn gunicorn into an ASGI server. Gunicorn is a fantastic, battle-tested process manager and WSGI server for Python, and combining it with uvicorn gives you one of the best ASGI servers. Uvicorn also implements an alternative gunicorn worker with support for PyPy.
Code Example
Taken from the Uvicorn docs
import uvicorn
async def app(scope, receive, send):
...
if __name__ == "__main__":
uvicorn.run("example:app", host="127.0.0.1", port=5000, log_level="info")
Hypercorn
Hypercorn supports HTTP, HTTP/2, HTTP/3 (QUIC), and WebSocket protocols and utilizes the python hyper libraries and uvloop. Initially, hypercorn was a part of the Quart web framework but transitioned to a standalone ASGI server. Hypercorn is maintained by Philip Jones, a member of the Pallets Project that maintains Flask.
Hypercorn stands out as a well maintained ASGI server that supports HTTP/2, HTTP/3, and Trio, an alternative implementation to the Python standard library’s AsyncIO package. As opposed to uvicorn, hypercorn works similar to gunicorn and operates as a process manager.
Code Example
Taken from the Hypercorn docs
import asyncio
from hypercorn.asyncio import serve
from hypercorn.config import Config
config = Config()
config.bind = ["localhost:8080"]
async def app():
...
asyncio.run(serve(app, config))
Frameworks
Starlette
Starlette can be used as a web framework or a toolkit and supports WebSocket, GraphQL, HTTP server push, in-process background tasks, and more. It falls somewhere in the middle between Django’s batteries include approach and Flask’s minimalism. Starlette is also maintained by Encode OSS. Starlette also tends to be near the top of popular performance benchmarks.
Starlette occupies a unique position in the current ASGI framework ecosystem since other popular projects such as FastAPI build on top of it. Tom Christie has a great track record of open source maintenance and development with Django REST Framework, and Encode has funding.
Code Example
Taken from the Starlette docs
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import (
Route,
Mount,
WebSocketRoute
)
from starlette.staticfiles import StaticFiles
def homepage(request):
return PlainTextResponse('Hello, world!')
def user_me(request):
username = "John Doe"
return PlainTextResponse('Hello, %s!' % username)
def user(request):
username = request.path_params['username']
return PlainTextResponse('Hello, %s!' % username)
async def websocket_endpoint(websocket):
await websocket.accept()
await websocket.send_text('Hello, websocket!')
await websocket.close()
def startup():
print('Ready to go')
routes = [
Route('/', homepage),
Route('/user/me', user_me),
Route('/user/{username}', user),
WebSocketRoute('/ws', websocket_endpoint),
Mount('/static', StaticFiles(directory="static")),
]
app = Starlette(
debug=True,
routes=routes,
on_startup=[startup]
)
FastAPI
FastAPI is a framework built on top of Starlette that adds Pydantic, a Python package that provides data validation and settings management using Python’s type annotations. By adding Pydantic, FastAPI endpoints validate input data and auto generate documentation. FastAPI is maintained by Sebastián Ramírez who has sponsor funding for FastAPI. By using Starlette under the hood, FastAPI’s performance is near the top of popular performance benchmarks.
As the name implies, FastAPI is intended for API applications, and this is where it excels. In the past, if you were creating a small API application, Flask would be the choice. Now, I would consider using FastAPI over Flask. The AsyncIO ecosystem isn’t as mature as the WSGI/Flask ecosystem, but FastAPI looks like one of the big future frameworks in the Python web ecosystem.
Code Example
Taken from the FastAPI docs
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
is_offer: Union[bool, None] = None
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
return {"item_id": item_id, "q": q}
@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
return {"item_name": item.name, "item_id": item_id}
Quart
Quart bills itself as an AsyncIO reimplementation of the Flask microframework API and provides a migration guide. Additionally, some Flask extensions work with Quart. The author of Quart, Philip Jones, also maintains Hypercorn. Quart’s performance ranks lower than some of the previously mentioned frameworks. Quart stands out with its potential access to the large package ecosystem surrounding Flask.
Code Example
Taken from Quart docs
from dataclasses import dataclass
from datetime import datetime
from quart import Quart
from quart_schema import (
QuartSchema,
validate_request,
validate_response
)
app = Quart(__name__)
QuartSchema(app)
@dataclass
class TodoIn:
task: str
due: datetime | None
@dataclass
class Todo(TodoIn):
id: int
@app.post("/todos/")
@validate_request(TodoIn)
@validate_response(Todo)
async def create_todo(data: Todo) -> Todo:
return Todo(id=1, task=data.task, due=data.due)
Other Frameworks
- Sanic
- Supports ASGI, WebSocket, and background tasks
- BlackSheep
- Supports ASGI, WebSocket, and background tasks
- Aiohttp
- AsyncIO Client and Server
Benefits of Using AsyncIO
- Improved Throughput
- AsyncIO does not usually improve latency for I/O requests. In fact, if you look at the “latency” tab of the previously linked performance benchmark, AsyncIO frameworks perform worse in latency than Django and Flask. However, AsyncIO improves the throughput of your application, meaning the same server hardware can handle more requests per second using AsyncIO.
- Availability of New Protocols
- Due to the limitations of WSGI, it doesn’t support newer protocols such as HTTP/2, HTTP/3 and WebSocket. With AsyncIO, Python web servers and frameworks can support these newer protocols.
Obstacles to Using AsyncIO
- Django and Flask’s Package Ecosystems
- Large third-party package ecosystems developed around both Django and Flask. Taping into these ecosystems saves you development time. The same ecosystem has not yet developed around AsyncIO frameworks.
- Synchronous I/O Blocks AsyncIO
- Let’s say I’m building a SaaS application that needs to accept payments. I choose Stripe, but Stripe’s python library doesn’t support AsyncIO. If I use Stripe’s library, whenever I make an API request to Stripe, it blocks the event loop, and you lose the benefits of AsyncIO.
My Answer
While Python’s AsyncIO web ecosystem has come a long way, I still choose Flask or Django for production applications that don’t require newer protocols such as HTTP/2, HTTP/3 and WebSocket. Django and Flask each have a robust set of third-party packages to help you quickly build your application. On top of that, Django and Flask are both mature, production-tested frameworks used by a lot of companies. I think the new AsyncIO frameworks will reach that point as well, but they’re still too new. If you’re looking at using the AsyncIO ecosystem, your first question should be “Do I need it?” My guess is most people don’t. WSGI servers and frameworks are usually performant enough.
If supporting HTTP/2 or HTTP/3 is a requirement, you don’t have much choice on the server side. I recommend Hypercorn. If you aren’t required to support HTTP/2 or HTTP/3, then Uvicorn becomes my preferred server option. When it comes to frameworks, I would reach for FastAPI for small-API focused applications. Tom Christie has a great track record of open source maintenance with Django REST Framework, and FastAPI adds nice additions on top of Starlette. If you’re building a larger application with some HTML template rendering, I would choose Starlette over FastAPI for the flexibility. If you need to migrate an existing Flask project to enable newer protocols, Quart is the clear choice.
I want to be clear that I think ASGI and AsyncIO are great developments for Python, and I applaud all the people putting work into the ecosystem. I think the ecosystem will continue to grow and mature, but Django and Flask are my choice right now.