Find blocking work inside the event loop before raising FastAPI concurrency. Classify each dependency as asynchronous I/O, blocking I/O or CPU work, then inspect where it actually runs. An async def route can still block every other coroutine when it calls a synchronous utility directly. More concurrent requests can magnify that bottleneck rather than remove it.
Follow the called function, not only the route keyword
An async network client can yield while waiting. A blocking SDK call or time.sleep does not yield merely because the surrounding function is async. FastAPI documents that ordinary def path operations and dependencies run through its threadpool, while ordinary utility functions called directly follow normal Python execution. That distinction explains why moving a route to async without changing its blocking dependencies can regress unrelated-request progress.
FastAPI: async routes, synchronous functions and threadpool behavior
Inventory database clients, provider SDKs, file reads, image processing and serialization. Record whether each offers an async API, requires deliberate offloading or belongs in a separate worker. Large CPU work has a different scaling problem from waiting on a remote service. Thread offloading can keep a loop responsive for blocking I/O, but it does not make all CPU work parallel or remove threadpool and memory limits.
Executed event-order fixture
Our isolated asyncio fixture starts a synthetic dependency and an unrelated coroutine together. In the blocking case, the dependency uses time.sleep; the unrelated coroutine makes progress only after it finishes. In the yielding case, the dependency awaits asyncio.sleep; the unrelated coroutine progresses before the dependency finishes. The result records event order rather than claiming HTTP throughput or a production latency improvement.
Actual local blocking order:
dependency_started -> dependency_finished -> unrelated_progress
Actual local yielding order:
dependency_started -> unrelated_progress -> dependency_finishedThe sleep durations are fixture inputs used to make scheduling visible, not a benchmark of a real SDK. This experiment isolates cooperative scheduling on one loop. It does not exercise FastAPI routing, ASGI middleware, database pooling or threadpool saturation. Use that boundary honestly when connecting the mechanism to an application investigation.
| Layer | Observed or needed result | Boundary |
|---|---|---|
| Blocking sleep fixture | Unrelated progress after dependency | Executed local loop |
| Awaited sleep fixture | Unrelated progress before completion | Executed local loop |
| HTTP endpoint comparison | Latency and event-loop delay | Not executed |
| Capacity decision | CPU, pool and thread limits | Measure representative workload |
Measure the independent request as well as the slow task
In a disposable HTTP service, run a slow synthetic endpoint alongside a small unrelated health or read endpoint. Capture event-loop delay, the small endpoint's latency, pool waits and CPU utilization. If the slow dependency is asynchronous but the database pool is exhausted, changing route syntax will not help. If serialization consumes the loop after the network response arrives, the provider latency is not the entire bottleneck.
Keep concurrency, worker count, payload size and connection limits fixed for the baseline. Change one execution boundary at a time, then compare under the same workload. A single faster run does not establish capacity. Record sample counts and the environment and avoid turning a local artificial delay into an expected customer performance number.
Offload with cancellation and capacity in mind
If a blocking library must remain, use a supported thread offload boundary and a bounded concurrency policy. Cancelling the awaiting coroutine may not stop the underlying blocking operation; inspect the library's timeout and cancellation behavior. For long CPU or durable work, a task queue or separate execution service can provide a clearer ownership boundary than holding an HTTP request open indefinitely.
The saved event-order fixture provides actual local evidence for blocking versus yielding. The application change still needs a representative endpoint test with the selected client library and installed framework versions. Raise concurrency only after identifying the constrained resource and its safe limit. A useful tuning record explains where requests waited, which change altered that wait and which resource became the next bound.
Cloud Run cold start measurement
Observability for small teams: start with the failed customer task
Bring one API or data workflow and a sanitized failure example for a bounded implementation assessment.
Why can an async FastAPI route still block other requests?
Direct synchronous calls inside async code still run on the event loop unless explicitly offloaded; inspect dependency execution and resource waits before tuning concurrency.
Does async def make a blocking SDK asynchronous?
No. Use its async API or an explicit supported offload boundary with timeouts and capacity limits.
Does a normal utility function automatically use FastAPI’s threadpool?
No. FastAPI’s route/dependency handling differs from a normal function called directly by your code.
