Library / SDK
Kludex/fastapi-tips avatar
Kludex/fastapi-tips

fastapi-tips: a numbered list of things the FastAPI docs leave implicit

FastAPI Tips by The FastAPI Expert!

3,628 stars147 forksUnknownLicense varies

At a glance

What is it?
A single README of numbered tips on performance, WebSockets, testing and lifespan, each one short enough to apply while you are still in the file that needs it.
Who is it for?
The value of this repository is that it is made of self-contained fixes. There is no architecture to adopt and no dependency to add, only a numbered list where each entry names a problem, states the cost and shows the replacement code.
Can I use it commercially?
Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
Is it still maintained?
Yes. The repository last received commits 23 days ago.
What is it written in?
GitHub does not report a main language for this repository.

Answers come from the project's GitHub data, last synced on September 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A repository that is one file

The repository contains a `.gitignore` and a `README.md`. There is no package, no module and no test suite, because there is nothing to install. The README is titled as a hundred and one tips for FastAPI, numbered sequentially, and the project invites additions through issues or pull requests.

That format is the whole design. Each tip is short, states a specific cost, and ends with code you can paste. There is no table of contents to maintain and no versioning scheme to follow, which is why the repository can accumulate a hundred entries without them drifting into a book.

The authorship is the other thing to note. Kludex is the maintainer behind Mangum, the ASGI adapter for AWS Lambda, and the README asks for GitHub Sponsors to fund more content of this kind. So the format is chosen deliberately: advice that is short enough to write in an evening and useful enough to be worth sponsoring.

It also asks readers to watch the repository for new tips, which is a reasonable request given that entries arrive one at a time.

Tip one is a two-word install

The first entry says Uvicorn does not come with `uvloop` and `httptools`, which are faster than the default asyncio event loop and the default HTTP parser, and installs them:

bash
pip install uvloop httptools

The stated behaviour is that Uvicorn will use them automatically if they are present in the environment, so there is no configuration change to make. That is the shape of the whole entry: one install, no code, immediate effect.

The one caveat is platform-specific and is flagged as a warning. `uvloop` cannot be installed on Windows. The README handles the common developer setup where Windows is local and Linux is production by suggesting a PEP 496 environment marker so the package is skipped on Windows, giving `uvloop; sys_platform != 'win32'` as the example. That is a better answer than the usual advice to develop in a virtual machine, though it does mean the dependency list differs between your laptop and your servers.

The thread pool ceiling, which is the tip that changes behaviour

The second entry is the one most likely to affect an existing application, because it describes a limit you can hit without noticing.

The claim is that there is a performance penalty when you use non-async functions in FastAPI, and that async should be preferred. The mechanism is named: FastAPI will call `run_in_threadpool` for a synchronous handler, which internally uses `anyio.to_thread.run_sync` to run the function in a thread pool. The consequence is the part worth remembering: there are only 40 threads available in that thread pool, and if you use all of them, the application will be blocked.

Forty is not many when a handler makes a blocking call to a database or an HTTP service. A service where synchronous endpoints each wait on I/O will exhaust the pool and then stop serving, which presents as a mysterious hang rather than an error.

The fix is shown in full, and it works by raising the token limit on anyio's default thread limiter from the lifespan handler:

py
import anyio
from contextlib import asynccontextmanager
from typing import Iterator

from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI) -> Iterator[None]:
    limiter = anyio.to_thread.current_default_thread_limiter()
    limiter.total_tokens = 100
    yield

app = FastAPI(lifespan=lifespan)

Note what this entry does not say: raising the limit is not free, since each thread carries stack memory, so the number should be chosen against your concurrency requirements rather than set high by habit.

Two WebSocket entries that are really one change

Entries three and four describe the same rewrite from two angles.

The third says most examples on the internet read WebSocket messages with `while True`, and attributes the uglier notation to Starlette's documentation having omitted the `async for` form for a long time. The replacement is a loop over the connection's text iterator:

py
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> None:
    await websocket.accept()
    async for data in websocket.iter_text():
        await websocket.send_text(f"Message text was: {data}")

The fourth entry is the payoff. With `while True` you have to catch `WebSocketDisconnect` yourself, and the `async for` notation catches it for you. If you need to release resources when the socket disconnects, the exception is what you do that in, so the two entries have to be read together.

The version nuance here is the most useful detail in the pair. On an older FastAPI version only the receive methods raise `WebSocketDisconnect`, while the send methods do not. In current versions all methods raise it, and in that case you need to put the send calls inside the `try` block too. That is the sort of difference that decides whether your cleanup code runs, and it is exactly the kind of thing a numbered list is good at surfacing.

Testing async apps and where state should live

The fifth entry is about tests, and its argument is consistency. If your application uses async functions, it is easier to use HTTPX's `AsyncClient` than Starlette's `TestClient`. The tip shows both against the same endpoint: the synchronous client form calls `get` directly, and the asynchronous form wraps the app with `ASGITransport`, awaits the request inside an `async with` block, and is driven by `anyio.run(main)`.

There is a follow-on problem the entry handles rather than ignores. If your app uses lifespan events, whether through `on_startup`, `on_shutdown` or the `lifespan` parameter, a bare `AsyncClient` will not run them, because they fire during application startup and nothing here starts the application. The tip points at the `asgi-lifespan` package, whose `LifespanManager` wraps the app so those events run, and shows the combination with `AsyncClient`. It closes by asking readers to support the package's creator, Florimond Manca, through GitHub Sponsors.

The sixth entry is a deprecation in practice. FastAPI now supports lifespan state, which the README describes as the standard way to manage objects that need creating at startup and using during the request-response cycle. The guidance is that `app.state` is no longer recommended and lifespan state should be used instead. The example it gives shows the lifespan pattern with an `AsyncClient` created inside the lifespan function and closed on exit.

Those two entries are the ones to apply first in an existing codebase, since `app.state` is common in older FastAPI projects and the change is mechanical rather than behavioural.

Editorial conclusion

The value of this repository is that it is made of self-contained fixes. There is no architecture to adopt and no dependency to add, only a numbered list where each entry names a problem, states the cost and shows the replacement code. The thread pool tip is the one most likely to change behaviour you already have, because a synchronous handler is a common convenience that quietly becomes a ceiling at concurrency. The WebSocket and lifespan entries are smaller and just as easy to get wrong. Since there is no code in the repository beyond the README itself, each tip is complete as written, which means the tradeoff is that you cannot check whether an entry still applies to the FastAPI version you run.

Frequently asked questions

How many threads does FastAPI's thread pool have by default?

The repository states that there are only 40 threads available in the thread pool FastAPI uses for synchronous handlers, and that using all of them blocks the application. The recommended fix is to raise the limit on anyio's default thread limiter to 100 inside the lifespan handler.

Why should I install uvloop and httptools?

The first tip states that Uvicorn does not ship with them by default and that they are faster than the default asyncio event loop and HTTP parser. Installing them with `pip install uvloop httptools` is enough, since Uvicorn picks them up automatically when they are present. `uvloop` cannot be installed on Windows, so an environment marker is suggested for mixed local and production setups.

Should I use TestClient or AsyncClient to test a FastAPI app?

The tip recommends HTTPX's AsyncClient when the application uses async functions, since it keeps testing consistent with the code under test. If the app has lifespan events, a bare AsyncClient will not run them, and the `asgi-lifespan` package with its LifespanManager is needed to trigger startup and shutdown.

Is app.state deprecated in FastAPI?

The README does not use the word deprecated, but it says `app.state` is not recommended for use anymore and that lifespan state should be used instead. Lifespan state is described as the standard way to manage objects created at startup and used during the request-response cycle.

What does the async for form of a FastAPI WebSocket loop actually change?

It removes the need to catch `WebSocketDisconnect` yourself, since iterating with `async for` handles the disconnect. That matters for cleanup code. The README also notes that on older FastAPI versions only receive methods raise the exception, while in current versions all methods do, which decides whether send calls need to sit inside the `try` block.

Official sources

  1. Issues
  2. Kludex/fastapi-tips on GitHub
  3. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/kludex-fastapi-tips.svg)](https://hysenlabs.com/projects/kludex-fastapi-tips)