1.241 Python Web Frameworks#
Python web frameworks compared: FastAPI, Flask, Django, Starlette, Litestar, Quart, aiohttp, Tornado, Sanic, Falcon. WSGI vs ASGI vs own-loop, and why download counts mislead here.
At a glance#
| Library | How it works | Best for | Latest release |
|---|---|---|---|
| FastAPI | ASGI, built on Starlette; type annotations are the specification | Typed JSON APIs where generated OpenAPI and editor support matter most | 0.141.1 · 2026-07-29 |
| Flask | WSGI kernel plus a large extension ecosystem; Jinja2 is a hard dependency | Small synchronous services, and the framework most developers already know | 3.1.3 · 2026-02-19 |
| Django | Full stack: ORM, migrations, admin, auth, forms, templating, i18n | Database-backed applications where the admin interface is part of the product | 6.1.1 · 2026-09-02 |
| Starlette | Minimal ASGI toolkit; no opinion above the protocol | Very small services, and unusual ones whose data handling fits no framework | 1.6.0 · 2026-08-08 |
| Litestar | ASGI, no Starlette dependency since 2.0; layered config, controllers, DTOs | Large APIs needing structure, and teams unwilling to commit to Pydantic alone | 2.24.0 · 2026-06-11 |
| Quart | Flask’s API reimplemented on ASGI; maintained by Pallets | An existing Flask codebase that needs real concurrency | 0.23.1 · 2026-08-29 |
| aiohttp (server) | asyncio client AND server with its own HTTP server; deliberately not ASGI | Services that are themselves substantial HTTP clients — proxies, gateways, relays | 3.14.3 · 2026-07-23 |
| Tornado | Its own coroutine framework and server, now running on asyncio; not ASGI | Maintaining what exists; long-lived connections where its handling is still better | 6.5.8 · 2026-08-07 |
| Sanic | asyncio framework packaged with its own server and worker manager; ASGI-compatible | Throughput, and deployments wanting framework and server tuned together | 25.12.1 · 2026-05-31 |
| Falcon | WSGI AND ASGI as genuine peers from one codebase; zero required dependencies | Data-plane services judged on predictability and dependency surface | 4.3.1 · 2026-06-16 |
| Django Ninja | FastAPI-shaped typed API layer running inside a Django project | Wanting Django’s ORM and admin without giving up a modern typed API | 1.7.0 · 2026-09-03 |
| Pyramid | Configurable rather than opinionated; routing AND traversal dispatch | Content-shaped URL hierarchies where traversal beats a routing table | 2.1 · 2026-03-11 |
| Bottle | A single file, no dependencies, WSGI | Vendoring into a project, or shipping where a package index is unreachable | 0.13.4 · 2025-06-15 |
| BlackSheep | ASGI, typed handlers, DI, OpenAPI; auth model borrowed from ASP.NET Core | Readers who find FastAPI’s authentication story thin | 2.6.3 · 2026-06-04 |
| Robyn | Python API over a Rust runtime; compiled extension | Watching a real trend, not shipping | 0.88.0 · 2026-06-25 |
Latest release observed from PyPI in 2026-09.
What the research found
- The category has three architectures, not fifteen frameworks, and the third one is the one that gets missed — WSGI (Flask, Django primarily, Bottle, Pyramid), ASGI (FastAPI, Starlette, Litestar, Quart, BlackSheep, Sanic, Falcon’s asgi app), and own-loop-own-server (aiohttp, Tornado, Robyn). The third row brings its own server, so the 1.242 application-server decision does not exist for it and no ASGI middleware attaches. A team that reads ‘async Python web framework’ as implying ASGI will be wrong about aiohttp and Tornado in ways that surface only at deployment. Falcon is the only framework treating WSGI and ASGI as genuine peers from one codebase.
- Async syntax and async concurrency are different things, and Flask is where most people find out — Flask accepts
async defhandlers and runs each in a private event loop inside a WSGI worker that stays blocked for the request’s whole life.awaitworks; throughput does not move. This is not a defect but the honest consequence of WSGI, and it has a specific resolution — Quart, Flask’s own API on ASGI, maintained by Pallets. Django’s async path is a milder version of the same shape: real, partial, and limited exactly where the ORM is. - Download counts measure position in the dependency graph, and in this category they actively mislead — The most-installed package here is Starlette at 676 million/mo, ahead of FastAPI at 591 million — because every FastAPI install pulls Starlette, making Starlette’s total FastAPI’s plus everything else by construction. aiohttp’s 648 million is overwhelmingly client installs. Tornado’s 118 million is substantially Jupyter infrastructure. Django, with the largest professional deployment base in Python, ranks sixth at 53 million. No adoption claim in this survey rests on a download number alone.
- Stars and downloads disagree so violently that the disagreement is the measurement — Robyn has 7,400 stars against 19,000 monthly installs — roughly 390 stars per install. Starlette has 12,600 stars against 676 million installs — roughly 54,000 installs per star. Robyn is interesting; Starlette is load-bearing, and nobody clicks a star for load-bearing. A framework high on stars and low on downloads has an audience that reads about it; the inverse has an audience that ships it.
- FastAPI’s dominance is a fact about the ecosystem, not about the code — 102,000 stars against Litestar’s 8,400, and Litestar has the better structural argument — layered configuration, controllers, validation that is not a single-library commitment, and DTOs that decouple wire shape from model. The strongest technical case in this category is not the one with the ecosystem, and that ecosystem (answered questions, integrations, developers who know it, and the training data behind coding assistants) is itself a legitimate decision input rather than a distraction from one.
per request framework cost us
- falcon-wsgi: 5.79
- blacksheep: 6.28
- falcon-asgi: 6.65
- bottle: 8.49
- starlette: 8.55
- litestar: 16.97
- sanic: 18.19
- pyramid: 20.96
- fastapi: 33.35
- django: 38.26
- flask: 40.54
- django-ninja: 55.09
- quart: 84.2
- NOT MEASURED: aiohttp, tornado, robyn – own their server, expose no application callable
validation decode and validate us
- msgspec: 0.275
- pydantic-v2: 0.993
- json.loads NO VALIDATION: 1.579
- dataclass NO VALIDATION: 1.947
- attrs+cattrs: 2.492
def handler ceiling
- limiter tokens: 40
- wall seconds 50ms handler: c1 0.052, c40 0.07, c41 0.106, c80 0.128, c120 0.176
- async def wall seconds c1 to c120: 0.051 to 0.054 – flat
Explainer
Domain Explainer: Python Web Frameworks#
What these libraries do, what separates them, and the one idea underneath the whole category. Terms are defined where they first appear.
The problem all of these solve#
A server is a computer that stays switched on and waits. Somewhere else, a browser or a
phone app sends it a request — a short message saying what it wants and where from:
“GET me the page at /items/42”. The server sends back a response: some text, an image,
a page.
That exchange happens over a set of conventions called HTTP, and the conventions are
fiddly. The message arrives as raw bytes. Something must find where the message ends, split
out the address from the headers, work out that 42 in /items/42 is meant to be a number
rather than the text “42”, find whichever piece of your code is supposed to answer, run it,
take whatever comes back, turn it into bytes again, and add the right headers so the browser
understands what it received.
A web framework is the thing that does all of that so you can write only the piece in the
middle — the part where you decide what /items/42 should actually say.
The piece you write is called a handler: a function that receives a request and returns an answer. The framework’s job is everything on either side of it.
The hardware store#
Think of a hardware store.
Some tools do one job and do it superbly: a good screwdriver. Some are a whole kit in a case: a cordless drill with fourteen bits, a charger, a level and a stud finder. Neither is better. The screwdriver is better when you need a screwdriver, and the kit is better when you do not yet know which of the fourteen things you will need.
Python’s web frameworks sit along exactly that range, and the disagreement between them is almost entirely about how much comes in the box:
- Django is the kit in the case. It brings a way to store data, a way to change the shape of that stored data safely over time, a ready-made administration screen for editing it, user accounts, passwords, permissions, page templates, translations. You did not ask for most of it and it is all there.
- Flask is the good screwdriver. Routing, templates, and stop. Everything else is a separate purchase from a large aisle of add-ons.
- Falcon and Bottle are smaller still, by design.
- FastAPI is a specialist tool that became the default: it does one thing, described below, better than anything else.
Choosing between them is not choosing a better tool. It is choosing how much you want decided for you, and each choice has a cost paid at a different time.
Waiting#
This is the deepest split in the category, and every other difference sits downstream of it.
Most of what a web server does is wait. It asks a database for a row and waits. It calls another company’s service and waits. The waiting is not milliseconds of thinking; it is often hundreds of milliseconds of nothing at all.
There are two ways to arrange a program around that waiting, and they are the fault line under every framework here.
The old way: one worker per request. The server keeps a pool of workers — think of them as staffed counters. A request arrives, a worker takes it, and that worker is occupied until the answer goes back out. If the handler spends 200 ms waiting for a database, the worker stands there for 200 ms doing nothing. Ten counters means ten customers at once, no matter how much of that time is spent waiting.
The convention for this arrangement is called WSGI (say “wiz-ghee”). Flask, Django, Bottle and Pyramid are built on it.
The newer way: one worker, many conversations. Instead of standing idle, the worker starts a request, reaches a point where it must wait, sets it aside, and picks up the next one. When the database answers, it comes back to the first. One worker can hold hundreds of half-finished requests as long as most of them are waiting rather than thinking.
The convention for this is ASGI. FastAPI, Starlette, Litestar and Quart are built on it.
In Python the code is marked with the words async and await — await is the programmer
saying “this is a good place to set me aside.”
Neither is better in general. If your handler waits a lot, the second arrangement serves far more people with the same hardware. If your handler does actual computation and never waits, setting it aside achieves nothing and costs a little.
Async words without async behavior#
Flask lets you write async and await. It looks like you have the second arrangement.
You do not. Flask sits on WSGI, so the worker is still occupied for the whole request. The
await runs, and the counter stays staffed by someone standing still. You get the
vocabulary of setting requests aside without the benefit of it — and the only way to find
out is to measure, because nothing errors.
This survey measured it, and the fix has a name: Quart, which is Flask’s own design rebuilt on ASGI by the same maintainers. Same words, same shapes, and requests really do get set aside.
There is a price, and this survey measured that too: Quart costs about twice as much per individual request as Flask. If your handlers wait, that is repaid thousands of times over. If they do not, you have paid double for nothing.
The forty-slot cloakroom#
One more piece of the same idea, because it is where working systems break.
Frameworks like FastAPI let you write a handler the old-fashioned way, without async. To
avoid one such handler jamming everything, the framework quietly hands it to a side pool of
threads — think of a cloakroom with a fixed number of hooks.
The pool holds 40. Forty of those handlers can be in progress at once. The forty-first waits for a hook to free up, and every request in that group waits with it.
Forty sounds like a lot, and the catch is what it counts: requests in progress, not requests per second. If each one waits a full second, forty in progress is forty requests per second — a rate that does not look busy at all. The symptom is a service that is suddenly slow for no visible reason.
Where the other pieces live#
Three things are commonly confused with the framework and are not it:
- The server. Something must own the actual network connection and speak HTTP to the outside world. That is a separate program — Uvicorn, Gunicorn, Granian — and a separate decision. The framework begins where the server hands off. (A few frameworks, like aiohttp and Tornado, bring their own and make that decision for you.)
- The database layer. Storing and querying data is its own tool. Django brings one; most others let you choose.
- The front end. What the browser draws — the buttons and the layout — is a different world with different tools.
Two words#
Validation. A request arrives as text. Someone must check that the thing labeled a price really is a number, that a required field is present, that a string is not four thousand characters long. Doing this by hand in every handler is tedious and easy to get wrong.
FastAPI’s central idea is that you describe the shape you expect, once, and the framework enforces it — rejecting anything that does not fit before your code runs. The common worry is that all this checking must be slow. This survey measured it, and it is not: checking a message and reading it turned out to be faster than reading it without checking, because the checking library reads it in one careful pass instead of two sloppy ones.
OpenAPI. If other people’s programs call your service, they need a description of what it accepts and returns. Written by hand, that description drifts out of date the first time someone changes the code and forgets. Because FastAPI already knows the shape of everything, it writes that description itself, from the code, so it cannot disagree with reality. That single property explains most of FastAPI’s popularity.
What to take away#
- The frameworks differ mainly in how much comes in the box. Django brings everything; Falcon and Bottle bring almost nothing; the rest sit between.
- The technical split that matters is how they handle waiting — one worker per request (WSGI) versus one worker holding many (ASGI).
- Writing
asyncin a WSGI framework does not make it ASGI. The words work; the worker still stands still. - Speed is rarely what decides. The slowest and fastest frameworks measured here differ by about 78 millionths of a second per request. A single database query typically takes fifty to a hundred times that. What usually decides is whether an administration screen comes free, whether a machine-readable description gets written for you, and how many people you can ask when you are stuck.
S1: Rapid Discovery
aiohttp (server)#
What it is#
aiohttp is an asyncio HTTP client and server. This survey judges the server half only; the client half belongs to survey 1.067.1, Python HTTP Clients, where it is one of the principal candidates.
aiohttp.web is a complete framework: routing, request and response objects, middleware,
WebSockets, streaming, application lifecycle signals, and a production-capable server of
its own. It predates ASGI and does not implement it.
The shape of the opinion#
aiohttp’s opinion is that HTTP is one problem and a library should handle both ends of it. The client and server share connection handling, streaming primitives, multipart parsing and the same async idioms, so a service that both serves and calls HTTP uses one mental model and one dependency.
For proxies, gateways, scrapers, webhook relays and anything else whose main work is receiving a request and making several more, that unification is a genuine architectural advantage and the main reason to choose aiohttp over an ASGI framework.
What it refuses to decide: validation, serialization, schema generation, storage — as minimal as Starlette in that respect, with a much larger streaming and connection surface.
Async posture#
asyncio-native, and not ASGI. This is the important structural fact. aiohttp implements its own server and its own application protocol; it does not run on uvicorn, Hypercorn or Granian, and ASGI middleware does not apply to it.
The practical consequences:
- The 1.242 server decision does not exist for aiohttp. It brings its own.
- The ASGI middleware ecosystem — the Starlette middleware everything else shares — is unavailable. aiohttp middleware is its own kind.
- Tooling that assumes an ASGI application object does not attach.
This is not a deficiency; it is a different lineage. aiohttp was serving asyncio HTTP before ASGI was specified. But a team that assumes “async Python web framework” implies “ASGI” will be wrong about aiohttp in ways that surface late.
What comes in the box#
| Routing, application object, middleware | Yes |
| Its own HTTP server | Yes — no separate server needed |
| WebSockets (client and server) | Yes |
| Streaming request/response bodies | Yes — unusually strong |
Multipart, cookies, sessions (via aiohttp-session) | Partly first-party |
Jinja2 templating (aiohttp-jinja2) | Companion package |
| HTTP client | Yes — the other half of the library |
| Validation, OpenAPI, ORM, admin | No |
| ASGI compatibility | No |
Measured position#
648 million installs a month — second-highest in this survey, behind Starlette and ahead of FastAPI. 16,500 stars. Version 3.14.3, released 2026-07-23, from 312 releases since 2013-10-25. Python ≥3.10.
The download figure overstates the server enormously. aiohttp is installed overwhelmingly
as a client — it is a dependency of a very large number of SDKs and libraries that
never serve a request. Nothing in the registry data separates the two uses, so the
correct reading is: this is one of the most-installed packages in Python, and no
conclusion about the popularity of aiohttp.web follows from that.
The license is unusual and worth checking against policy: Apache-2.0 AND MIT, a
compound expression, not a choice between them. It is the only compound license in this
survey.
Trade-offs#
You get one library for both ends of HTTP, excellent streaming, a mature and heavily exercised asyncio implementation with thirteen years of production hours, and no separate server to choose, configure or deploy.
You give up the entire ASGI ecosystem — middleware, servers, and the tooling that targets ASGI applications. You give up validation, OpenAPI and dependency injection, with no first-party equivalent and no dominant third-party one. You give up the ability to migrate cheaply: moving from aiohttp to an ASGI framework later is a rewrite of the application’s edges, not a configuration change.
Where it is weak: as a general-purpose API framework in 2026. Where the work is serving a documented API to clients, an ASGI framework gives more and costs less. aiohttp’s server earns its place where the service is itself an HTTP client of consequence — and that is a narrower brief than its download count suggests.
Also in the category#
Five frameworks that are part of the field without being live candidates for most new work. Each is here for a reason stated below; none is padding, and one of them — Django Ninja — matters more than its download count suggests.
Django Ninja#
What it is. A FastAPI-shaped API layer that runs inside a Django project. Typed handlers, Pydantic validation, generated OpenAPI, and full access to the Django ORM, auth, admin and migrations underneath.
Why it matters. It is the mechanism by which Django answers FastAPI, and it makes a choice that otherwise looks binary into something else. A team that wants Django’s data layer and admin and a modern typed API does not have to run two services or give one up. Django REST Framework is the older answer to the same question, is far more widely deployed, and is class-and-serialiser shaped rather than annotation shaped — Django Ninja is the one that reads like FastAPI.
Measured. 9,200 stars, 3.4 million installs a month, MIT, version 1.6.3 released 2026-08-24 from 81 releases since 2019-12-04. Python ≥3.7 — the most permissive floor in the survey, which sits oddly against the Django 6.x it plugs into requiring 3.12.
The trade. You are still in Django: WSGI-first, the ORM’s async limits apply, and throughput is Django’s. You get typed handlers and generated documentation without leaving the ecosystem.
Pyramid#
What it is. The surviving framework of the Pylons project, designed around the idea that an application should be able to start small and grow without being rewritten. Configuration is explicit, almost everything is pluggable, and it supports both URL routing and traversal — hierarchical dispatch over an object graph, which no other framework here offers.
Why it matters. Traversal is distinctive and niche: for content-management-shaped problems where the URL structure mirrors a tree of objects, it is a better fit than routing tables. Pyramid is also the clearest example in Python of “configurable rather than opinionated” carried to its conclusion.
Measured. 4,100 stars, 2.2 million installs a month. Version 2.1 released 2026-03-11,
from 157 releases since 2010-11-05. License is LicenseRef-Repoze-BSD-derived — a
BSD-derived license with an additional clause, not plain BSD, and the only non-standard
license in this survey. Policy-sensitive readers should read it rather than assume.
The trade. Flexibility bought with configuration. Pyramid asks decisions of you that Django answers and Flask lets you defer, which was a stronger pitch when the alternatives were less complete.
Bottle#
What it is. A WSGI micro-framework distributed as a single file with no dependencies — routing, templating, request/response and a development server in one module.
Why it matters. The single-file property is a real capability, not a novelty: it can be vendored into a project, dropped onto a device, or shipped where a package index is not reachable. Nothing else in this survey can be deployed by copying one file.
Measured. 8,800 stars, 10 million installs a month — seventh here, ahead of Quart. MIT. No declared Python floor. Version 0.13.4, released 2025-06-15 — fourteen months before measurement, and the only project in this survey without a release in over a year.
The trade. That release gap is the finding. Bottle is not dead — 10 million installs a month and a repository pushed to in July 2026 — but it is the one project here whose maintenance cadence has visibly slowed, and it is at 0.13 after seventeen years. S4 treats it as the survey’s clearest decay signal.
BlackSheep#
What it is. An ASGI framework with typed handlers, dependency injection, generated OpenAPI and first-class authentication and authorisation strategies. Architecturally it is FastAPI’s nearest neighbour, with a design borrowed visibly from ASP.NET Core.
Why it matters. Its auth model is more complete than FastAPI’s out of the box — authentication and authorisation as configured strategies rather than as dependencies you assemble. For a reader who finds FastAPI’s auth story thin, this is where the category’s better answer currently lives.
Measured. 2,400 stars and 43,000 installs a month — the second-lowest here. MIT, Python ≥3.10, version 2.6.3 released 2026-06-04 from 108 releases since 2018-11-24. Eleven open issues.
The trade. Seven years of steady releases and almost no adoption. The code is not the problem; the absence of a community around it is. Choosing BlackSheep means being on your own with it.
Robyn#
What it is. A Python web framework with a Rust runtime. The routing and request handling are implemented in Rust and exposed to Python; handlers are ordinary Python functions, sync or async.
Why it matters. It is the most-watched instance of a real trend — Rust reimplementation of Python infrastructure — and the trend is worth tracking whether or not this project is the one that arrives.
Measured. 7,400 stars against 19,000 installs a month, the lowest in this survey.
That is roughly 390 stars per monthly install, the most extreme attention-to-use ratio here by an
order of magnitude, and observed-data.md uses it as the illustration of what a star
measures. BSD-2-Clause. No declared Python floor. Version 0.88.0 released 2026-06-25,
from 162 releases since 2021-06-16.
The trade. A Rust extension module means platform wheels: your deployment target must have one or a toolchain to build it, which is a constraint none of the pure-Python frameworks impose. At 0.88.0 after five years, and with an installed base four orders of magnitude below FastAPI’s, it is a project to watch rather than to ship. S4 states that as a viability position rather than a preference.
What is compared here#
Fifteen frameworks are compared. Each file states the same six things in the same order — what it is, what it decides for you, its async posture, what ships with it, its measured position, and what you give up — so they can be read against each other.
What counts as a web framework#
A Python web framework, for this survey, is a library that:
- Receives HTTP requests and dispatches them to Python functions you wrote. Routing is the irreducible job. Everything else — templating, ORM, validation, auth — is a framework’s opinion about what else belongs, and the range of those opinions is most of what separates these projects.
- Is installed with
pipand imported. A hosted platform is not a framework; neither is a code generator that emits one. - Is not the server. The thing that owns the socket, forks workers and speaks HTTP/1.1 to the network is a separate decision with a separate set of candidates, and it is survey 1.242, ASGI & WSGI Application Servers. Uvicorn, Gunicorn, Hypercorn and Granian are out of scope here. This survey stops at the application object; 1.242 starts there.
The split at (3) is the boundary test for every borderline case below. When a project does both jobs, S1 judges only the framework half and says so.
Boundary cases#
Three projects sit on the line, and each is included for a stated reason:
- Starlette is a toolkit that FastAPI is built on, and is also a complete framework in its own right — people ship Starlette applications with no FastAPI. In. Its double life is the single most useful thing to understand in this category.
- aiohttp is predominantly an HTTP client (that is survey 1.067.1) and its server half is a real, widely deployed framework. In, judged on the server half only.
- Tornado is a framework and a server and an event loop, predating asyncio and having partly become it. In, judged on the framework half, with the overlap named.
Sanic is likewise a framework bundled with its own server; it is judged as a framework and its server is noted where it changes the framework decision.
Why these fifteen#
The set was assembled from three sources and then filtered:
- The dependency evidence.
record_priorities.pytraced sixteen real application manifests and found FastAPI in four, Flask in two, Starlette in one. Those are in by demonstrated use. - The PyPI download tail. Every general-purpose Python web framework above roughly 1M installs a month, which is where the category’s long tail stops being a category and starts being someone’s personal project.
- The named alternatives. Projects that appear in the others’ own comparison documentation — Litestar, Falcon, BlackSheep, Robyn — regardless of download count, because a framework the incumbents feel obliged to answer is part of the field.
Excluded: Django REST Framework and Flask-RESTX are extensions to a framework already here, not frameworks; Zope and web2py are below the traffic floor and no longer appear in anyone’s comparison; Reflex, NiceGUI and Streamlit generate a front end and own the request loop, which makes them a different category (closer to 1.110 Frontend Frameworks than to this one).
Django Ninja is the one extension that is included, because it is the mechanism by which the Django ecosystem answers FastAPI, and leaving it out would make Django look absent from a comparison it is actually competing in.
How each was assessed#
Every S1 file states the same six things, in the same order, so they can be read against each other:
- What it is — one paragraph, no marketing adjectives.
- The shape of the opinion — what it decides for you, what it refuses to decide.
- Async posture — WSGI, ASGI, both, or its own thing. This is the deepest architectural fault line in the category and it is frequently misstated.
- What comes in the box — ORM, templating, validation, admin, auth, forms.
- Measured position — downloads, stars, cadence, license, Python floor, from
observed-data.md. No number appears that is not in that table. - Trade-offs — what you give up by choosing it, stated as plainly as what you get.
Out of scope#
- No installation instructions.
pip install flaskis not research. - No tutorials. A routing example appears only where the shape of the API is the point being made, and never as something to copy.
- No benchmarks. Framework throughput numbers are the most contaminated data in this category — almost all of them trace to TechEmpower runs whose configuration differs from any real deployment, and vendor blog posts that reproduce them selectively. S2 addresses performance properly, with its own measurements and stated conditions.
- No verdict for a particular job. Category-first (RAIL 0): each framework is judged on the category’s merits. The applications that triggered this survey are one S3 persona, not an S1 finding.
Django#
What it is#
Django is a full-stack web framework: ORM, migrations, templating, forms, authentication, sessions, an automatically generated administrative interface, internationalisation, caching, and a project layout that assumes all of them. It is the only project in this survey that answers “what else does a web application need” with a complete list rather than an extension registry.
It is sixteen years old, is maintained by a foundation rather than an individual or a company, and is the framework most likely to be underneath a large Python web application that people are paid to work on full-time.
The shape of the opinion#
Django’s opinion is that the framework should supply everything a web application routinely needs, so that teams stop rebuilding it. The consequences run deep:
- The ORM is not optional in practice. Migrations, the admin, the auth system, forms and the permission framework are all built on Django models. You can use Django with a different data layer, and you give up most of the framework by doing it.
- The admin is a genuine differentiator, not a demo. A working CRUD interface over your models, with permissions, search and filtering, derived from model definitions, is a thing no other framework in this survey ships. For internal tools it is frequently the entire reason Django is chosen.
- The layout is prescribed. Apps, models, views, URLs, settings. Two Django projects by unrelated teams are navigable by each other’s developers, which is a real organizational property and the exact opposite of Flask’s.
What Django refuses to decide: very little. That is the point, and the cost.
Async posture#
WSGI-first with a genuine, incomplete ASGI path. Django has supported ASGI since 3.0
and async views since 3.1, and the coverage has grown steadily — async views, async
middleware, async ORM query methods (acreate, aget, afilter), async test clients.
The summary is that Django’s async story is real but partial, and the gaps are where the framework is deepest. The ORM’s async methods wrap synchronous execution in a thread rather than speaking to the database over an async driver, so async ORM calls do not deliver connection-level concurrency. Substantial parts of the ecosystem — much of the admin, many third-party packages — remain synchronous. Django’s async is best understood as letting async code coexist with a synchronous framework, not as an async-native architecture.
For a workload dominated by concurrent outbound I/O, this is the decisive limitation. For the database-backed applications Django is designed for, it usually is not.
What comes in the box#
| Routing, WSGI + ASGI app | Yes |
| ORM + migrations | Yes — and central to everything else |
| Admin interface | Yes — generated from models |
| Auth: users, permissions, groups, password hashing | Yes |
| Forms + validation | Yes |
| Templating | Yes (Django templates; Jinja2 supported) |
| Sessions, caching, i18n/l10n, CSRF, security middleware | Yes |
| Management commands, system checks, test framework | Yes |
| OpenAPI generation | No — Django REST Framework or Django Ninja |
Measured position#
89,000 stars. 53 million installs a month — the sixth-highest here, below Flask and roughly a tenth of FastAPI’s. BSD-3-Clause. Version 6.1, released 2026-08-05, from 441 releases since 2010-05-17 — the most releases of any project in the survey.
The download ranking needs the discount observed-data.md sets out: Django is installed to build a deployed application, while FastAPI and
Starlette are additionally installed as transitive dependencies of tooling and SDKs.
Counting installs counts the latter many times over. Django’s 479 open issues and its
441-release history describe a project with more sustained activity than any other here.
Python ≥3.12 is the most aggressive floor in the survey, well ahead of Flask’s 3.9 and Django Ninja’s 3.7. Django drops old Python versions on a published schedule; this is a maintenance policy, not neglect, but it does mean Django 6.x is unavailable to anyone pinned below 3.12.
Trade-offs#
You get the deepest batteries-included stack in Python, an admin interface that routinely eliminates weeks of internal-tool work, a security posture maintained by people who take it seriously and publish advisories, a documented long-term-support release cadence, and the largest professional hiring pool of any framework here.
You give up the ability to take only the parts you want — the framework is coherent because its pieces assume each other, and using Django without its ORM is using a different, worse framework. You give up async-native concurrency. You give up small: a Django project has a floor of structure below which it will not go, and for a six-endpoint service that floor is the whole application.
Where it is weak: small services, API-only backends where the admin and templating are dead weight, and I/O-concurrency-bound workloads. The first two are what Django Ninja exists to address.
Falcon#
What it is#
Falcon is a minimal framework for HTTP APIs, aimed at what its own documentation calls data-plane services — the ones that sit in the request path of other systems and are judged on latency and reliability rather than developer convenience.
It supports WSGI and ASGI from one codebase, has no required dependencies, and is explicit about doing less than its competitors on purpose.
The shape of the opinion#
Falcon’s opinion is that a framework in the data plane should be predictable and
small. It does not do magic. There is no dependency injection, no automatic validation,
no schema generation from annotations. Resources are classes with on_get, on_post
methods; the request and response objects are passed in and mutated.
Its stated design goals are a small dependency footprint, no reflection or metaclass machinery in the request path, and stable behavior under load. In a category where most projects compete on how much they can infer from your code, Falcon competes on inferring nothing.
Async posture#
Both WSGI and ASGI, from a single codebase — the only framework here that does this
properly. falcon.App is WSGI, falcon.asgi.App is ASGI, and the routing, middleware
and resource model are shared. Flask is WSGI with an async escape hatch; Django is
WSGI-first with a partial ASGI path; Falcon supports both as peers.
For a team maintaining services in both worlds, that is a real and unusual property.
What comes in the box#
| Routing, resource classes, middleware | Yes |
| WSGI and ASGI applications | Yes — both first-class |
| Media handlers (JSON, msgpack, multipart) | Yes |
| WebSockets, SSE (ASGI) | Yes |
| Testing helpers | Yes |
| Zero required dependencies | Yes — unusual |
| Validation, OpenAPI, DI, ORM, admin, templating | No — by design |
Measured position#
9,800 stars — more than Litestar’s — against 1.6 million installs a month. Apache-2.0. Python ≥3.9. Version 4.3.1, released 2026-06-16, from 102 releases since 2013-01-22.
Thirteen years of releases and a current one; this is a stable, maintained project with a small and durable constituency rather than a growing one.
Zero required dependencies is a genuine differentiator in an environment where supply-chain surface is a live concern. FastAPI pulls Starlette and Pydantic and their trees; Falcon pulls nothing.
Trade-offs#
You get predictability, a tiny dependency surface, WSGI/ASGI duality, and a framework that will behave the same under load as it did in testing.
You give up everything modern API frameworks generate for you. Validation, schemas, documentation and injection are yours to build or to import separately. On an API of any size this is a substantial amount of work that FastAPI or Litestar would have done.
Where it is weak: developer velocity on a conventional CRUD API, which is most APIs. Falcon’s trade is deliberate and it is not the right trade for the common case — which is exactly what its maintainers say.
FastAPI#
What it is#
FastAPI is a framework for building HTTP APIs in which the type annotations on your
function signature are the specification. You declare a handler that takes an int
and a Pydantic model; FastAPI derives the request parsing, the validation, the error
responses, and the OpenAPI document from that declaration alone. Nothing is registered
twice.
It is not a full-stack framework and does not try to be. There is no ORM, no admin, no form library, no session middleware, no user model. It is an API layer, and everything underneath it is your choice.
The shape of the opinion#
FastAPI’s opinion is narrow and deep: the signature is the contract. Almost every distinctive feature follows from committing to that one idea harder than anyone else in the category.
Its dependency-injection system is the clearest expression. A dependency is an ordinary function whose own parameters are resolved the same way a handler’s are, recursively, so authentication, database sessions and pagination arrive as typed arguments rather than as globals or decorators. Sub-dependencies are cached per request. It is the most substantial idea FastAPI contributes to the category, and it is more copied than credited — Litestar and BlackSheep both ship a version of it.
What FastAPI refuses to decide: storage, migrations, templating, background job processing, and the identity of the server that runs it.
Async posture#
ASGI, with a synchronous escape hatch that matters more than the documentation
suggests. FastAPI is built on Starlette and is async-native. A handler declared
async def runs on the event loop. A handler declared def is run in a thread pool
instead, so blocking code does not stall the loop.
The fallback cuts both ways. It lets a synchronous database driver work correctly without rewriting the
application. It also means a def handler is capped by the thread pool’s size, so the
performance the framework is known for does not automatically apply to code that opted
out of async. Neither the benefit nor the ceiling is prominent in the framework’s own
materials.
What comes in the box#
| Routing, ASGI app | Yes |
| Request validation | Yes — Pydantic, mandatory, not optional |
| Response serialization | Yes, from the return annotation |
| OpenAPI + Swagger UI + ReDoc | Yes, generated, served by default |
| Dependency injection | Yes — the distinguishing feature |
| Auth primitives | Yes: OAuth2 flows, API keys, HTTP Basic as dependencies |
| WebSockets, background tasks, sessions | Inherited from Starlette |
| ORM, migrations, admin, forms, templating | No |
The auth line is worth reading closely: FastAPI ships the plumbing for OAuth2 — the flows, the scopes, the token extraction — and no user store, no password handling and no session management. It gives you the shape of authentication, not authentication.
Measured position#
102,000 stars — more than Django and Flask, and it is eight years old against their sixteen. 591 million installs a month, third in the category behind two libraries it depends on or competes with sideways. MIT. Python ≥3.10. Version 0.141.1, released 2026-07-29, from 317 releases since 2018-12-08.
That version number deserves attention. FastAPI has been at 0.x for eight years and
317 releases while Starlette, the library it is built on, reached 1.0 and is now at
1.6.0. The dependency committed to a stability guarantee before the dependent did. In
practice FastAPI’s minor versions have carried breaking changes, and the 0. prefix is
the notice that they may.
Trade-offs#
You get the strongest editor and type-checker experience in the category; generated API documentation that cannot drift from the implementation because it is derived from it; a dependency system that makes testing easier; and the largest community of any modern Python API framework by a wide margin.
You give up Pydantic-optionality — validation is not a component you can swap, it is the framework’s substrate, and a Pydantic major-version migration is a FastAPI migration. You give up server-rendered HTML as a first-class concern; templating works but nothing about the framework is arranged for it. You give up the 0.x stability promise. And you take on the unbundled cost: every full-stack concern — users, admin, migrations, sessions — is a library you select, integrate and maintain yourself, which is the right trade for an API and the wrong one for a content site.
Where it is weak: long-lived synchronous workloads under def handlers,
where the thread pool becomes the bottleneck and the framework’s reputation for speed
actively misleads.
Flask#
What it is#
Flask is a WSGI framework that provides routing, request and response objects, a templating integration, and a context system — and stops. It was written as a deliberate minimum: the parts of a web application that must be shared, with everything else left to extensions.
Sixteen years on it is still the framework most Python developers learn first, and the one most likely to be underneath a small internal service that has been running untouched for years.
The shape of the opinion#
Flask’s opinion is that a framework should be a small kernel with a large ecosystem. It decides almost nothing for you. There is no ORM, no admin, no validation layer, no project layout, no user model. What there is instead is a stable extension interface that has kept working long enough for a deep bench of extensions to accumulate: Flask-SQLAlchemy, Flask-Login, Flask-Migrate, Flask-WTF, Flask-Admin.
The cost of that design is the thing Flask is most criticised for and it is a real cost: your architecture is your own problem. Two Flask applications by two teams may share no structural conventions at all. Django tells you where things go; Flask does not, and at scale that is a coordination cost paid in code review rather than in framework configuration.
The famous parts of Flask’s design — the application and request context, g,
current_app, the thread-local proxies — exist to let handlers reach shared state
without threading it through every call. They are also the part newcomers get wrong most
often, because a proxy that works inside a request and raises outside one is a category
of bug that does not exist in frameworks that pass state explicitly.
Async posture#
WSGI, with async handlers supported and a caveat that is easy to miss. Since 2.0,
Flask accepts async def handlers. It runs each one in a fresh event loop inside the
worker thread that WSGI handed it.
That is real async support in the sense that await works. It is not async in the sense
that matters for concurrency: because the surrounding server is WSGI, each request still
occupies a worker for its whole life, so an async def Flask handler awaiting a slow
call does not free the worker to serve another request. You get the syntax without
the throughput. For concurrent I/O under a Flask-shaped API, the answer is
Quart, which is Flask’s own ASGI reimplementation and is covered in this survey.
This distinction — async syntax versus async concurrency — decides more than anything else in the category, and Flask is where it usually surfaces, because Flask is where the syntax is available and the benefit is not.
What comes in the box#
| Routing, WSGI app | Yes |
| Templating (Jinja2) | Yes — a hard dependency, not an option |
| Request/response objects, sessions | Yes (signed-cookie sessions) |
| CLI scaffolding, blueprints | Yes |
| Development server, debugger | Yes |
| Validation, ORM, migrations, admin, auth | No — extensions |
| OpenAPI generation | No — extensions, and none is canonical |
The last row is where Flask has visibly lost ground. FastAPI’s generated, always-correct API documentation has no Flask equivalent that the community has settled on; the candidates are several, and choosing among them is a decision Flask makes you make.
Measured position#
72,000 stars, 200 million installs a month — fourth in the category and roughly a third of FastAPI’s. BSD-3-Clause. Python ≥3.9, the second-most permissive floor here. Version 3.1.3, released 2026-02-19, from just 64 releases since 2010-04-16.
Two of those figures are worth reading together. Sixty-four releases in sixteen years is about four a year, an order of magnitude slower than FastAPI’s 317 in eight — and 3 open issues on GitHub, against Django’s 479 and Litestar’s 319. That is not abandonment; the repository was pushed to twelve days before measurement. It is a finished project under maintenance, with a maintainer team that closes things. Whether that reads as stability or as stagnation is a genuine judgment, and S4 takes it up.
Trade-offs#
You get the largest body of existing knowledge in the category — the tutorials, the Stack Overflow answers, the extensions, and the enormous number of developers who already know it. You get a framework small enough to hold in your head. You get sixteen years of evidence that the API does not churn.
You give up built-in async concurrency, which for an I/O-bound service is the whole game. You give up generated API documentation. You give up architectural guidance, which is a gift on a small project and a liability on a large team. And you inherit the extension ecosystem’s variance: Flask itself is meticulously maintained, and the extension you depend on may not be.
Where it is weak: high-concurrency I/O-bound APIs, and any project where a machine-readable API contract is a requirement rather than a nicety.
Litestar#
What it is#
Litestar is an ASGI API framework that occupies FastAPI’s territory and argues about how it should be organized. It offers typed handlers, generated OpenAPI, and dependency injection — and adds data-layer integration, a plugin system, and a class-based controller model that FastAPI does not have.
It began in 2021 as Starlite, a project built on Starlette, and was renamed Litestar in 2023 after removing that dependency. It is the youngest project in this survey by five years.
The shape of the opinion#
Litestar’s opinion is that FastAPI is right about types and wrong about structure. Three differences carry most of the argument:
- Layered configuration. Dependencies, guards, middleware, serialization settings and exception handlers can be declared at the application, router, controller or handler level, and resolve down the hierarchy. FastAPI’s equivalents are flatter and more repetitive on a large surface.
- Controllers as classes. Routes group into controller classes with shared dependencies and configuration. On a large API this is real organization; on a small one it is ceremony.
- Validation is not tied to one library. Litestar supports Pydantic, attrs and
msgspec, and its own
DTOsystem governs what is exposed on the wire independently of the model. FastAPI has one answer, and it is Pydantic.
The DTO layer is Litestar’s most substantive original contribution. Declaring that a
model’s password_hash is never serialized and its id is never accepted on input — as
a property of the DTO rather than by maintaining a parallel response model — addresses a
real and tedious source of bugs.
Async posture#
ASGI-native, with the same synchronous-handler-to-threadpool escape that FastAPI offers. No WSGI legacy. Since Litestar 2.0 it has no Starlette dependency; the ASGI handling is its own.
That independence cuts both ways. It removes a layer and gives the maintainers control over the whole stack; it also means Litestar does not inherit fixes from the most widely-deployed ASGI toolkit in Python, and its ASGI implementation has far fewer production hours behind it.
What comes in the box#
| Routing, ASGI app, controllers | Yes |
| Validation | Yes — Pydantic or attrs or msgspec |
| DTOs (wire shape decoupled from model) | Yes — distinctive |
| OpenAPI + several doc UIs | Yes |
| Dependency injection, layered | Yes |
| SQLAlchemy integration, repository pattern | Yes — first-party |
| Auth primitives (JWT, sessions), guards | Yes |
| Background tasks, WebSockets, channels, events | Yes |
| Response caching, rate limiting, HTMX support | Yes |
| ORM of its own, admin | No |
Litestar ships more out of the box than FastAPI — the SQLAlchemy repository layer, channels, rate limiting and caching are all first-party — which is its pitch and its risk. Every one of those is a component the core team must keep working.
Measured position#
8,400 stars, 2.1 million installs a month. MIT. Python ≥3.8, <4.0. Version 2.24.0,
released 2026-06-11, from 65 releases since 2023-04-01.
The scale gap is the finding. Litestar has 1/12 of FastAPI’s stars and 1/280 of its monthly installs. Against a category where the incumbent has 102,000 stars, technical merit is not the binding constraint on adoption.
319 open issues against 8,400 stars is the highest issue-to-star ratio in this survey — roughly 8× FastAPI’s. That is ambiguous evidence: it is consistent with a project whose surface area has outgrown its maintainer capacity, and equally with an actively triaged tracker on a young project that has not yet closed its backlog. S4 resolves it against contributor and release data rather than guessing here.
The rename is a real cost. Material written before 2023 refers to Starlite, the package name changed, and search results are split across two names. Anyone evaluating Litestar on what they can find written about it is reading a corpus with a discontinuity in the middle.
Trade-offs#
You get better structure for a large API than FastAPI offers, a validation layer that is not a single-library commitment, DTOs that solve a genuine problem, and more included batteries than any other ASGI framework here.
You give up ecosystem. The community is a fraction of FastAPI’s, which means fewer answered questions, fewer third-party integrations, fewer developers who already know it, and — the one that matters most in 2026 — substantially less training data behind the coding assistants your team uses. You give up Starlette’s production hours. You accept a larger first-party surface maintained by a smaller team.
Where it is weak: as a choice you have to defend to a team that has not heard of it. The technical case is strong; the institutional case is the hard part, and pretending otherwise does the reader no favors.
Observed data#
Every number in S1 comes from this table. It was measured, not recalled.
Measured: 2026-08-28
Sources: PyPI JSON API (https://pypi.org/pypi/<pkg>/json) for version, license,
Python floor and release dates; pypistats.org /api/packages/<pkg>/recent for
last-month downloads; GitHub REST repos/<owner>/<repo> for stars, forks, open issues
and last push.
| framework | version | downloads/mo | stars | license | Python floor | last release | first release |
|---|---|---|---|---|---|---|---|
| Starlette | 1.6.0 | 675,837,616 | 12,578 | BSD-3-Clause | ≥3.10 | 2026-08-08 | 2018-06-25 |
| aiohttp | 3.14.3 | 647,872,473 | 16,531 | Apache-2.0 AND MIT | ≥3.10 | 2026-07-23 | 2013-10-25 |
| FastAPI | 0.141.1 | 590,641,409 | 101,903 | MIT | ≥3.10 | 2026-07-29 | 2018-12-08 |
| Flask | 3.1.3 | 200,130,604 | 72,144 | BSD-3-Clause | ≥3.9 | 2026-02-19 | 2010-04-16 |
| Tornado | 6.5.8 | 118,493,611 | 22,179 | Apache-2.0 | ≥3.9 | 2026-08-07 | 2010-05-18 |
| Django | 6.1 | 53,161,848 | 89,028 | BSD-3-Clause | ≥3.12 | 2026-08-05 | 2010-05-17 |
| Bottle | 0.13.4 | 10,115,487 | 8,778 | MIT | none declared | 2025-06-15 | 2009-07-07 |
| Quart | 0.22.0 | 4,339,722 | 3,658 | MIT | ≥3.11 | 2026-08-19 | 2017-05-21 |
| Django Ninja | 1.6.3 | 3,375,145 | 9,178 | MIT | ≥3.7 | 2026-08-24 | 2019-12-04 |
| Pyramid | 2.1 | 2,215,995 | 4,095 | Repoze BSD-derived | ≥3.10 | 2026-03-11 | 2010-11-05 |
| Litestar | 2.24.0 | 2,101,697 | 8,426 | MIT | ≥3.8, <4 | 2026-06-11 | 2023-04-01 |
| Falcon | 4.3.1 | 1,648,360 | 9,795 | Apache-2.0 | ≥3.9 | 2026-06-16 | 2013-01-22 |
| Sanic | 25.12.1 | 1,579,480 | 18,646 | MIT | ≥3.10 | 2026-05-31 | 2016-10-15 |
| BlackSheep | 2.6.3 | 43,418 | 2,358 | MIT | ≥3.10 | 2026-06-04 | 2018-11-24 |
| Robyn | 0.88.0 | 19,004 | 7,382 | BSD-2-Clause | none declared | 2026-06-25 | 2021-06-16 |
On precision#
The table above records what was measured. The prose everywhere else rounds it.
A monthly download count is not a precise quantity, and a star count is precise only for as long as nobody clicks. It moves every day, it counts mirrors and CI runs alongside people, and it depends on where the month boundary falls. Quoting 675,837,616 to nine significant figures implies a stability the number does not have, so the surveys say “676 million” and the ratios derived from it are given to two figures.
The exact values stay here, with their date and their source, because that is what makes the measurement re-checkable. Rounding is for reading; the record is for verifying.
What these numbers do not mean#
Downloads count installs, not decisions. Starlette outranks FastAPI by 85 million installs a month, and nobody chose Starlette 85 million more times than they chose FastAPI. Every FastAPI install pulls Starlette in as a dependency, so Starlette’s total is at least FastAPI’s by construction, plus everything else that depends on it. The same distortion inflates aiohttp, which is a client far more often than it is a server. A download number is a position in the dependency graph. Read it as “how hard is this to avoid”, never as “how many people picked it”.
Stars count attention, and attention is not use. The two rankings disagree so violently that the disagreement is itself the finding:
| stars | downloads/mo | reading | |
|---|---|---|---|
| Robyn | 7,400 | 19,000 | starred roughly 390× more often than installed in a month |
| Starlette | 12,600 | 676 million | installed roughly 54,000× per star |
| FastAPI | 102,000 | 591 million | the only one high on both |
Robyn is not a failure and Starlette is not a phenomenon. Robyn is interesting — a Rust runtime under a Python API is worth a click — and Starlette is load-bearing, which nobody clicks a star for. A framework with high stars and low downloads has an audience that reads about it; a framework with the inverse has an audience that ships it.
Django’s number needs the most care. 53 million a month puts it below Flask and an order of magnitude below FastAPI, and that ranks reach, not popularity. Django is installed to build a deployed site; FastAPI and Starlette are additionally installed as transitive dependencies of tooling, SDKs, and each other. Counting installs counts the second kind twice and the first kind once.
Release cadence#
Only one framework here has gone more than a year without a release: Bottle, last published 2025-06-15, 14 months before this measurement. Every other project shipped within the last six months, and six shipped within the last month.
Age is not the same as decay. Bottle first published in 2009 and Pyramid in 2010, and both still release; Litestar first published in 2023 and is the youngest thing here by five years.
Quart#
What it is#
Quart is Flask’s API, reimplemented on ASGI. The routing decorators, blueprints, request
and application contexts, g, current_app, template rendering and configuration all
carry the same names and the same semantics as Flask’s, with handlers declared
async def and awaited properly.
Since 2022 it has been maintained by Pallets, the same organization that maintains Flask, Jinja, Werkzeug and Click. That is the fact that distinguishes it from every other “Flask but async” project: it is not a third-party reimplementation, it is Flask’s own.
The shape of the opinion#
Quart’s opinion is that the Flask API was right and only its concurrency model was
wrong. It changes as little as possible. A Flask application that does not depend on
WSGI-specific behavior or on synchronous-only extensions ports by adding async and
await at the handler boundary and changing the import.
That fidelity is the entire value proposition, and it is also the constraint: Quart inherits Flask’s design decisions wholesale, including the thread-local-style context proxies that are already the part of Flask people get wrong. Under async they are backed by context variables rather than thread locals, which fixes the correctness problem and does not make the model easier to reason about.
Async posture#
ASGI-native, and the answer to the Flask async trap. Where Flask runs an async def
handler in a private event loop inside a blocked WSGI worker — giving you await without
concurrency — Quart runs on the event loop for real. An awaiting handler yields to
others.
This is why the Flask/Quart pair matters more than either framework alone: it is the
cleanest demonstration in the category that async syntax and async concurrency are
different things. A team that added async def to Flask handlers and saw no throughput
change has met the distinction; Quart is where it is resolved.
Quart additionally supports WebSockets and HTTP/2, neither of which Flask can express.
What comes in the box#
| Routing, ASGI app, blueprints | Yes — Flask’s API |
Templating (Jinja2), contexts, g | Yes — Flask’s semantics |
| Signed-cookie sessions, CLI | Yes |
| WebSockets | Yes — beyond Flask |
| HTTP/2, server push | Yes — beyond Flask |
| Validation, ORM, migrations, admin, OpenAPI | No — as Flask |
Flask extensions do not universally work. Some are async-compatible, some have Quart
forks (quart-auth, quart-cors), and some synchronous extensions work only through a
compatibility shim that reintroduces blocking. Ecosystem compatibility is per-extension,
which is the main practical friction in a port.
Measured position#
3,700 stars, 4.3 million installs a month. MIT. Python ≥3.11 — the second-most aggressive floor here, behind only Django’s 3.12. Version 0.22.0, released 2026-08-19, from 67 releases since 2017-05-21.
3,700 stars against Flask’s 72,000 is a ratio of about 1:20. Quart is not obscure — 4.3 million installs a month is real use — but the great majority of Flask users who need concurrency have not moved to it. The Pallets stewardship is the reason to expect it to persist regardless; a project maintained alongside Flask by Flask’s own maintainers has a different risk profile from a comparably-sized independent project.
It is still at 0.x after nine years, which places it with FastAPI in the group whose version number promises nothing.
Trade-offs#
You get genuine ASGI concurrency without learning a new framework, an escape route for an existing Flask codebase that does not require a rewrite, WebSockets and HTTP/2, and institutional backing from the organization that maintains Flask itself.
You give up the extension ecosystem’s guarantees — the reason to use Flask is often Flask-SQLAlchemy, Flask-Login and Flask-Admin, and “works with Quart” is a per-extension question with per-extension answers. You give up Flask’s community scale by a factor of twenty, which shows up in how much has been written about the problem you are having. You give up nothing in API design, because there is no new API design.
Where it is weak: as a greenfield choice. A new async API with no Flask code to preserve and no Flask-shaped team gets more from FastAPI or Litestar — generated OpenAPI, validation, dependency injection — none of which Quart offers. Quart’s case is strongest exactly where Flask is already installed.
S1 verdict#
S1 does not pick a framework. It establishes what the choices actually are, so that S2 can measure the differences that turned out to be real and S3 can match them to people. Five findings survive the pass.
1. Three architectures, not fifteen frameworks#
Almost every distinction that matters reduces to how a framework relates to the concurrency model:
| architecture | frameworks | what follows from it |
|---|---|---|
| WSGI | Flask, Django (primarily), Bottle, Pyramid | One worker per in-flight request. async def may be accepted without buying concurrency. |
| ASGI | FastAPI, Starlette, Litestar, Quart, BlackSheep, Falcon (asgi), Sanic | Runs on any ASGI server; shares a middleware ecosystem; the server is a separate decision (survey 1.242). |
| Own loop, own server | aiohttp, Tornado, Robyn | Brings its own server. The 1.242 decision does not exist. ASGI middleware and tooling do not apply. |
The third row is the one most often missed. A team that has internalised “async Python web framework” as meaning “ASGI” will be wrong about aiohttp and Tornado in ways that only surface when they try to attach ASGI middleware or deploy under uvicorn.
Falcon is the only framework that treats WSGI and ASGI as genuine peers from one codebase.
2. Async syntax is not async concurrency#
Flask accepts async def handlers and runs each in a private event loop inside a blocked
WSGI worker. await works. Throughput does not improve, because the worker is still
occupied for the request’s whole life.
This is not a Flask defect — it is the consequence of WSGI — but it is the single most consequential misunderstanding in the category, and it has a specific resolution: Quart, Flask’s own API on ASGI, maintained by Pallets. Django’s async path is a milder version of the same shape: real, partial, and limited where the ORM is.
3. Download counts do not measure adoption#
The most-installed package here is Starlette, at 676 million a month, ahead of FastAPI’s 591 million — because every FastAPI install pulls Starlette. aiohttp’s 648 million is mostly client installs. Tornado’s 118 million is substantially Jupyter. And Django, the framework with the largest professional deployment base in Python, ranks sixth at 53 million.
A download number is a position in the dependency graph. The star counts disagree with it violently and measure something else again — attention, not use. Robyn has roughly 390 stars per monthly install; Starlette has roughly 54,000 installs per star.
No adoption claim in this survey rests on either number alone. S4 revisits this with contributor and release evidence.
4. FastAPI’s lead is ecosystem, not code#
102,000 stars — more than Django’s 89,000 and Flask’s 72,000, from a project half their age. Litestar has a defensible technical argument that FastAPI is right about types and wrong about structure, and it has 8,400 stars and 1/280 of FastAPI’s installs.
The S1 finding is that the strongest technical case in this category is not the one with the ecosystem, and that ecosystem — answered questions, third-party integrations, developers who already know it, and the training data behind coding assistants — is itself a decision input rather than a distraction from one. S3 and S4 weigh it; S1 records that the gap is not explained by merit.
5. Two version numbers#
FastAPI has been 0.x for eight years and 317 releases. Starlette, which it is built on, reached 1.0 and is now 1.6.0. The dependency committed to stability before the dependent did, and FastAPI’s minors have carried breaking changes.
Starlette also changed hands — the repository is now Kludex/starlette, transferred
from Encode (confirmed by parent: null on a repo created 2018-06-25, so a transfer and
not a fork). A foundational library moving from an organization to an individual
maintainer is a viability question, not a trivium, and S4 takes it up.
Open questions#
S1 leaves four claims explicitly unsettled, because they cannot be settled by reading:
- Throughput. Every framework here that claims speed cites contaminated sources. S2 measures, states its conditions, and reports what varies.
- The
def-handler ceiling. FastAPI’s thread-pool fallback is described everywhere and quantified nowhere. Where does it actually bind? - Django’s async ORM. Whether
afilterand friends deliver connection-level concurrency or wrap sync execution in a thread is checkable, and the answer changes Django’s position for I/O-bound work. - Litestar’s 319 open issues against 8,400 stars — the highest ratio here. Backlog or capacity? Contributor and close-rate data answers it; guessing does not.
What is not concluded here#
No framework is recommended here, and none is dismissed for being wrong for a particular job. Bottle’s fourteen-month release gap, BlackSheep’s 43,000 installs and Robyn’s platform-wheel requirement are recorded as category facts, and what they mean depends entirely on who is asking — which is S3.
Sanic#
What it is#
Sanic is an async web framework packaged with its own high-performance HTTP server, built for throughput. It provides routing, blueprints, middleware, WebSockets, streaming, background tasks, and a process manager that handles worker supervision and graceful reload.
Its distinguishing claim is speed, and unusually for that claim, the framework and the server are designed together rather than composed.
The shape of the opinion#
Sanic’s opinion is that the framework and the server should be one decision. Where FastAPI leaves the server to uvicorn or Granian, Sanic ships a server it controls and tunes them jointly. The 1.242 decision does not exist for a Sanic deployment.
The API sits close to Flask’s — decorator routing, blueprints — while being async-native throughout. It is the most familiar-looking of the fast frameworks.
Sanic also ships more operational machinery than its peers: a worker manager with multi-process supervision, shared state between workers, restart handling and a signals system. That is infrastructure other frameworks push to the server or the process manager.
Async posture#
asyncio-native with its own server, and ASGI-compatible in both directions. A Sanic application can be run under an external ASGI server, which distinguishes it from aiohttp and Tornado — it participates in the ASGI ecosystem while not depending on it.
What comes in the box#
| Routing, blueprints, middleware | Yes |
| Its own server + worker manager | Yes — distinctive |
| WebSockets, streaming, background tasks | Yes |
| Signals, graceful restart, shared worker state | Yes |
| ASGI compatibility | Yes |
| Validation | Partial — sanic-ext |
| OpenAPI | Via sanic-ext |
| ORM, admin, auth | No |
sanic-ext is a first-party extension supplying validation, OpenAPI and CORS. That it is
separate rather than core is the practical difference from FastAPI: the batteries exist
and are one more thing to install and keep aligned.
Measured position#
18,600 stars — more than Litestar, Falcon or Quart — against 1.6 million installs a month, among the lowest here. Version 25.12.1, released 2026-05-31, from 84 releases since 2016-10-15. MIT. Python ≥3.10.
That star-to-install ratio is the finding. Sanic accumulated substantial attention in the
2016–2019 window when it was the fastest option and FastAPI did not yet exist, and its
installed base has not grown with it. The version scheme is calendar-based
(25.12.1 = 2025, December), which makes release age legible at a glance and makes the
current version look older than it is.
Trade-offs#
You get genuine throughput, a framework and server tuned together, the most complete operational tooling of any framework here, and a familiar decorator API.
You give up the ecosystem gravity that followed FastAPI. Validation and OpenAPI are an extension rather than the substrate. And the performance argument that was decisive in 2017 is much weaker in 2026, when the gap between well-configured ASGI stacks is small relative to what applications actually spend their time on.
Where it is weak: as a differentiated choice. Its best argument is speed, in a category where speed is rarely the binding constraint and where the measurement is contested. S2 examines that claim directly.
Starlette#
What it is#
Starlette is the ASGI toolkit that FastAPI is built on. It provides routing, request and response objects, middleware, WebSockets, background tasks, sessions, static files, test client and an application object — everything an HTTP framework needs and nothing that interprets your data.
It is also, independently, a complete framework that people ship applications on directly. Understanding that double life is the most useful single fact in this category, because it reframes the FastAPI decision: choosing FastAPI is choosing Starlette plus a validation and documentation layer, and that layer is separable.
The shape of the opinion#
Starlette’s opinion is that a web framework should implement HTTP and stop. It has
no view on your data. There is no validation, no serialization contract, no schema
generation, no ORM, no dependency injection. A handler receives a Request and returns a
Response, and what happens in between is entirely yours.
The consequence is that a Starlette application has no framework-shaped constraints on its interior. There is no equivalent of “the FastAPI way” to do something, because Starlette has not taken a position on anything above the protocol.
This makes it the natural choice in two opposite situations: the very small service where a validation layer is overhead, and the very unusual service whose data handling does not fit any framework’s assumptions. It is a poor choice in the large middle, where the conventions FastAPI supplies are worth having.
Async posture#
ASGI-native, and the reference implementation of what that means. Starlette was written for asyncio from the first commit; there is no synchronous legacy and no compatibility layer. It runs on any ASGI server and supports HTTP, WebSockets and lifespan events through one interface.
Its threadpool handling for synchronous callables (run_in_threadpool) is the mechanism
FastAPI’s def handlers use — the escape hatch documented under FastAPI’s name is
Starlette’s code.
What comes in the box#
| Routing, ASGI app, middleware | Yes |
| Request/response, WebSockets, SSE-capable streaming | Yes |
| Background tasks, lifespan events | Yes |
| Sessions, CORS, GZip, HTTPS redirect, trusted host | Yes, as middleware |
| Static files, Jinja2 templating integration | Yes |
| Test client | Yes — httpx-based, and unusually good |
| Validation, serialization, OpenAPI | No — by design |
| ORM, admin, auth, forms | No |
Measured position#
676 million installs a month — the most-installed package in this survey, ahead of aiohttp and FastAPI. 12,600 stars. BSD-3-Clause. Python ≥3.10. Version 1.6.0, released 2026-08-08, from 202 releases since 2018-06-25.
The download figure is an artifact and must be read as one: every FastAPI install pulls Starlette, so Starlette’s count is FastAPI’s plus everything else. It measures how unavoidable Starlette is, not how often it was chosen. Its star count, 12,600 against FastAPI’s 102,000, is the better proxy for deliberate adoption — and the gap between the two numbers is the clearest illustration in this survey of why download counts should never be quoted as popularity.
The repository changed hands. Starlette now lives at Kludex/starlette, not
encode/starlette. The GitHub API returns the Kludex path with parent: null and
source: null and a creation date of 2018-06-25, which identifies a repository
transfer rather than a fork — the project moved from Encode, Tom Christie’s
organization, to its principal maintainer Marcelo Trylesinski. S4 takes up what that
means for viability; for S1 the fact is simply that documentation and links predating the
move point at the old organization.
It reached 1.0. After six years at 0.x, Starlette committed to a stable major version while FastAPI, which depends on it, remains at 0.141.1. The dependency is now more strongly versioned than the dependent.
Trade-offs#
You get the smallest correct ASGI foundation available, an unusually clean middleware model, a test client good enough that other frameworks borrow it, and no opinions to fight.
You give up everything FastAPI adds, and the list is longer than it first appears: request parsing beyond form and JSON decoding, any validation at all, response model enforcement, generated OpenAPI, Swagger UI, and dependency injection. On a service of any size you will build a thinner, less tested version of those yourself.
Where it is weak: as a default. Most teams reaching for Starlette directly would be better served by FastAPI, and the ones who should reach for it know why they are doing so. Choosing Starlette to “avoid the overhead of FastAPI” without being able to name the overhead is the characteristic mistake.
Tornado#
What it is#
Tornado is a web framework and an asynchronous networking library, written at FriendFeed and open-sourced in 2009 — before asyncio existed. It supplies routing, request handlers, templating, WebSockets, an HTTP client, and its own IOLoop.
Its position in 2026 is unusual: it is a framework whose most important contribution was absorbed into the standard library. Since Python 3.5 Tornado’s IOLoop has run on top of asyncio rather than alongside it, so the event loop that once distinguished Tornado is now the event loop the standard library provides.
The shape of the opinion#
Tornado’s opinion is that long-lived connections are the hard part of web serving. It was built for a real-time feed, and the design shows: WebSockets and long polling are first-class, and the framework assumes connections that stay open rather than request-response transactions that complete.
Handlers are classes with get, post, put methods, a design that predates the
decorator-and-function convention now standard across the category. It is not worse; it
is unfamiliar to anyone who learned Python web development after about 2015.
Async posture#
Its own coroutine framework, now running on asyncio, and not ASGI. Tornado interoperates with asyncio — you can await asyncio primitives in Tornado handlers and run Tornado on an asyncio loop — but it is not an ASGI application and does not run under uvicorn or Hypercorn. Like aiohttp, it brings its own server and sits outside the ASGI ecosystem.
What comes in the box#
| Routing, class-based handlers, its own server | Yes |
| WebSockets, long polling | Yes — the original brief |
Templating, escape/XSRF helpers, secure cookies | Yes |
| Async HTTP client | Yes |
| Auth helpers (OAuth mixins) | Yes, dated |
| Validation, OpenAPI, ORM, admin | No |
| ASGI compatibility | No |
Measured position#
118 million installs a month — fifth in this survey, ahead of Django. 22,000 stars. Apache-2.0. Python ≥3.9. Version 6.5.8, released 2026-08-07, from 84 releases since 2010-05-18.
The download count needs the same discount as aiohttp’s, for a specific and identifiable reason: Tornado is a dependency of Jupyter. A very large share of those installs are notebook infrastructure, not applications anyone wrote a Tornado handler for. It is maintained and actively released — the last push was four days before measurement — but its install count is substantially not about web development.
Trade-offs#
You get sixteen years of production hardening on long-lived connections, a self-contained stack with no server decision, and a codebase whose stability is exceptional even by this category’s standards.
You give up the ASGI ecosystem, generated documentation, validation, and the convention every newer framework shares. You take on an API that Python developers hired in the last decade will not recognize.
Where it is weak: as a choice for new work. Tornado’s distinguishing advantage — a good event loop — became a standard library feature, and the frameworks built after that happened start from it. Its remaining brief is maintaining what exists and the narrow set of cases where its connection handling is still specifically better.
S2: Comprehensive
Method#
Everything in this pass was run on 2026-08-28, not cited. The recipe, the pinned versions,
the harness and the raw per-round numbers are in bench/, and the measurements
can be re-run from them.
What was measured#
| Install cost | wheel size, transitive dependencies, import time — 15 frameworks |
| Per-request cost | routing, parameter conversion, dispatch, serialization — 13 frameworks |
| Validation | JSON bytes to a validated object, four libraries |
| Concurrency ceiling | where a blocking handler stops scaling |
The workload#
One route. GET /items/42 returns {"id": 42, "name": "item"} through a typed or converted
path parameter, in every framework.
Identical work is not assumed, it is checked before any timing runs: verify_fair.py
drives all thirteen applications and asserts HTTP 200 with a byte-identical decoded body.
It passes 13/13. Response bytes still differ — 23, 24 or 26 — because frameworks disagree
about JSON separators and trailing newlines; that is recorded per row rather than forced,
since overriding each framework’s serializer would measure a serializer chosen here.
Seven rounds of 2,000 requests, 400 warm-up. The figure reported is the minimum of the per-round means, with the median alongside so the spread is visible.
No server is involved#
The framework is handed the request directly, through its ASGI or WSGI callable. There is no socket, no uvicorn, no load generator.
That isolates the framework from the server, which is the distinction most published comparisons in this category lose: “FastAPI is faster than Flask” is almost always FastAPI-with-one-server against Flask-with-a-different-one — two variables, one number. Server choice is a separate decision and a separate survey (1.242).
So there is no throughput figure here. Throughput under concurrent load needs a socket and a load generator; that measurement is not in this survey, and any claim about requests per second should be read as unmeasured.
Three frameworks are not measured#
They own their server and expose no application callable, so there is nothing to hand a request to. They can only be measured with a socket, which this survey does not do. Their install costs are measured; their per-request costs are not.
Reading the numbers#
Measured on aarch64, CPython 3.12.3, WSL2 on a laptop.
pydantic-core, msgspec and Robyn are compiled; the rest is interpreted CPython. A
compiled-against-interpreted ratio is built differently for different instruction sets, so
these figures compare with each other and not with numbers measured on x86_64.
A no-op application costs 1.16 µs per request through the same harness. It is small against every framework measured, and it is not subtracted from anything.
Feature and architecture matrix#
Consolidated from S1’s per-framework files and the L1–L4 measurements. Every figure is
bench/results/*.json, measured 2026-08-28 on aarch64 / CPython 3.12.3.
| framework | protocol | µs/req (L2) | install (L1) | dists | validation | OpenAPI | DI | ORM/admin |
|---|---|---|---|---|---|---|---|---|
| FastAPI | ASGI | 33.35 | 11.3 MB | 10 | Pydantic, mandatory | yes | yes | no |
| Starlette | ASGI | 8.55 | 2.1 MB | 4 | none | no | no | no |
| Litestar | ASGI | 16.97 | 27.1 MB | 22 | Pydantic/attrs/msgspec | yes | yes | no |
| Quart | ASGI | 84.20 | 7.1 MB | 16 | none | no | no | no |
| BlackSheep | ASGI | 6.28 | 34.4 MB | 14 | typed handlers | yes | yes | no |
| Sanic | ASGI+own | 18.19 | 27.0 MB | 12 | via sanic-ext | via ext | no | no |
| Falcon | both | 6.65 / 5.79 | 4.3 MB | 1 | none | no | no | no |
| Flask | WSGI | 40.54 | 4.6 MB | 7 | extensions | extensions | no | extensions |
| Django | WSGI+partial ASGI | 38.26 | 24.0 MB | 3 | forms | no | no | yes |
| Django Ninja | WSGI | 55.09 | 43.3 MB | 9 | Pydantic | yes | partial | yes (Django’s) |
| Bottle | WSGI | 8.49 | 0.4 MB | 1 | none | no | no | no |
| Pyramid | WSGI | 20.96 | 8.8 MB | 11 | none | no | no | via ecosystem |
| aiohttp | own loop | not measurable | 12.3 MB | 10 | none | no | no | no |
| Tornado | own loop | not measurable | 2.5 MB | 1 | none | no | no | no |
| Robyn | own loop | not measurable | 27.2 MB | 12 | none | no | no | no |
not measurable is a finding, not a gap: those three own their server and expose no application callable to drive in process. They are reachable only at L5.
Validation, isolated (L3)#
| µs | validates | compiled | |
|---|---|---|---|
| msgspec | 0.275 | yes | yes |
| Pydantic v2 | 0.993 | yes | yes |
json.loads | 1.579 | no | yes |
| dataclass | 1.947 | no | no |
| attrs + cattrs | 2.492 | yes | no |
The thread-pool ceiling (L4)#
anyio default limiter: 40 tokens. Applies to FastAPI, and by construction to
Starlette and Litestar, which use the same limiter.
concurrent def handlers | wall time (50 ms handler) |
|---|---|
| ≤ 40 | ~50–70 ms — one batch |
| 41 | 106 ms — two batches |
| 120 | 176 ms — three batches |
S2 measurement plan#
Written before S2, per Step 3.5 of ADDING-RESEARCH.md. Levels are ordered by cost. The
default cut line is everything that runs in one container with no external service and
no second machine; the rest is deferred and filed.
Open questions#
S1 ended by naming four things it could not settle by reading. Three are measurements and one is not — and saying so matters, because dressing the fourth as a benchmark would misrepresent what it is:
- Throughput. Every published number in this category cites a contaminated source.
- FastAPI’s
def-handler ceiling. Described everywhere, quantified nowhere. - Django’s async ORM. Connection-level concurrency, or sync wrapped in a thread?
- Litestar’s 319 open issues on 8,400 stars. Not a benchmark. Close rate, issue age
and contributor count from the GitHub API — tier
registry, rungcited.
The structural constraint#
The category has three architectures (S1 finding 1), and no single harness reaches all fifteen frameworks:
| architecture | frameworks | in-process harness |
|---|---|---|
| ASGI | FastAPI, Starlette, Litestar, Quart, BlackSheep, Sanic, Falcon-asgi | call the callable with a scope dict |
| WSGI | Flask, Django, Bottle, Pyramid, Falcon-wsgi | call it with an environ dict |
| own loop, own server | aiohttp, Tornado, Robyn | none — there is no callable to call |
aiohttp, Tornado and Robyn are unreachable below the socket level. That is not a gap in the method; it is S1’s three-architecture finding restated as a measurement constraint, and S2 reports it as one rather than quietly dropping three frameworks from a table.
The levels#
| level | settles | cost | rung | covers | |
|---|---|---|---|---|---|
| L0 | Registry facts | versions, licenses, floors, cadence | done in S1 | cited | 15/15 |
| L1 | The install — wheel size, transitive dependency count, venv bytes, python -X importtime, app-construction time | Falcon’s zero-dependency claim; what FastAPI’s Pydantic substrate actually costs; supply-chain surface | ~30 min | measured-local | 15/15 |
| L2 | One request, in process, no socket — drive the application callable directly | the headline: framework overhead isolated from the server, which is exactly 1.241’s scope | ~half a day | measured-local | 12/15 |
| L3 | Validation isolated — one payload through Pydantic, msgspec and attrs | whether Litestar’s pluggable validation is a performance lever or a preference | ~2 h | measured-local | subset |
| L4 | The def-handler ceiling — concurrent requests through a sleeping handler until the thread pool binds | S1 question 2 | ~3 h | measured-local | FastAPI, Starlette, Litestar |
| L5 | Real server, socket, load generator — throughput and p50/p95/p99 under a concurrency sweep | S1 question 1, and the only level that reaches the own-loop three | 1–2 days | measured-local | 15/15 |
| L6 | Django’s async ORM against a real Postgres | S1 question 3 | ~1 day | measured-local | Django |
| L7 | x86_64 and aarch64 × a workload matrix | where the ranking changes with workload | 2 machines | measured-local | 15/15 |
| L8 | Floor model — reader re-runs it | the reader can check me | a real build | measured-browser | limited, see below |
The cut#
S2 takes L1–L4. One container, no external service, roughly a working session — within an order of magnitude of the 1,422-line median that the literature-review S2 has been costing. It settles two of S1’s four questions outright and delivers the framework-overhead story that is this survey’s actual scope.
L5, L6 and L7 are deferred and filed as a follow-up bead on re-3xp.1. L5 and L6 are
not optional forever — they close S1 questions 1 and 3, and the survey must say plainly
that those remain open until they run. L7 stays deferred unless a specific contested claim
needs it.
Question 4 is answered at L0 from the GitHub API, alongside L1, and reported as cited.
Two rules#
L2 needs a fair-handler rule. Same response bytes, same route shape, same serialization work in every framework. Time different jobs and you have a ranking of different jobs.
L5, when it runs, must hold the server constant. Every ASGI framework under one uvicorn version; every WSGI framework under one gunicorn. Report deltas within a server, never across. Comparing FastAPI+uvicorn to Flask+gunicorn reproduces exactly the contamination S1 refused to quote — two variables and one number, which is what makes the published benchmarks in this category unusable.
Where the floor model comes from#
Verified against the Pyodide 314.0.5 lock on 2026-08-28: it ships fastapi 0.136.1, starlette 1.0.0, pydantic 2.12.5, anyio 4.13.0, jinja2 3.1.6, and does not ship flask, django, werkzeug, litestar or uvicorn. The missing ones are pure Python and should reach the page through micropip; Robyn is a Rust extension and can never run there.
And no browser tab has a listening socket, so L5 is permanently out of reach of a Workshop page. The floor model therefore derives from L2 — the cheap rung — and skips everything above it. It is not gated on L5 existing and should not wait for it.
Filed as its own bead so it can proceed in parallel rather than queueing behind L5.
S2 verdict#
S2 does not pick a framework — that is S3 and S4. It reports what running the code established, what it overturned, and what it left unmeasured.
What the measurements show#
1. The overhead is 24.8 µs, and not validation#
L2: FastAPI 33.35 µs/request, Starlette 8.55 µs — a 3.9× ratio on a framework that is Starlette plus a layer. L3: that layer’s validation costs 0.993 µs.
So the remaining ~24 µs is dependency resolution, response-model handling and FastAPI’s routing above Starlette’s. Anyone tuning FastAPI by reaching for a faster validator is optimizing 4% of the overhead. This is knowable only because the two levels were measured separately; as one number it would have been invisible, and the intuitive explanation would have stood.
2. Validation is cheaper than no validation#
L3: model_validate_json 0.993 µs, json.loads 1.579 µs. Pydantic v2 does not parse and
then check — it parses in Rust and builds the validated object in one pass, never
constructing the intermediate dict that the standard library spends its time on.
Validation is not a tax on parsing; in Pydantic v2 it replaces the parser. Avoiding it to keep a hot path lean makes the path slower. The direction is structural; the 1.6× is aarch64 and not portable.
3. Quart costs 2.1× Flask#
L2: Quart 84.20 µs, Flask 40.54 µs. Quart is the most expensive framework measured.
S1 established Quart as the resolution to Flask’s async trap, and that stands. L2 adds the price, and the two are the trade rather than a contradiction: on I/O-bound work the cost is repaid many times by not blocking a worker; on fast or CPU-bound handlers there is nothing to repay it with. Where the crossover sits needs concurrent load, which is L5.
4. The def-handler ceiling is 40#
L4: anyio’s default limiter is 40 tokens. Forty concurrent def handlers finish in one
50 ms batch; forty-one take 106 ms. Wall time tracks ⌈C/40⌉ × 50 ms across the range.
async def is flat from C=1 to C=120.
It is a limit on concurrency, not throughput — 40 in-flight is 40 requests per second if each takes a second, so a service can reach it while its request rate still looks modest. S1 question 2: settled.
5. Minimalism, measured#
L1: Falcon, Bottle and Tornado each install one distribution. Falcon is also the fastest WSGI framework (5.79 µs) and third fastest overall. Bottle is 0.4 MB — 108× smaller than Django Ninja.
Litestar brings 22 distributions, the most here and 2.2× FastAPI’s 10. S1 called its larger first-party surface “its pitch and its risk”; L1 says the pitch and the risk are the same 22 rows.
6. BlackSheep is second-fastest and almost unused#
L2: 6.28 µs, 5.3× cheaper per request than FastAPI, on 43,000 installs a month. S1 concluded “the code is not the problem; the absence of a community around it is.” Nothing measured here complicates that, and L2 makes the gap starker rather than smaller.
Claims that changed under measurement#
| S1 said | S2 found |
|---|---|
| FastAPI’s layer over Starlette is separable | It costs 24.8 µs/request — and is not mostly validation |
| Litestar’s pluggable validation might be a performance lever | It is, and the lever is 0.72 µs — 2–4% of framework overhead |
| Quart is the resolution to Flask’s async trap | True, and it costs 2.1× Flask per request |
| Falcon’s zero dependencies are a supply-chain property | Confirmed: one distribution, and fastest WSGI |
The def fallback has a ceiling | The ceiling is 40, and it is a cliff, not a slope |
Nothing S1 asserted was overturned. Two things it left as plausible mechanisms turned out to have the wrong cause (finding 1) or a much smaller magnitude than the argument implied (finding 2 of the table).
Both deferred questions are now answered#
S1 question 1 — throughput — is measured. L5 ran the same workload over a socket with the server held constant. The L2 ranking held in direction for every framework and gained something per-request cost cannot show: FastAPI saturates at about 14,200 rps while Starlette continues to 33,000, a 2.4x throughput gap where L2 measured a 3.9x per-request one. L5 is also where aiohttp and Tornado became measurable at all, which L2 had to exclude.
S1 question 3 — Django’s async ORM — is measured. L6 put a
pg_sleep(0.5) against real PostgreSQL. asyncpg is flat at 0.51 s from 1 to 64 concurrent
queries; Django’s async path takes 3.39 s at 64, with effective parallelism of about ten.
S1 was right that it wraps synchronous execution in a thread pool, and the pool is small.
Both ran on the rig built for survey 1.242 with the axes transposed — one harness, two
surveys. Bead re-3xp.7.
S1 question 4 was never a benchmark. Litestar’s 319 open issues on 8,400 stars is
GitHub API data — close rate, issue age, contributor count — and belongs at rung cited
alongside L0. Dressing it as a measurement would have misrepresented it.
What is still not measured#
HTTP/2, TLS, multi-machine deployment, and anything above the rig’s ceiling of roughly 35,000-40,000 requests a second, where the load generator saturates and no server number is trustworthy.
Re-run this yourself#
Findings 1 and 2 have a floor model: layer cake. It races FastAPI against Starlette in your own browser and prices the layer on your machine, then measures validation against plain decoding on JSON you paste in.
It derives from L2 and L3 — the cheap rungs — and not from L5, because a browser tab has no listening socket and never will. The expensive measurement is permanently out of reach of a page, which is why the floor model was not gated on it.
Building it corroborated both findings in a different runtime, which is worth more than the page itself. Under WebAssembly — CPython 3.14.2 rather than 3.12.3, fastapi 0.136.1, starlette 1.0.0, pydantic 2.12.5, all different versions on a different interpreter — the race returned Starlette 18.4 µs against FastAPI 68.0 µs, a 3.7× multiple where the native bench measured 3.9×. And validation beat plain decoding again: 2.00 µs against 2.54, a 1.27× lead where native measured 1.6×.
The absolute numbers are two to three times slower, as WebAssembly should be. The ratios held across a change of interpreter, package versions and instruction set. For findings that are ratios rather than absolutes, that is independent corroboration rather than a repeat measurement — and it is the strongest evidence in this pass that the two headline results are structural rather than an artifact of one machine.
How to read these numbers#
aarch64, WSL2, CPython 3.12.3, one laptop. These figures may be compared with each other. They may not be compared against a number measured on x86_64, and the reason is specific rather than cautious: pydantic-core, msgspec and Robyn are compiled, everything else is interpreted, and a compiled-versus-interpreted ratio is built differently for the two targets. This corpus put an ARM measurement on an x86 axis once already, in 1.253.
Recipe, pins, workload and raw per-round numbers: bench/. If the next person cannot
re-run it, the rung is not measured-local — it is repeated with extra steps.
L1 — install cost#
One clean uv venv per framework, measured 2026-08-28 on aarch64 / CPython 3.12.3.
Raw: bench/results/l1.json. Recipe: bench/README.md.
| framework | version | site-packages | distributions | import (µs) |
|---|---|---|---|---|
| Bottle | 0.13.4 | 0.4 MB | 1 | 36,621 |
| Starlette | 1.6.0 | 2.1 MB | 4 | 73,990 |
| Tornado | 6.5.8 | 2.5 MB | 1 | 89,426 |
| Falcon | 4.3.1 | 4.3 MB | 1 | 72,422 |
| Flask | 3.1.3 | 4.6 MB | 7 | 89,807 |
| Quart | 0.22.0 | 7.1 MB | 16 | 160,365 |
| Pyramid | 2.1 | 8.8 MB | 11 | 98,197 |
| FastAPI | 0.141.1 | 11.3 MB | 10 | 193,708 |
| aiohttp | 3.14.3 | 12.3 MB | 10 | 154,818 |
| Django | 6.1 | 24.0 MB | 3 | 7,881 |
| Sanic | 25.12.1 | 27.0 MB | 12 | 118,085 |
| Litestar | 2.24.0 | 27.1 MB | 22 | 161,679 |
| Robyn | 0.88.0 | 27.2 MB | 12 | 85,100 |
| BlackSheep | 2.6.3 | 34.4 MB | 14 | 75,169 |
| Django Ninja | 1.6.3 | 43.3 MB | 9 | 179,903 |
Import is the minimum of five cold subprocesses, read from -X importtime on the full
dotted module. Install size and distribution count are architecture-independent for
pure-Python wheels and not for Robyn, whose Rust extension is built per target.
Falcon, Bottle and Tornado install one distribution#
S1 recorded Falcon’s “zero required dependencies” as a supply-chain differentiator on the strength of its own documentation. It holds: one distribution, itself. So do Bottle and Tornado.
That is three of fifteen. Everything else brings a tree, and the trees differ by more than an order of magnitude at the extremes.
Litestar brings 22 distributions#
22 distributions — more than any other framework here, and 2.2× FastAPI’s 10.
S1 said Litestar “ships more out of the box than FastAPI — the SQLAlchemy repository layer, channels, rate limiting and caching are all first-party — which is its pitch and its risk.” The pitch and the risk are the same 22 rows. A team counting supply-chain surface is choosing between 22 and 10 before it writes a line.
Bottle is 0.4 MB#
S1 called the single-file property “a real capability, not a novelty”. At 0.4 MB and one distribution it is 108× smaller than Django Ninja and 28× smaller than FastAPI. Nothing else in this category can be deployed by copying one file, and the measurement says the file is that small.
Django’s import number measures almost nothing#
7,881 µs — the cheapest import here by 4.6×, on the framework with the deepest feature set. That is not a finding about Django’s weight.
import django loads a nearly empty package. Django’s actual startup cost is
django.setup(), which reads settings, builds the app registry and populates the ORM’s
model metadata — and that is application construction, not import. The 24.0 MB on disk is
the size signal that means something; the import figure measures almost nothing and is
reported here
only so that nobody quotes it as though it did.
BlackSheep is heavy#
34.4 MB and 14 distributions — third heaviest, on a framework with 43,000 installs a month. S1 noted its auth model is more complete than FastAPI’s out of the box; L1 says that completeness is not free, and it arrives whether or not you use it.
Django Ninja costs Django plus Pydantic#
43.3 MB, the largest install measured. S1 called it “a FastAPI-shaped API layer that runs inside a Django project”; on disk that is the full Django tree plus the Pydantic stack, and the convenience is additive rather than free.
L2 — per-request cost#
No socket, no server, no load generator. The application callable is driven directly with an ASGI scope dict or a WSGI environ dict, so what is timed is routing, parameter conversion, handler dispatch and serialization — the framework, and nothing under it.
Workload: GET /items/42 → {"id": 42, "name": "item"}, typed path parameter, identical
in all thirteen (gate: verify_fair.py, 13/13). Seven rounds of 2,000 requests, 400
warm-up. Reported figure is the minimum of per-round means. aarch64, CPython 3.12.3.
Raw: bench/results/l2.json.
| framework | kind | µs/request | median | response |
|---|---|---|---|---|
| (harness control) | — | 1.16 | — | 23 B |
| Falcon | WSGI | 5.79 | 6.12 | 26 B |
| BlackSheep | ASGI | 6.28 | 6.91 | 23 B |
| Falcon | ASGI | 6.65 | 7.23 | 26 B |
| Bottle | WSGI | 8.49 | 9.68 | 23 B |
| Starlette | ASGI | 8.55 | 8.95 | 23 B |
| Litestar | ASGI | 16.97 | 17.63 | 23 B |
| Sanic | ASGI | 18.19 | 18.91 | 23 B |
| Pyramid | WSGI | 20.96 | 23.03 | 26 B |
| FastAPI | ASGI | 33.35 | 34.06 | 23 B |
| Django | WSGI | 38.26 | 40.82 | 26 B |
| Flask | WSGI | 40.54 | 42.34 | 24 B |
| Django Ninja | WSGI | 55.09 | 61.90 | 26 B |
| Quart | ASGI | 84.20 | 89.06 | 24 B |
ASGI rows carry roughly 1 µs of event-loop overhead that WSGI rows do not — the harness control. It is small against every gap discussed below and it is not subtracted.
FastAPI costs 3.9× Starlette#
33.35 µs against 8.55 µs. FastAPI is Starlette plus a validation, injection and documentation layer, so the difference is that layer, and it is 24.8 µs per request — about 74% of FastAPI’s total.
S1 said the layer was separable and that “choosing FastAPI is choosing Starlette plus a validation and documentation layer.” L2 puts a price on it. Whether 24.8 µs matters depends entirely on what the handler does next: against a 5 ms database query it is 0.5% and invisible; against a cache hit answered from memory it is most of the request.
What that 24.8 µs is not: validation. L3 measures validation at roughly 1 µs. The overhead is dependency resolution, response-model handling and FastAPI’s own routing layer sitting above Starlette’s. This decomposition is only visible because L2 and L3 were both run, and it corrects the intuitive explanation.
Quart costs 2.1× Flask#
84.20 µs against 40.54 µs, and Quart is the slowest framework measured.
This is the pass’s most counterintuitive result and it needs stating carefully, because
the obvious reading is wrong. S1 established that Flask accepts async def without buying
concurrency, and that Quart — Flask’s own API on ASGI, maintained by Pallets — is the
resolution. That remains true. L2 adds the price: you buy concurrency and you pay for
it per request.
The two facts are not in tension, they are the trade. A Flask worker is occupied for the whole life of a request, so on I/O-bound work Quart’s higher per-request cost is repaid many times over by not blocking. On CPU-bound or trivially fast handlers there is nothing to repay it with, and Quart is simply the more expensive choice.
L2 cannot say where the crossover is, because that needs concurrent load — L5, deferred.
Falcon is fastest in both protocols#
Fastest WSGI (5.79 µs) and third-fastest overall (6.65 µs ASGI), from the framework that also ships one distribution and no validation, no OpenAPI and no injection.
S1 reported Falcon’s design goals — small dependency footprint, no reflection or metaclass machinery in the request path — from its own documentation. L1 and L2 together confirm both halves. Falcon is the only framework here that is simultaneously the lightest to install and among the fastest to run, and it achieves that by not doing the things the others do.
BlackSheep is fast and unused#
6.28 µs — second fastest of thirteen, 5.3× cheaper per request than FastAPI — on a framework with 43,000 installs a month, the second-lowest in the category.
S1 recorded BlackSheep as architecturally FastAPI’s nearest neighbour with a more complete auth model and almost no adoption, and concluded “the code is not the problem; the absence of a community around it is.” L2 supports that reading rather than complicating it: on this workload the code is very good indeed.
Litestar costs half of FastAPI#
16.97 µs against FastAPI’s 33.35 — roughly half the per-request cost, on a framework S1 described as having the better structural argument and a fraction of the ecosystem.
S1 could only report the structural claim: layered configuration, controllers, DTOs, validation that is not a single-library commitment. L2 adds that Litestar is also substantially cheaper per request than the incumbent it argues with. That does not settle the choice — L1 says Litestar brings 22 distributions to FastAPI’s 10, and S1’s ecosystem gap is unchanged — but it removes “you pay for the structure in performance” from the list of reasons not to use it.
Django Ninja adds about 17 µs to Django#
55.09 µs against Django’s 38.26. The typed-handler-and-OpenAPI layer costs roughly what its FastAPI equivalent costs relative to Starlette, in proportion, on top of a framework that starts more expensive.
What is not covered#
Nothing here is throughput. These are single-request costs with no contention, no connection handling, no server and no concurrency. A framework that is cheap per request may still be the wrong choice under load, and the ranking above may reorder once a socket and a load generator are involved.
That measurement is L5, deferred (bead re-3xp.7). S1’s question 1 is still open.
L3 — validation#
FastAPI’s validation is Pydantic and cannot be swapped; Litestar accepts Pydantic, msgspec or attrs. S1 recorded that as a structural difference and could not say whether it was a performance lever. This level measures it.
Frameworks receive bytes, so every contender does the same job: decode JSON and produce a validated typed object. Comparing a validator that starts from a dict against one that starts from bytes would flatter the first.
Payload: {"id": 42, "name": "item", "tags": ["a","b","c"], "price": 9.99, "active": true}
— 5 fields, one list, 71 bytes. Seven rounds of 20,000. Minimum of per-round means.
aarch64, CPython 3.12.3. Raw: bench/results/l3.json.
| contender | µs | median | validates? | implementation |
|---|---|---|---|---|
| msgspec 0.21.1 | 0.275 | 0.283 | yes | compiled (C) |
| pydantic 2.13.5 | 0.993 | 1.009 | yes | compiled (Rust) |
json.loads | 1.579 | 1.648 | no | compiled (C) |
| dataclass | 1.947 | 1.965 | no | interpreted |
| attrs 26.1.0 + cattrs | 2.492 | 2.689 | yes | interpreted |
Validation costs less than no validation#
0.993 µs against json.loads’s 1.579 µs. Pydantic is 1.6× faster than the parser it
is supposed to be sitting on top of, while doing strictly more work.
The reason is that parse-then-validate is not the sequence that runs. model_validate_json does not call json.loads and then check types; it
parses the bytes in Rust and constructs the validated object in one pass, never building
the intermediate Python dict. The standard library builds that dict, and building it is
the expensive part.
The consequence is specific: validation is not a tax on parsing. In Pydantic v2 it
replaces the parser. Avoiding Pydantic to keep a hot path lean, and calling json.loads
instead, makes the request slower.
This ratio is architecture-bound. pydantic-core and CPython’s json are both compiled,
but for different targets and by different toolchains, and the figure above is aarch64. The
direction of the result is a structural fact about doing one pass instead of two; the
1.6× is not portable and must not be quoted against an x86_64 number.
msgspec is 3.6× Pydantic#
0.275 µs against 0.993. Both are compiled, both validate, and msgspec is comfortably the fastest thing measured at this level.
This is where Litestar’s pluggable validation stops being a preference and becomes a lever — but a small one in context. The gap is 0.72 µs per request. Against Litestar’s own 16.97 µs of framework overhead (L2) it is 4%; against FastAPI’s 33.35 µs it is 2%. S1 asked whether the multi-library validation was a performance argument. It is, and the argument is worth less than a microsecond.
The better reading of Litestar’s flexibility is the one S1 already gave: not being welded to one library’s release cycle. A Pydantic major-version migration is a FastAPI migration; it is not necessarily a Litestar one.
attrs is the slowest option#
2.492 µs — 2.5× Pydantic, and slower than the unvalidated dataclass control. attrs and
cattrs are interpreted Python doing json.loads first and structuring second: two passes,
in the slower language, which is exactly the cost Pydantic v2 avoids.
Choosing attrs in Litestar is a choice about type ergonomics and existing codebase fit. It is not a performance choice, and on this workload it is the worst one available.
Where the overhead goes#
L2 measured FastAPI at 24.8 µs per request above Starlette, the library it is built on. The intuitive explanation is validation.
It is not. Validation is 0.993 µs — 4% of that gap.
The other 96% is dependency resolution, response-model handling and FastAPI’s routing layer above Starlette’s. Anyone tuning a FastAPI application by reaching for a faster validator is optimizing the smallest component of the overhead, and the reason that is knowable is that the two levels were measured separately rather than as one number.
L4 — the def-handler ceiling#
S1: FastAPI runs an async def handler on the event loop and a plain def handler in a
thread pool, so blocking code does not stall the loop. S1 noted that the fallback cuts both
ways, and that the ceiling is “described everywhere and quantified nowhere.”
Here it is quantified.
Method. One handler that sleeps 50 ms, written both ways. C requests issued concurrently against the application callable; wall time for all C. An async handler should stay flat at ~50 ms however large C grows. A sync handler can only run as many at once as the pool has slots, so wall time should step every time C crosses a multiple of that number — and the step names the pool.
The pool, read from the runtime: anyio’s default thread limiter is 40 tokens.
| C | async def | def (pool) | ratio | ⌈C/40⌉ × 50 ms |
|---|---|---|---|---|
| 1 | 0.051 s | 0.052 s | 1.01× | 50 ms |
| 8 | 0.051 s | 0.055 s | 1.07× | 50 ms |
| 20 | 0.052 s | 0.060 s | 1.16× | 50 ms |
| 40 | 0.052 s | 0.070 s | 1.35× | 50 ms |
| 41 | 0.052 s | 0.106 s | 2.06× | 100 ms |
| 80 | 0.066 s | 0.128 s | 1.92× | 100 ms |
| 120 | 0.054 s | 0.176 s | 3.25× | 150 ms |
Raw: bench/results/l4.json.
The 41st request waits#
Forty concurrent def handlers complete in one 50 ms batch. Forty-one take 106 ms —
the extra request cannot start until a thread frees, so it runs in a second batch, and
every request in the group waits for it.
One additional request doubles the latency of the whole group. Not degrades: doubles.
The measured wall times track ⌈C/40⌉ × 50 ms across the whole range, with the excess
over the prediction being dispatch overhead that grows mildly with C. The ceiling is not
approximate and it is not load-dependent — it is the pool size, and the pool size is 40.
async def does not move#
0.051 s at C=1 and 0.054 s at C=120. Two thousand three hundred percent more concurrency, 6% more wall time. The event loop is doing what it exists to do, and the contrast is the whole point of the level.
What it means in practice#
S1 said the def fallback “lets a synchronous database driver work correctly
without rewriting the application” and that “a def handler is capped by the thread
pool’s size, so the performance the framework is known for does not automatically apply to
code that opted out of async.” Both halves are confirmed, and the second now has a number:
the cap is 40.
The practical shape of it:
- Below 40 concurrent blocking handlers, the thread pool is invisible and the fallback is exactly as good as advertised.
- At 41 it becomes the dominant term in latency, abruptly.
- A service with slow blocking handlers and modest traffic can cross 40 in-flight requests without its request rate looking high at all — 40 in-flight is 40 requests per second if each takes a second.
That last point is what makes the ceiling easy to miss. It is a limit on concurrency, not on throughput, and the two coincide only when handlers are fast.
Scope#
Measured on FastAPI, and the limiter belongs to anyio, which Starlette and Litestar also
use — so the same ceiling is expected there and was not separately measured. The number is
adjustable at runtime; nothing here says 40 is the right value, only that it is the default
and that crossing it costs a full extra batch.
The step structure is a property of the thread pool and portable. The absolute wall times are aarch64 on a laptop and are not.
L5 — throughput under load, the server held constant#
The level S1 asked for and S2 originally deferred. Same workload as L2 — GET /items/42
returning 23 bytes — now over a real socket, with the same uvicorn under every ASGI
framework and the same gunicorn under every WSGI one, so a difference is the framework’s.
45 runs, 0 failed, 1 excluded as client-bound. aarch64, 8 cores, client and server sharing
the machine. Raw: bench/results/l5.json.
ASGI, under one uvicorn#
| framework | c=8 | c=32 | c=128 | L2 µs/req |
|---|---|---|---|---|
| BlackSheep | 12,158 | 31,092 | 33,825 | 6.28 |
| Starlette | 12,040 | 30,295 | 32,950 | 8.55 |
| Falcon (asgi) | 12,733 | 30,881 | 32,182 | 6.65 |
| Litestar | 11,029 | 23,788 | 22,220 | 16.97 |
| FastAPI | 7,461 | 14,230 | 13,557 | 33.35 |
| Sanic | 11,079 | 8,870 | 7,084 | 18.19 |
| Quart | 5,552 | 6,918 | 6,670 | 84.20 |
WSGI, under one gunicorn#
| framework | c=8 | c=32 | c=128 |
|---|---|---|---|
| Bottle | 1,803 | 2,078 | 1,887 |
| Falcon (wsgi) | 1,942 | 1,894 | 1,818 |
| Pyramid | 1,709 | 1,709 | 1,620 |
| Django Ninja | 1,548 | 1,522 | 1,507 |
| Flask | 1,571 | 1,540 | 1,466 |
| Django | 1,564 | 1,525 | 1,420 |
Own server — not on the same axis#
aiohttp, Tornado and Robyn own their server, so these are framework-and-server figures and cannot be compared with the tables above.
| c=8 | c=32 | c=128 | |
|---|---|---|---|
| aiohttp | 16,124 | 35,056 (client-bound) | 33,999 |
| Tornado | 8,789 | 9,067 | 8,121 |
This is the level at which they became measurable at all. L2 drove the application callable directly and these expose none; that exclusion was reported as a finding and it resolves here.
Robyn is not measured. Version 0.88.0 logs its route and then never opens a socket on
this host, with and without sys.argv cleared before construction. Tried twice, not
diagnosed, and recorded rather than guessed at.
L2’s ranking holds under load, and adds a ceiling#
L2 measured FastAPI’s per-request cost at 3.9× Starlette’s with no server involved. Under load the same pair reads 13,557 against 32,950 requests a second — a 2.4× throughput gap, and the shape is worse than the ratio:
FastAPI saturates. It reaches about 14,200 rps at concurrency 32 and does not improve at 128; Starlette continues from 30,300 to 33,000. The per-request cost L2 measured turns out to be a ceiling rather than a constant tax, and a service sized on FastAPI’s low-concurrency numbers will meet it.
Litestar’s L2 advantage over FastAPI (16.97 µs against 33.35) shows up as 22,220 against 13,557 — a 1.6× throughput lead, in the same direction and smaller in magnitude, which is what a fixed per-request cost becoming one term among several looks like.
Two frameworks get worse as load increases#
Sanic: 11,079 rps at c=8, 8,870 at c=32, 7,084 at c=128, with p99 latency reaching 169 ms. It is the only framework here whose throughput declines monotonically with concurrency.
Sanic is designed to run under its own server, and this measurement runs it under uvicorn to keep the axis intact. That limits what the number says: the figure is Sanic-under-uvicorn, not Sanic, and Sanic-under-Sanic was not measured. The 1.242 survey’s boundary rule cuts both ways here.
Quart is slowest of the ASGI set at every concurrency, at 6,670 rps against Starlette’s 32,950 — consistent with L2’s finding that it is the most expensive framework measured, and the price of Flask’s API on ASGI.
The protocol gap, from the framework side#
The fastest WSGI framework measured serves 1,887 requests a second. The fastest ASGI framework serves 33,825. Eighteen times, and 1.242 measured the same gap from the server side at 25×.
Within WSGI, every framework lands between 1,420 and 1,887 — a 33% spread across six frameworks including Django and Flask. The protocol sets the number and the framework adjusts it slightly, which is the clearest statement this survey can make about where performance actually comes from.
S1’s question 1: answered#
Throughput. Every framework here that claims speed cites contaminated sources.
Measured, with the server held constant. The ranking matches L2’s per-request costs in direction for every framework, and adds the saturation behavior that per-request cost cannot show.
L6 — Django’s async ORM: connection concurrency, or a thread pool?#
S1 asserted this and could not show it:
The ORM’s async methods wrap synchronous execution in a thread rather than speaking to the database over an async driver, so async ORM calls do not deliver connection-level concurrency.
Method. A query that sleeps in the database — SELECT pg_sleep(0.5) — so the time is
spent on the connection, not in Python. N of them issued concurrently, wall time measured.
The two possible shapes are far apart:
- connection-level concurrency → N queries take ~0.5 s however large N is
- a thread pool over synchronous I/O → N queries take ⌈N / pool⌉ × 0.5 s
asyncpg runs alongside as the control: a truly async driver, on the same machine and the same database, showing what connection-level concurrency looks like here.
PostgreSQL 16.14 in Docker, aarch64. Raw: bench/results/l6.json.
| N | asyncpg | Django sync_to_async | asyncpg, in sleeps | Django, in sleeps | Django’s effective parallelism |
|---|---|---|---|---|---|
| 1 | 0.514 s | 0.545 s | 1.0× | 1.1× | — |
| 4 | 0.521 s | 0.553 s | 1.0× | 1.1× | 3.6 |
| 16 | 0.514 s | 1.133 s | 1.0× | 2.3× | 7.0 |
| 40 | 0.512 s | 2.117 s | 1.0× | 4.2× | 9.5 |
| 64 | 0.558 s | 3.386 s | 1.1× | 6.8× | 9.5 |
S1 was right, and the number is about ten#
asyncpg is flat. Sixty-four concurrent half-second queries take 0.558 seconds — the same as one. Every query is in flight on its own connection at the same time, which is what connection-level concurrency means.
Django’s async path is not flat. It tracks ⌈N / pool⌉ almost exactly, and the effective parallelism settles at about 9.5 — sixty-four queries take 3.4 seconds, not 0.5.
That is a thread pool, and it is a small one. The asynchronous call returns a coroutine and awaits correctly; underneath, the query runs on a worker thread with a synchronous driver, and the number of those threads is the ceiling.
What this means for a Django service#
Below about ten concurrent database queries, the async ORM behaves as if it were doing what it appears to do. At N=4 it is within 6% of asyncpg. A developer testing locally will see async working.
Above that it degrades linearly, and the symptom is latency rather than errors: at 64 concurrent queries the last one waits 3.4 seconds for a query the database answered in 0.5.
This is the same shape as 1.241’s L4 finding one layer up — FastAPI’s def handlers capped
at 40 threads — and the two compound. A Django async view calling the ORM is queueing behind
a pool of roughly ten, inside a request that may already be queueing behind a pool of forty.
What was measured#
sync_to_async(thread_sensitive=False) around a cursor executing the sleep, which is the
mechanism Django’s a-prefixed ORM methods use underneath. Django’s own async ORM API was
not called directly — the model layer would have added its own cost to both sides and this
level is about the driver path, not the ORM’s Python overhead.
So the finding states: Django’s async database path is a thread pool of about ten over a synchronous driver. It is not a claim about how much the ORM adds on top of that.
The pool size is configurable and this measured the default. Raising it moves the ceiling and does not change the shape.
S1’s question 3: answered#
Django’s async ORM. Whether
afilterand friends deliver connection-level concurrency or wrap sync execution in a thread is checkable, and the answer changes Django’s position for I/O-bound work.
It wraps. Effective parallelism about ten, against asyncpg’s unbounded. For I/O-bound work this is the constraint S1 suspected, now with a number on it.
S3: Need-Driven
The seven situations#
| the situation | what eliminates options | lands on | |
|---|---|---|---|
| 1 | Publishing a typed API other teams consume | the contract must be generated and enforced, not maintained by hand | FastAPI |
| 2 | A content site with a non-technical editor | somebody must edit content without a deploy | Django |
| 3 | An internal tool for twelve colleagues | the interface must be nearly free to build | Django |
| 4 | A service whose work is calling other services | concurrency, not speed | aiohttp |
| 5 | A Flask codebase timing out under load | a rewrite is not available; extensions must survive | Quart, conditionally |
| 6 | A component in someone else’s request path | predictability and dependency surface | Falcon |
| 7 | A small team self-hosting what it depends on | maintenance attention, not compute | split by shape |
The verdict sets out the three places these answers conflict.
What counts as a situation#
Each one names a constraint that eliminates options. If nothing is eliminated, there is no decision to make and the framework choice is taste.
That is why Django appears three times and is unmentionable four: an included admin interface is most of the product for personas 2, 3 and 7, and 24 MB of surface in a request path for persona 6. The same property, read against different constraints.
Where the triggering application sits#
They get no more weight than the other six, they did not choose which frameworks were compared, and they appear nowhere in the S1 verdicts. A survey bent toward the job that prompted it under-serves every other reader.
S3 verdict#
The grid#
| persona | binding constraint | lands on |
|---|---|---|
| 1. Typed API for other teams | generated contract, enforced at runtime | FastAPI (Litestar if large; Django Ninja if Django exists) |
| 2. Content site with an editor | non-technical editing without a deploy | Django (Flask if editing is external) |
| 3. Internal tool | interface must be nearly free to build | Django — the admin is the deliverable |
| 4. I/O-bound gateway | concurrency, not speed | aiohttp (FastAPI/Starlette/Litestar if the API matters) |
| 5. Flask hitting its ceiling | rewrite unavailable; extensions must survive | Quart if handlers wait; otherwise stay on Flask |
| 6. Data plane | predictability + dependency surface | Falcon |
| 7. Self-hosting small team | maintenance attention, not compute | split — FastAPI, Flask, Falcon or Django by shape |
Three places the personas disagree#
These are not inconsistencies to be resolved. They are the reason a single recommendation would be wrong.
Django is the answer three times and unmentionable four times. For personas 2, 3 and 7 the admin interface is most of the product. For persona 6 it is 24 MB of attack surface in a request path, and for persona 4 its ORM’s async limits are disqualifying. The same property — everything included — is the reason to choose it and the reason not to.
FastAPI’s ecosystem is decisive for the sole maintainer and irrelevant to the data plane. Persona 7 values 102,000 stars as availability of help at 11pm; persona 6 values a single distribution more than any amount of community. Both readings are correct.
Quart is the right answer and a straight loss, depending on one unmeasured fact. For persona 5 it is either the fix or a 2.1× per-request penalty for nothing, and which one depends on whether the handlers wait. S2 cannot say where the crossover is — that needs L5, which is deferred.
Which measurements decide anything#
FastAPI costs 3.9× Starlette per request. Across seven personas, that fact changed one decision — persona 6’s, and there it lost to dependency count rather than to speed.
For personas 1, 2, 3 and 7 the framework’s dispatch cost is dwarfed by a database query, a template render, or an outbound call. This is the most useful thing S3 can say back to S2: the number is correct, it was worth measuring, and it is not what most readers should decide on.
The measurement that changed the most decisions was L4 — the 40-thread ceiling — which is not a performance number at all. It is a correctness trap, and it bites personas 4, 5 and 7.
Who this category serves badly#
Worth naming, because a survey with a good answer for every reader has not looked hard enough:
- A team needing an admin interface without Django’s ORM. The admin is built on Django models. There is no framework here that offers one over an arbitrary data layer, and teams in this position routinely adopt Django’s ORM they did not want in order to get the interface they did.
- A team that wants Litestar’s structure with FastAPI’s ecosystem. It does not exist, the gap is not closing on the evidence in S1, and no persona above resolves it: for most, the answer is to take the ecosystem and live with the flat organization.
- Anyone needing hard latency guarantees. Nothing here offers them. Falcon is the most predictable measured and CPython’s garbage collector still ends the conversation.
Persona 2 — A content site with an editor#
Who#
A site where the words are the product: a publication, a marketing site with real editorial workflow, an association’s public pages. Someone who does not write code needs to change what a page says, on their own, on a Tuesday.
What actually binds#
Somebody must be able to edit content without a deploy. That requires an authenticated interface over stored content, with permissions, and it must exist before launch rather than after.
This is the constraint that makes the category look completely different from persona 1. The generated-OpenAPI question is irrelevant here; nobody is consuming a machine contract. What matters is whether an editing interface exists, and for most of this category the answer is that you build one.
Second constraint: server-rendered HTML is the primary output. Templating is not a side feature.
What the measurements say#
Almost nothing in S2 is relevant to this persona, and saying so is the finding.
Django is 38.26 µs/request against Falcon’s 5.79 — 6.6× — and it does not matter at all. A content page’s cost is template rendering and database queries, both of which dwarf the framework’s dispatch. Choosing Falcon here to save 32 µs and then building an admin interface by hand is trading weeks of work for microseconds nobody will observe.
L1 is mildly relevant and points the same way: Django’s 24.0 MB and 3 distributions is a small price for what arrives with it.
Where it lands#
Django, without much hesitation, and for one reason above the others: the admin. A working CRUD interface over the content model, with permissions, search and filtering, generated from the model definitions, is a thing no other framework in this survey ships. For this persona it is frequently most of the product.
Flask if the site is small and the editing story is external — a headless CMS, a git-based workflow, or a static generator with a build step. Then the framework is doing much less and Flask’s 4.6 MB and seven distributions are a better fit than Django’s floor of structure.
Nothing else. Pyramid’s traversal is a better fit for deeply hierarchical content than a routing table, and its 4,100 stars and 2.2M installs mean the team will be solving problems alone. That is a real trade and for most teams it is the wrong side of it.
Persona 6 — A data-plane service#
Who#
A component other systems depend on synchronously: an auth check, a feature-flag resolver, a routing shim, a metering endpoint. Every millisecond it adds is added to somebody else’s p99, and its failures are somebody else’s outage.
What actually binds#
Predictability over convenience. This persona is judged on tail latency and on not breaking, and both are hurt by machinery that does clever things at request time.
Dependency surface is a security constraint, not a preference. Every transitive package is something that can be compromised or abandoned, and a component in the request path is a high-value target.
What the measurements say#
This is the persona S2 was most useful for, because both binding constraints were measured directly.
L1: Falcon, Bottle and Tornado install exactly one distribution. Falcon’s zero-dependency claim, which S1 could only report from its own documentation, is true. FastAPI brings 10, Litestar 22. For this persona that is not a footnote — it is the difference between a supply chain you can read and one you audit with a tool.
L2: Falcon is the fastest framework measured in both protocols — 5.79 µs WSGI and 6.65 µs ASGI. It is simultaneously the lightest to install and among the fastest to run, which it achieves by not doing what the others do.
The combination is unusual enough to be the persona’s answer on its own. Nothing else here is at the top of both lists.
Where it lands#
Falcon. One distribution, fastest measured, and genuine WSGI/ASGI duality from one codebase, which S1 noted no other framework here manages. The cost is stated plainly by its own maintainers and confirmed by measurement: no validation, no OpenAPI, no injection, all of which this persona builds or does without.
BlackSheep is the interesting near-miss and deserves naming. At 6.28 µs it is second fastest overall and it does ship typed handlers, DI and generated OpenAPI, with a stronger auth model than FastAPI’s. But L1 puts it at 34.4 MB and 14 distributions — third heaviest — which fails this persona’s other constraint. And with 43,000 installs a month it fails the operational one: a component in the request path should not be the only thing in the stack nobody else runs.
Not FastAPI or Litestar here, not because of speed but because the persona is paying for generated documentation and dependency injection it has no use for, in a place where every dependency is a liability.
Persona 5 — Flask at its ceiling#
Who#
A team with a working Flask application, several years old, that has started timing out under load. The handlers are not slow because the code is bad; they are slow because each one waits on something, and every wait occupies a worker.
Nobody is asking for a new framework. They are asking for the application to stop falling over.
What actually binds#
A rewrite is not available. Whatever the ideal framework would be for a greenfield version of this application is not the question, and answering that question is the most common way this persona gets bad advice.
The extension surface is what decides the cost. The application is Flask plus Flask-SQLAlchemy, Flask-Login, Flask-Migrate, Flask-WTF. Migration cost is not measured in handlers; it is measured in which of those still work.
What the measurements say#
This persona meets two S2 findings in tension and needs both.
L4 first: adding async def to Flask handlers does not help, and the reason is
structural. Flask runs an async handler in a private event loop inside a WSGI worker that
stays blocked for the request’s whole life. The syntax works and the throughput does not
move. Trying it and seeing no change is the usual route to concluding async is overrated.
L2 second, and it is the uncomfortable one: Quart costs 84.20 µs/request against Flask’s 40.54 — 2.1×, and Quart is the most expensive framework measured.
Read together, these are not a contradiction, they are the trade. Quart’s per-request cost is higher and its worker is not held during a wait. For a service whose handlers spend 200 ms waiting, 44 µs of extra dispatch is repaid several thousand times over. For a service whose handlers are fast, there is nothing to repay it with and Quart is a straight loss.
So this persona’s first job is to find out which they are — and S2 cannot tell them, because the crossover needs concurrent load, which is L5 and deferred.
Where it lands#
Quart, conditionally and with the condition load-bearing: if the handlers wait, and the extensions in use have Quart-compatible equivalents. It is Flask’s own API, maintained by Pallets — the same organization, not a third-party reimplementation — so the API knowledge in the team’s head transfers intact.
Stay on Flask and add workers if the handlers are CPU-bound or fast. This is a real answer and it is under-given. Quart solves a waiting problem; a team without a waiting problem should not pay 2.1× per request for the cure.
Not FastAPI, which is the usual advice and the wrong one here. It is a different API, a different validation model and a different extension ecosystem — a rewrite wearing a recommendation’s clothes. FastAPI is the right answer for this team’s next application, not this one.
Persona 3 — An internal tool#
Who#
Someone building an operations console, a data-correction tool, an approvals queue. The users are twelve colleagues. Nobody will ever benchmark it, and the entire value is that a human can look at a table and change a row without filing a ticket.
What actually binds#
The interface must be nearly free to build, because the tool is not the business and its budget reflects that. If building it costs a sprint, it does not get built and someone keeps editing the database by hand.
Traffic is a non-constraint. Twelve users generate load that any framework in this survey handles without noticing.
What the measurements say#
S2 is irrelevant to this persona in its entirety, and it needs saying because it is the clearest case in the survey of a measurement that should not influence a decision. Every per-request number in L2 spans 5.79 to 84.20 µs. At this persona’s traffic the difference between the fastest and slowest framework here is unobservable.
Optimizing this choice on S2 is choosing the wrong instrument.
Where it lands#
Django, and more decisively than for persona 2. The admin is not a supporting feature for this reader — it is the deliverable. Model definitions plus registration produce a permissioned CRUD interface with search and filtering, and the tool exists.
Django Ninja alongside it if the tool also needs a small API for a script or a dashboard. 43.3 MB is the largest install in the survey and this persona should not care.
Anything else is a mistake here, and the mistake has a recognizable shape: a team picks FastAPI because it is what they use for services, then spends three weeks building a worse admin. The framework was not wrong for their other work. It is wrong for this.
Persona 4 — An I/O-bound gateway#
Who#
A gateway, an aggregator, a webhook relay, a scraper’s control plane. It receives a request, makes several outbound HTTP calls, combines what comes back. Its own computation is trivial; it spends its life waiting on somebody else’s network.
What actually binds#
Concurrency, not speed. The service must hold many requests in flight while each waits.
A framework that occupies a worker for a request’s whole life caps this persona at
workers concurrent requests, and workers are expensive.
This eliminates the WSGI row wholesale — and eliminates Flask specifically even though
Flask accepts async def, because that syntax runs in a blocked worker and buys nothing
here. S1’s async-syntax-versus-async-concurrency finding is not an academic distinction for
this reader; it is the whole decision.
What the measurements say#
L4 is this persona’s most important measurement and most of the category ignores it.
The def-handler ceiling is 40. A service holding 40 blocking calls in flight is at the
cliff, and the 41st request doubles the group’s latency. For a persona whose outbound calls
take hundreds of milliseconds, 40 in flight is a low request rate — 40 in flight at 500 ms
each is 80 requests per second. This ceiling is reachable at traffic that does not look
like traffic.
The lesson is not “raise the limiter”. It is that every handler in this service must be
async def and every client call must be awaited, and one synchronous library
imported by accident puts the whole service back under a 40-request cap.
L2’s per-request costs are secondary. 8.55 µs versus 33.35 µs is noise beside a 200 ms outbound call.
Where it lands#
aiohttp deserves first mention here and almost nowhere else in this survey. It is client and server in one library sharing connection handling and streaming primitives, and this persona is exactly the case S1 identified for it: “services that are themselves substantial HTTP clients.” The cost is real and stated — no ASGI middleware, no generated documentation, and moving off it later is a rewrite of the application’s edges.
FastAPI or Starlette if the service also publishes an API worth documenting, or if the team wants to stay inside the ASGI ecosystem. Starlette if the outbound work is the whole job and the inbound surface is two endpoints.
Litestar on the same reasoning as FastAPI, at half the per-request cost, with the ecosystem trade S1 named.
Not Flask, not Django, not Bottle, not Pyramid — not because they are slow, but because they are the wrong shape for a job that is defined by waiting.
Persona 7 — Self-hosting a small stack#
Who#
A small team or an individual running the software their own work rests on: an internal bot, a status dashboard, a link shortener, a media pipeline, an MCP server. They deploy it, they maintain it, and they are also its only on-call.
This is the persona that triggered the survey, and per RAIL 0 it is one of seven rather than the survey’s frame. It gets no more weight here than the internal-tool builder or the data-plane operator, and it did not choose which frameworks were compared.
What actually binds#
Maintenance attention is the scarcest resource, not compute. These applications must survive months of neglect and then accept a change from someone who has forgotten how they work. A framework that requires active tending is more expensive than one that is slower.
The stack must be small enough to hold in one head, because there is only one head and it is also doing other things.
Deployment surface matters more than throughput. A single small machine, often shared, frequently a container on a VPS.
What the measurements say#
L1 matters more here than L2, which inverts the usual reading of a performance survey.
Install size and distribution count are proxies for how much can break and how much must be updated. Litestar’s 22 distributions and Django Ninja’s 43.3 MB are not abstractions for this reader — they are things that will need a security update on a Sunday. Falcon, Bottle and Tornado’s single distribution is a real operational property.
L2 is mostly noise at this persona’s traffic, with one exception: Quart at 84.20 µs is the slowest framework measured, and this persona is the least likely to have the load that justifies paying it.
L4 is a live hazard rather than a curiosity. A self-hosted service calling an external
API from a def handler hits the 40-thread ceiling at very low traffic, and the
symptom — everything slow at once, no obvious cause — is exactly the kind of problem a
sole maintainer loses an evening to.
Where it lands#
There is no single answer, and the split is a finding rather than a hedge:
FastAPI when the service has an API surface anyone else calls, including future-you. Its ecosystem is worth more to a sole maintainer than to a large team, for a reason worth stating: when this persona is stuck at 11pm, the size of the answered-questions corpus is the support contract. 102,000 stars is not a popularity metric here, it is availability of help.
Flask when the service is small, synchronous and shaped like a form and a page. Sixteen years of stability and 7 distributions is a good bet for something that must still work after a year of nobody touching it.
Falcon or Bottle when the deployment is constrained or the service must run unattended. One distribution is one thing to update.
Django when the application has real data and needs an interface over it — the same answer as personas 2 and 3, arrived at independently, which is a sign it is right.
Not Litestar, BlackSheep or Robyn, on the same reasoning in each case and none of it about the code: a sole maintainer should not also be the local expert. That is a constraint about support, not quality, and for a larger team it would not apply.
Persona 1 — Publishing a typed API#
Who#
A backend team whose API is used by people they do not sit next to: another squad’s frontend, a partner integration, a mobile client shipped on a store’s timeline. The contract is the product. When it drifts, someone else’s build breaks and they find out in a support channel.
What actually binds#
The API description must be generated, not maintained. A hand-written OpenAPI file is a second source of truth, and second sources of truth drift — this is the failure the persona is organized around, not a nice-to-have.
That constraint alone eliminates most of the category. Starlette, Flask, Bottle, Pyramid, Falcon, Quart, aiohttp and Tornado generate nothing. For Flask the extension ecosystem offers several answers and has settled on none, which means choosing one is a decision this team has to make and defend.
Second binding constraint: the contract must be enforced at runtime, not merely described. A schema that documents a field the handler does not actually validate is worse than no schema, because it is believed.
What the measurements say#
FastAPI’s 33.35 µs/request (L2) is the number this persona is most often warned about and should care about least. Their handlers talk to a database; a 5 ms query makes 24.8 µs of framework overhead 0.5% of the request. The overhead is real and it is not their constraint.
The number that should interest them is L3’s: Pydantic validates in 0.993 µs, faster
than json.loads parses without validating. The runtime enforcement they require is not
something they are paying for. It is free, and then some.
Where it lands#
FastAPI, on ecosystem rather than merit — and the distinction should be stated plainly. Litestar generates the same documentation, costs half as much per request (16.97 µs) and has a better structural answer for a large surface. What FastAPI has is 102,000 stars’ worth of answered questions, integrations, and developers who already know it.
Litestar if the API is large enough that FastAPI’s flat organization has become the problem — layered configuration and controllers are a real answer — and the team can absorb being early.
Django Ninja if a Django application already exists. Adding a service to avoid adding a dependency is the more expensive mistake.
Not Starlette. This persona needs exactly the layer Starlette declines to provide, and would rebuild a thinner version of it.
S4: Strategic
Viability#
Every other comparison in this survey assumes the framework still exists. These are the signals that bear on whether it will, and what it would cost to be wrong.
What was measured#
Viability evidence is registry and API data, not benchmarks. Per the plan in
../S2-comprehensive/measurement-plan.md, S1’s fourth open question — Litestar’s issue
backlog — belongs here rather than in S2, because dressing GitHub data as a measurement
would misrepresent what it is.
Fetched from the GitHub REST API on 2026-08-28, rung cited:
- Contributor concentration — the share of all commits made by the single most active contributor, and by the top three. A bus-factor proxy.
- Issue close ratio — closed issues against closed plus open, using the search API with
type:issueso pull requests are excluded. - Release cadence and last release — from the PyPI JSON API (S1’s
observed-data.md).
A correction to the issue counts#
S1 reported “Litestar’s 319 open issues” and asked whether that was backlog or capacity.
The number is wrong, and the way it is wrong is instructive. open_issues_count on
GitHub’s repository endpoint includes pull requests. Querying the search API with
type:issue gives Litestar 220 open issues and 1,255 closed.
Every “open issues” figure in S1 carries the same inflation, because they all came from the same field. They were used comparatively and the comparison survives — the error is consistent across rows — but the absolute numbers are issues-plus-PRs and S4’s are not.
How to read a bus factor#
A high top-one share is a risk signal, not a verdict. It reads differently depending on what sits behind the individual:
- Behind an organization with more than one project — Pallets maintaining Flask, Quart, Jinja, Werkzeug and Click — a concentrated commit history is a division of labor.
- Behind a foundation with a governance document and a release process — Django — it is close to irrelevant.
- Behind one person and no institution, it is the whole risk.
The number is the same in all three cases. S4 says which case each project is in, because that is the part the number cannot tell you.
Bottle — viability#
Position: maintenance only. The clearest decay signal in the survey, and it is not ambiguous.
| signal | value | reading |
|---|---|---|
| last release | 2025-06-15 | 14 months before measurement |
| issue close ratio | 75.1% (215 open, 650 closed) | lowest here by 10 points |
| top-1 commit share | 78.9% | concentrated |
| version | 0.13.4 | still 0.x after 17 years |
| installs | 10 million/mo | seventh here, ahead of Quart |
Three signals agree#
S1 flagged the release gap and declined to call the project dead, noting 10 million installs a month and a repository pushed to in July 2026. That caution was right, and S4 can now sharpen it because two further signals point the same way.
Bottle is the only project in this survey without a release in over a year. Every other one shipped within six months; six shipped within one month.
Its close ratio is the lowest measured, at 75.1% against a survey median well above 90%. 215 open issues on a project this small is a backlog that is not being worked down — and unlike Litestar, whose 85.1% close ratio and 22.6% concentration read as active triage, Bottle has neither the throughput nor the contributor spread to suggest the backlog is being managed.
0.13 after seventeen years is not a version number heading anywhere.
What it does not mean#
Bottle is not abandoned and nothing here says it is. Ten million installs a month is real use, the repository sees activity, and the code works.
The distinction that matters is maintenance versus development. Bugs may get looked at; new capability is not arriving, and a security-relevant issue would be entering a queue with 215 things already in it.
What choosing Bottle costs#
S3 sent two personas toward Bottle for its single-file property — the data-plane operator and the self-hosting maintainer, both of whom value one distribution and 0.4 MB.
That property is real and this is its price. The single-file deployment story is unmatched in this category; the project providing it is in maintenance. Both facts are true and a reader choosing Bottle should be choosing it knowing the second one.
For a vendored, pinned, single-file dependency doing a stable job, a project in maintenance is a smaller problem than it would be for a framework expected to grow with an application. That is the case where Bottle still makes sense, and it is narrower than its install count implies.
FastAPI — viability#
Position: safe now, structurally concentrated, and carrying two risks at once.
| signal | value | reading |
|---|---|---|
| top-1 commit share | 51.8% | over half the project by one person |
| top-3 share | 75.8% | |
| issue close ratio | 100% (1 open, 3,547 closed) | intensively maintained today |
| last push | 2026-08-26 | two days before measurement |
| version | 0.141.1, 317 releases since 2018 | no stability commitment after eight years |
| institution | none | not a foundation, not an organization |
Concentration against maintenance#
FastAPI is the most-starred web framework in Python — 102,000, more than Django’s 89,000 or Flask’s 72,000 — and its commit history is more concentrated than Sanic’s, Litestar’s, Starlette’s, aiohttp’s, Pyramid’s or Flask’s.
Its maintenance right now is not in question. A 100% issue close ratio with one open issue against 3,547 closed is an unusually well-tended tracker, and the repository was pushed to two days before measurement.
The risk is entirely about succession. Nothing institutional sits behind the project — no foundation, no stewardship organization, no company with a published commitment. Compare Django: 12.6% top-1 share, a foundation, a governance document, a security team and an LTS schedule. If Django’s most active contributor stopped tomorrow, a process exists. For FastAPI, nobody has said what happens.
The compounding risk#
S1 found FastAPI still at 0.x after eight years and 317 releases, with minor versions carrying breaking changes, while Starlette beneath it reached 1.0.
A reader usually assumes at least one of the two risks is covered: either the project is institutionally backed, or it has committed to semantic stability. FastAPI has done neither, and the two compound — a breaking minor release is harder to absorb from a project with no succession plan.
What this does not mean#
It is not a prediction of abandonment, and nothing in S3 changes because of it. Six of seven personas that reach for FastAPI should still reach for it; its ecosystem is the reason, and the ecosystem is real.
It means the risk should be named rather than assumed away by the star count. A reader choosing a foundation for something that must outlive its authors’ interest is choosing between a 51.8% concentration with no institution and a 12.6% concentration with one — and the star counts point the wrong way.
S4 verdict#
Litestar’s issue backlog#
Litestar’s backlog is triage, not capacity failure.
220 open issues against 1,255 closed — an 85.1% close ratio — on a project whose commit history is the second-least concentrated in the survey (top-1 22.6%, behind only Django). This is a project with more contributors sharing more of the work than FastAPI has, closing five issues for every one it holds open.
S1 read the raw open-issue count as ambiguous evidence, “consistent with a project whose surface area has outgrown its maintainer capacity, and equally with an actively triaged tracker on a young project.” The second reading is the right one. 85.1% is a healthy ratio for a three-year-old project shipping 22 first-party components, and the number that looked alarming was inflated by pull requests in the first place.
Litestar’s risk is adoption, not maintenance. That was S1’s conclusion for a different reason, and S4 confirms it on better evidence.
FastAPI’s bus factor#
One person has written 51.8% of FastAPI’s commits.
This is the most-starred web framework in Python — 102,000 stars, more than Django or Flask — and its commit history is more concentrated than Sanic’s, Litestar’s, Starlette’s, aiohttp’s, Pyramid’s or Flask’s. The top three account for 75.8%.
Nothing behind it is institutional. FastAPI is not under a foundation, not under Pallets, and not under a company with a published stewardship commitment. Compare Django at 12.6% top-1 with a foundation, a governance document, a security team and a published LTS schedule.
This is not a prediction that FastAPI will be abandoned. Its close ratio is 100% and its last push was two days before measurement — it is intensively maintained right now. It is a statement about what happens if that stops, and the answer is that nobody has said who would pick it up.
It compounds with the version number. S1 found FastAPI still at 0.x after eight years and 317 releases, while Starlette beneath it reached 1.0. A project with a concentrated bus factor and no stability commitment is carrying two risks that a reader usually assumes at least one of is covered.
Starlette changed hands#
S1 established by API evidence that encode/starlette now resolves to Kludex/starlette
with parent: null and source: null on a repository created 2018-06-25 — a transfer,
not a fork. The project moved from Encode, Tom Christie’s organization, to its principal
maintainer Marcelo Trylesinski.
Why it matters more than it first appears: Starlette is the most-installed package in this survey at 676 million a month, and FastAPI is built on it. A very large fraction of Python web serving rests on this library whether or not anyone chose it.
The signals are reassuring rather than alarming. Starlette’s close ratio is 98.7% with only 10 open issues, its commit concentration is a moderate 31.5%, and reaching 1.0 after six years at 0.x is a maintainer taking on a stability obligation rather than shedding one. A transfer to an active maintainer who then commits to semantic versioning is a healthy transition, not a distress signal.
What it does mean concretely: documentation, links and organizational trust that predate the move point at the old path, and the governance question — who maintains it if Trylesinski stops — has moved from an organization to an individual.
Bottle is in maintenance, not development#
Three independent measurements agree:
- No release since 2025-06-15 — fourteen months, the only project here over a year.
- 75.1% close ratio — the lowest in the survey by 10 points.
- 78.9% top-1 commit share, with 215 open issues.
Still at 0.13 after seventeen years, still drawing 10 million installs a month. S1 called it “the one project here whose maintenance cadence has visibly slowed” and declined to call it dead. S4 keeps that judgment and sharpens it: Bottle is in maintenance, not development, and the open-issue backlog is not being worked down.
For S3’s persona 6 and persona 7, who were pointed at Bottle for its single-file property, this is the cost of that choice and it should be priced in. The property is real and the project is not growing.
Where the institution matters#
Three projects have top-1 shares above 78% and they are not the same risk:
- Quart, 82.4% — but maintained by Pallets, alongside Flask, Jinja, Werkzeug and Click. A concentrated history inside a multi-project organization is a division of labor. This is the lowest-risk high-concentration project here, and it is why S3 could recommend Quart to persona 5 without hedging on survival.
- Tornado, 82.3% — one maintainer, sixteen years, 90.4% close ratio, a release three weeks before measurement. Concentrated and demonstrably durable. The risk is real and it has not materialized in a decade and a half.
- BlackSheep, 82.1% and only 26 contributors total — the smallest contributor base in the survey by a wide margin, with 43,000 installs a month. S2 measured it as the second-fastest framework here. This is the survey’s sharpest quality-to-institution gap: excellent code, one person, almost nobody else running it. S3’s persona 6 declined it for exactly this reason.
Django has institutional depth#
12.6% top-1, 29.3% top-3 — less than a third of all commits from the three most active contributors, where every other project here is between 50% and 90%. A foundation, a published governance process, a security team with an advisory process, and an LTS release schedule.
For a reader whose question is “will this still be maintained when my application is eight years old,” Django is the only answer in this survey where survival does not depend on the health of one or two people.
That is the strategic counterweight to everything S2 measured. Django is 6.6× more expensive per request than Falcon and it is the only framework here that has already survived a generational handover of its own maintainers.
Strategic verdicts#
| framework | five-year outlook | what would cost you |
|---|---|---|
| Django | safest in the survey — foundation, LTS, 12.6% concentration | verbosity and async limits, not survival |
| Flask | very safe — Pallets, 100% close ratio, 16 years | stagnation risk, not abandonment |
| Starlette | safe, newly individual — 1.0, 98.7% close, transferred | governance now rests on one maintainer |
| FastAPI | safe now, concentrated — 51.8% top-1, no institution, 0.x | no named successor; breaking minors |
| Quart | safe by association — Pallets stewardship | small community, 0.x after nine years |
| aiohttp | safe — 34.9%, 95.1% close, 13 years | outside ASGI; migration is a rewrite |
| Litestar | healthy, unproven — 85.1% close, 22.6% top-1 | adoption, and 22 distributions |
| Tornado | durable, static | one maintainer; a shrinking brief |
| Falcon | stable, small constituency — 87.2% close, 13 years | narrow by design |
| Sanic | maintained, plateaued — 93.1% close, 21.4% top-1 | attention won in 2017 has not converted |
| Pyramid | maintained, niche | a shrinking pool of people who know it |
| Django Ninja | tied to Django’s health, 66.0% top-1 | one maintainer over a safe foundation |
| BlackSheep | excellent and alone — 26 contributors | you are the support team |
| Bottle | maintenance only — 14 months, 75.1% close | the single-file property, and no growth |
| Robyn | watch, do not ship — 67.8%, 19,000 installs/mo | platform wheels; four orders of magnitude below FastAPI |
Popularity and resilience point opposite ways#
S1 found that “the strongest technical case in this category is not the one with the ecosystem.” S4 adds a second dislocation on a different axis: the most popular framework has one of the most concentrated maintainer bases, and the least fashionable one has by far the least.
FastAPI is at 51.8% and Django at 12.6%. Popularity and institutional resilience are not merely uncorrelated in this category — on this evidence they run in opposite directions, because the projects that grew fastest grew around an individual and the one that grew slowest grew a foundation.
For most readers most of the time this changes nothing, and S3’s personas mostly do not turn on it. It matters for the reader choosing a foundation for something that must outlive its authors’ interest — and that reader is not well served by the star count.
Starlette — viability#
Position: safe, and the governance question in this survey most worth watching.
| signal | value | reading |
|---|---|---|
| top-1 commit share | 31.5% | moderate, healthier than FastAPI’s 51.8% |
| issue close ratio | 98.7% (10 open, 770 closed) | very well tended |
| version | 1.6.0 | reached 1.0 after six years at 0.x |
| installs | 676 million/mo | the most-installed package in this survey |
| repository | transferred, Encode → Kludex | organization to individual |
Why it matters#
Starlette has 12,600 stars, a twelfth of FastAPI’s. It is also the most-installed package in this survey, because every FastAPI install pulls it. A very large fraction of Python web serving rests on this library whether or not anyone chose it.
So its governance is not a niche question. It is load-bearing for a category.
The transfer#
S1 established by API evidence that encode/starlette now resolves to Kludex/starlette,
with parent: null and source: null on a repository created 2018-06-25. Those fields
distinguish a transfer from a fork: the repository moved, it was not copied. The project
went from Encode — Tom Christie’s organization, which also produced Django REST Framework
and httpx — to its principal maintainer, Marcelo Trylesinski.
Reading the signals#
Three signals point the same way:
- 98.7% close ratio with only 10 open issues. This is a tracker under control.
- 31.5% top-1 concentration — moderate for this survey, and better than the framework built on top of it.
- It reached 1.0. After six years at 0.x, the maintainer took on a semantic-versioning obligation rather than shedding one. A project in distress does not do that.
A transfer to an active maintainer who then commits to stability is a healthy transition. The inverted case — a project moving to an organization as its individual maintainer steps back — is the one that signals trouble, and this is not that.
What changed concretely#
Documentation, blog posts and links predating the move point at encode/starlette. And the
succession question has moved from an organization with several projects and several
maintainers to one person. That is a genuine reduction in institutional depth for a library
underneath the most popular framework in Python. Every health signal is currently good;
the depth behind them is thinner than it was.
Viability signals#
GitHub REST API, fetched 2026-08-28. Rung cited — this is somebody else’s measurement of
their own platform, read carefully, not something this survey ran.
Issue counts use the search API with type:issue, so pull requests are excluded. That
is why these differ from S1’s figures, which came from open_issues_count and include PRs.
| framework | contributors listed | top-1 share | top-3 share | open issues | closed | close ratio |
|---|---|---|---|---|---|---|
| Django | 100+ | 12.6% | 29.3% | — | — | uses Trac |
| Sanic | 100+ | 21.4% | 50.2% | 101 | 1,370 | 93.1% |
| Litestar | 100+ | 22.6% | 55.4% | 220 | 1,255 | 85.1% |
| Starlette | 100+ | 31.5% | 66.8% | 10 | 770 | 98.7% |
| aiohttp | 100+ | 34.9% | 63.1% | 160 | 3,080 | 95.1% |
| Pyramid | 100+ | 36.2% | 74.4% | 73 | 992 | 93.1% |
| Flask | 100+ | 40.7% | 72.9% | 0 | 2,763 | 100% |
| Falcon | 100+ | 46.6% | 76.2% | 146 | 993 | 87.2% |
| FastAPI | 100+ | 51.8% | 75.8% | 1 | 3,547 | 100% |
| Django Ninja | 100+ | 66.0% | 73.0% | 132 | 911 | 87.3% |
| Robyn | 87 | 67.8% | 77.0% | 53 | 546 | 91.2% |
| Bottle | 100+ | 78.9% | 83.3% | 215 | 650 | 75.1% |
| BlackSheep | 26 | 82.1% | 90.2% | 3 | 304 | 99.0% |
| Tornado | 100+ | 82.3% | 85.4% | 181 | 1,696 | 90.4% |
| Quart | 100+ | 82.4% | 86.0% | 24 | 260 | 91.5% |
“100+” is the API’s first page; the exact tail was not paged and is not needed for a concentration reading.
Django’s issue columns are empty on purpose. Django tracks bugs in Trac at
code.djangoproject.com, not GitHub, so its GitHub issue counts are zero for a reason that
has nothing to do with activity. Reporting 0 open / 0 closed as a finding would have been
straightforwardly wrong, and it is the kind of wrong that looks like data.