数据分析

FastAPI

按症状路由到 FastAPI 子指南,排查卡顿、验证错误与异步陷阱。

它能做什么

把 FastAPI 问题路由到一份独立的子指南(async.md、pydantic.md、database.md、deployment.md 等),覆盖事件循环阻塞、Pydantic v1 到 v2 迁移、Depends 缓存、307 重定向、连接池耗尽、测试事件循环已关闭、uvicorn/gunicorn worker 等情形;交付前对照九条输出门禁校验:async 路径不能有阻塞调用、每个外部调用都要显式超时、声明请求与响应模型、连接池规模不超过数据库上限等。生成代码会按 ~/Clawic/data/fastapi/config.yaml 中记录的栈选型(SQLAlchemy 同步/异步、uvicorn 或 gunicorn、JWT 或 session cookie、TestClient 或 httpx AsyncClient)进行调整。

什么时候用它

  • 全站路由同时变慢,健康检查也跟着卡住
  • 看似合法的请求返回 422,或 POST 收到 307
  • SQLAlchemy 抛 QueuePool limit of size 5 overflow 10 reached
  • pytest-asyncio 报 RuntimeError: Event loop is closed

技能文档

User preferences live in ~/Clawic/data/fastapi/config.yaml (see Configuration); nothing else is stored on the user's machine. If you have data at an old location (~/fastapi/ or ~/clawic/fastapi/), move it to ~/Clawic/data/fastapi/.

When To Use

  • Writing or reviewing FastAPI code: routers, dependencies, request/response models, middleware
  • Diagnosing latency and stalls: one blocking call, an exhausted pool, a saturated threadpool
  • Modelling data at the edge: validation errors, serialization, ORM objects, pydantic v1 → v2
  • Wiring auth, settings, database sessions, and background work into the dependency graph
  • Getting a service to production: workers, proxies, shutdown, health checks, logging, limits
  • Fixing tests that pass wrongly, hang, or die with a closed event loop
  • Not for Django, Flask, or plain-Python problems — django, flask, py own those

Quick Reference

SituationPlay
Every route got slow at once, health check includedThe loop is blocked: a sync call inside async def, or all threadpool slots busy → async.md
Choosing def vs async def for an endpointAny blocking call in the body → def; all-await → async defasync.md
CPU work, image processing, big parsing in a requestOff the loop and off the threadpool: process pool or queue → async.md
422 on a request that looks correctRead loc in the error body: alias, nesting, or a nullable field with no default → pydantic.md
.dict(), parse_obj, @validator, orm_mode no longer workPydantic v2 renames and behavior changes → pydantic.md
ORM instance will not serialize into the responsefrom_attributes plus a response model, or return the model you declared → pydantic.md
Depends runs more or fewer times than expectedPer-request cache, use_cache=False, singletons in lifespan → dependencies.md
Current user, scopes, tokens, password hashingauth.md
/users/me hits the {user_id} route; 307 on POST; router prefixesDeclaration order and trailing slashes → routing.md
Where files go, circular imports, app factory, one app or manystructure.md
Env var ignored, secret in the wrong place, per-environment configsettings.md
Sessions, pools, transactions, N+1, migrations, async ORM errorsdatabase.md
Error bodies inconsistent, custom handlers, validation error shapeerrors.md
CORS headers missing, middleware order, request IDs, body read twicemiddleware.md
Tests: dependency overrides, lifespan, async client, DB isolationtesting.md
Workers, gunicorn vs uvicorn, root_path, graceful shutdown, containersdeployment.md
Slow under load with the loop healthy: serialization, caching, paginationperformance.md
Large uploads or downloads, SSE, streaming JSON, client disconnectsstreaming.md
WebSocket drops, broadcast across workers, authenticating a socketwebsockets.md
Work after the response: BackgroundTasks vs a real queue, retriesbackground.md
Docs wrong or missing, generated clients, operation ids, examplesopenapi.md
Logs with no request context, health vs readiness, metrics, tracingobservability.md
Ownership checks, mass assignment, SSRF, path traversal, security headerssecurity.md
Server-rendered HTML: Jinja2, forms, HTMX partials, static filestemplates.md
Anything elseReproduce in one file: one route, no middleware, no DB — then add back one layer at a time until it breaks

Each file is one sub-job and self-contained: read SKILL.md by default, open exactly one guide when the situation matches.

Core Rules

  1. async def obliges the whole chain to await. One blocking call inside an async endpoint — psycopg2, requests, time.sleep, a large open().read() — freezes every concurrent request on that worker, not just its own. If any step blocks and cannot be replaced, declare the endpoint def and let Starlette move it to the threadpool.
  2. The threadpool has 40 slots per worker. Every def endpoint and def dependency shares AnyIO's default limiter of 40 threads: ceiling = 40 concurrent blocking calls per worker process, the 41st waits with no error message. Concurrency budget = 40 × workers; raise total_tokens in lifespan only after measuring.
  3. Dependencies cache per request, singletons live in lifespan. Depends(get_x) used in three places runs once per request and again on the next one. Anything that must exist once per process — DB engine, HTTP client, cache client — is created in the lifespan handler and stored on app.state; @lru_cache on a factory is the escape hatch for cheap, argument-free objects like settings.
  4. Validate at the edge, exactly twice. Request model in, response model out, plain objects in between. response_model re-validates everything you return, so a response is a second full validation pass — worth it as a contract, and skippable (response_model=None plus an explicit Response) only on a route you have profiled.
  5. Size the pool against the database, not the app. SQLAlchemy defaults to pool_size=5, max_overflow=10 = 15 connections per process, so total = workers × 15. Four workers = 60 of Postgres's default 100 max_connections; a second replica of the same service exhausts the server and every route starts failing on connect.
  6. One event loop and one memory space per worker. Lifespan runs once per worker, so 4 workers means 4 pools, 4 in-process caches, and 4 disjoint sets of WebSocket clients. Any state two requests must share goes to Redis or the database — a module-level dict is a per-process cache with a 1-in-N hit rate.
  7. Every outbound call carries a timeout. httpx.AsyncClient(timeout=5.0), DB statement timeouts, lock acquisition bounds. Without one, a hung upstream converts into an unbounded queue of pending tasks and the worker dies holding requests it will never answer.
  8. Raise errors, never return them from helpers. raise HTTPException(404, ...) travels through the exception handlers and produces the same body everywhere; a JSONResponse returned from a service function only works if the endpoint happens to pass it through, and it skips every handler and response-model check.
  9. Middleware order is the reverse of registration. The last add_middleware call is the outermost layer. CORS registered first ends up innermost, so a 500 raised above it reaches the browser with no CORS headers and the real error is invisible in devtools — register CORS last, and add an exception handler so failures are returned rather than propagated.

Version Floors

Behavior that changed with the framework, stated as the floor that gates it. Guides carry additional floors inline in the same form.

FeatureNeeds
Return-type annotation used as the response modelfastapi >=0.89
lifespan context manager (replaces @app.on_event)fastapi >=0.93
Annotated[X, Depends(...)] dependency stylefastapi >=0.95
Pydantic v2 models and model_configfastapi >=0.100
yield dependency resources no longer usable inside BackgroundTasksfastapi >=0.106
fastapi dev / fastapi run CLI (fastapi[standard])fastapi >=0.111
BaseSettings moved to the separate pydantic-settings packagepydantic >=2.0
asyncio.TaskGroup and asyncio.timeout for fan-out and deadlinespython >=3.11

Error Signatures

SignatureCauseGo to
422 with "loc": ["body", ...]Body does not match the model: alias, nesting depth, or a nullable field that is required in pydantic v2pydantic.md
307 Temporary Redirect on POSTTrailing-slash mismatch; method and body survive the redirect, but a proxy with wrong scheme headers sends the client to http://routing.md
500 in the browser with no CORS headersException escaped above CORSMiddleware (rule 9)middleware.md
QueuePool limit of size 5 overflow 10 reachedSessions never closed, or the pool sized without counting workers (rule 5)database.md
MissingGreenlet / greenlet_spawn has not been calledLazy relationship access outside an await in async SQLAlchemydatabase.md
asyncpg ... another operation is in progressOne connection or session shared by concurrent tasksdatabase.md
RuntimeError: Event loop is closed in testsSession-scoped async fixture bound to a function-scoped looptesting.md
FastAPIError: Invalid args for response fieldA non-pydantic type used as response_modelpydantic.md
Endpoint hangs waiting for the bodyMiddleware consumed the request stream and passed the exhausted receive downmiddleware.md
SSE or streamed response arrives only at the endProxy buffering the responsestreaming.md
WebSocket closes with 1006, never 1000Abnormal close: proxy idle timeout or worker restart, not application logicwebsockets.md
Everything returns 200 but clients time outWorker saturated: blocked loop or full threadpool (rules 1-2)async.md

Output Gates

Before delivering FastAPI code, verify:

  • No blocking call inside an async def path; blocking work is in a def endpoint, run_in_threadpool, or a queue
  • Every outbound HTTP, database, and lock call has an explicit timeout
  • Request and response models declared; ORM objects only cross the boundary through a model with from_attributes
  • workers × (pool_size + max_overflow) fits inside the database connection limit
  • Configuration and secrets read through the settings object, never os.getenv scattered through handlers
  • Auth applied at router level with health, readiness, and metrics endpoints excluded
  • Every read and write filtered by owner or tenant inside the query, not in a separate if
  • Errors raised as HTTPException with a consistent body; no stack trace, SQL, or upstream payload in detail
  • Request body size and upload size bounded somewhere (proxy or middleware), not left unlimited
  • Tests override dependencies instead of patching, and run inside the lifespan

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/fastapi/config.yaml. Never interview the user — record a preference the moment it is stated.

VariableTypeDefaultEffect
pydantic_version1 | 22Which API names appear everywhere: model_dump vs .dict(), field_validator vs validator, settings package (pydantic.md)
db_stacksqlalchemy-async | sqlalchemy-sync | sqlmodel | tortoise | nonesqlalchemy-asyncSession and engine pattern, driver choice, and whether endpoints are emitted def or async def (database.md)
server_stackuvicorn | gunicorn-uvicorn | hypercornuvicornProcess model, worker flags, and shutdown settings (deployment.md)
auth_schemejwt | session-cookie | oauth2-provider | api-key | nonejwtWhich current-user dependency and token lifecycle auth.md emits
min_fastapitext (version, e.g. 0.100)0.111Gates the Version Floors features; below the floor the fallback form is emitted instead
deploy_targetcontainer | vm | serverless | unknowncontainerWorker model, health-check style, and shutdown guidance (deployment.md, observability.md)
error_envelopebare | envelopedbareShape of every error body and the handlers generated in errors.md
task_backendbackground-tasks | celery | arq | rq | dramatiqbackground-tasksWhere after-response work goes and how failures are retried (background.md)
test_clientasync-httpx | testclientasync-httpxShape of emitted tests, fixtures, and the transport used (testing.md)
json_rendererdefault | orjsondefaultDefault response class and serialization advice (performance.md)
render_modejson | html-htmx | bothjsonWhether routes return JSON models or rendered templates, and which validation-error path is generated (templates.md)

Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied:

  • Tooling: package and env manager (pip, uv, poetry), linter and formatter, migration tool, load generator — affects every command block shown alongside code
  • Conventions: router and module naming, schema suffixes (Create/Read/Update), URL casing, versioning scheme, docstring style — affects generated routers and models (structure.md)
  • Platform: cloud, reverse proxy, Python version, CPU count, managed database — affects worker maths, proxy headers, and pool sizing (deployment.md)
  • Safety posture: how loudly to flag missing auth, unbounded uploads, absent timeouts, verify=False, secrets in logs — affects review comments, never the correctness rules
  • Integrations: cache, broker, and object store of choice (Redis, RabbitMQ, S3-compatible), auth provider, error tracker — affects which client patterns appear
  • Output format: whole file vs minimal diff, how much explanation, whether tests and migrations accompany every change
  • Work order: contract-first (OpenAPI or schema before handlers) vs code-first, migration before or after model change, review gates before edits — affects sequence, never correctness
  • Dependencies: banned or mandated libraries (httpx vs requests, SQLModel vs SQLAlchemy, ORM vs raw SQL), tolerance for new transitive dependencies

Traps

TrapWhy it failsDo instead
requests.get() inside async defBlocks the loop for the full round trip; under load the whole worker stalls (rule 1)httpx.AsyncClient from lifespan, or make the endpoint def (async.md)
@app.on_event("startup")Deprecated in favour of lifespan, and gives no way to hold a resource open across the app's lifelifespan async context manager (structure.md)
Module-level cache = {} as an application cacheOne copy per worker: hit rate divided by the worker count, invalidation reaching only one process (rule 6)Redis, or accept per-process caching explicitly (performance.md)
app.include_router(..., dependencies=[Depends(auth)]) on everythingHealth and metrics start returning 401, the orchestrator declares the pod dead and restarts it in a loopAuth on business routers only; probes on an unauthenticated router (observability.md)
items: list = [] "shared across requests"Pydantic deep-copies mutable defaults per instance — this is not the bug; the real need for default_factory is a computed default (uuid4, datetime.now)Field(default_factory=...) only when the value must be computed (pydantic.md)
Optional[str] and expecting it to be optionalIn pydantic v2 that is required-and-nullable; the client must send nullGive it a default: = None (pydantic.md)
Reading await request.body() in middleware, then calling the endpointThe receive channel is exhausted; the endpoint waits for a body that will never arrivePure ASGI middleware that replays the stream, or read the body only in the endpoint (middleware.md)
BackgroundTasks for anything that mattersRuns in the worker after the response; a deploy or crash loses it silently, and the client already got a 200Queue with retries and visibility (background.md)
uvicorn --reload --workers 4--reload forces a single process; the worker count is silently ignored, so dev and prod differ in concurrencyReload in dev, workers in prod, never both (deployment.md)
TestClient for testing async behaviorDrives the app through a sync portal — real concurrency bugs, cancellation, and loop blocking never show uphttpx.AsyncClient with ASGITransport (testing.md)
Returning the ORM object and hopingLazy attributes load during serialization, outside the session, after it closedLoad eagerly, convert with a response model (database.md)
Secrets in HTTPException(detail=...)Whatever is in detail is JSON in the client's hands, including upstream error payloadsGeneric message out, full context to the log with a correlation id (errors.md)

Where Experts Disagree

  • Async everywhere vs honest def endpoints. One school makes every endpoint async def for uniformity; the other declares def whenever the body blocks. Boundary: an all-await stack (async driver, httpx, async cache) → async def; a single blocking library in the chain → def is strictly safer, because the threadpool bounds the damage to 40 slots instead of stopping the loop.
  • ORM vs raw SQL vs SQLModel. SQLAlchemy gives migrations, identity map, and eager-loading control; raw SQL gives exact queries and no lazy-load surprises; SQLModel merges model and schema at the cost of coupling the table to the API contract. Boundary: one shape of data serving one API → SQLModel is fine; separate read and write shapes, or a schema older than the API → SQLAlchemy plus explicit response models.
  • Service layer or routers that talk to the database. A repository/service layer pays off when the same operation is called from HTTP, a worker, and a CLI; below that it is indirection. Boundary: the second caller of the same logic, not the first.
  • Bare bodies vs an error envelope. HTTP status plus a bare body is idiomatic and what generated clients expect; an envelope ({"data": ..., "error": ...}) survives clients that only read 200s. Boundary: public API or generated SDKs → bare with RFC 9457 problem details; a single front end with a legacy error contract → envelope, declared once in error_envelope and applied everywhere.

More Clawic skills, get them at https://clawic.com/skills/fastapi (install if the user confirms):

  • py — the Python language itself: packaging, typing, async primitives, tests
  • pg — PostgreSQL behind the API: query plans, indexes, connection limits
  • auth — authentication design across sessions, JWT, OAuth, MFA
  • api-design — resource modelling, versioning, and the cost of breaking changes
  • docker — containerizing and running the service

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/fastapi.

相关技能

按 Django 异常类型定位问题,落地生产级模型、视图、Admin、DRF 与迁移。

72 次安装2 星标

按文档化的 Python 规则手册和固定检查清单排查、审阅与编写代码。

156 次安装6 星标

Use Python for practical project setup, dependency install, script execution, and environment troubleshooting with safe defaults. Use when tasks involve pypr...

68 次安装

为 Node 与 TypeScript 项目设计 Prisma schema、编写类型安全查询,并解决迁移、连接池与 N+1 关联加载问题。

101 次安装2 星标

针对 Vue 3 响应式、组件、路由和性能问题,依据控制台报错与代码形态给出具体修复方案。

121 次安装8 星标

排查并修复 Svelte 与 SvelteKit 的响应式、SSR 与构建问题,并把 Svelte 4 代码迁移到 runes。

62 次安装3 星标