First stable release of the 0.8 line, and the first stable since 0.7.1.post3.
⚠️ Breaking
- Python 3.12+ is now required (was 3.9+).
- SQLAlchemy 2.0+ is now required (was 1.4.19+), and the dependency is
SQLAlchemy[asyncio]. PlainSQLAlchemy>=2.0only pullsgreenletfor a fixed list ofplatform_machinevalues that excludesarm64, so on Apple Silicon the first query failed withValueError: the greenlet library is required. Theasyncioextra requires it unconditionally.
Pool resilience
A saturated connection pool doesn't fail — it queues, for the whole engine-wide pool_timeout. When a readiness probe takes its connection from the same pool as business traffic, the probe queues too, the pod leaves Service endpoints, and callers get 503s while the application logs almost nothing. This release adds the API to fail fast on a saturated pool, to see saturation coming, and to bound load before it gets there.
db(pool_timeout=...)/db.connection(timeout=...)— a per-context checkout deadline layered on the engine-widepool_timeout, which SQLAlchemy cannot override per call. A probe fails in 1s instead of parking for 60.PoolTimeoutError— subclasses the builtinTimeoutError, so existing handlers keep working, and carriesretry_afterplus a pool snapshot. Exhaustion can be answered with a controlled503 + Retry-Afterinstead of an opaque500.db.pool_status()— livesize/capacity/checked_out/available/saturationfor metrics.NullPool/StaticPoolreportNonerather than crashing.pool_warn_threshold— a throttledWARNINGonce the pool crosses a share of its capacity, well before anything has timed out.exclude_paths— listed paths get no request session at all, so a probe cannot quietly borrow from the business pool.max_concurrent_requests/request_queue_timeout— bound in-flight requests per middleware instance, returning503 + Retry-Afterbefore the route runs.
Request session lifetime
- The request session is finalized once the response body is ready, so a slow background task no longer pins its connection. A failing commit still prevents a successful response from being reported.
yielddependencies that own their transaction (db.session.begin()/begin_nested()) are exempt from that early finalization. FastAPI runs their teardown after the response, so finalizing there rolled their work back right before their own commit — a 200 with no row written.- Streaming bodies: the request session is finalized when the body starts flowing, and touching it afterwards raises an error pointing at
async with db(). This behaves the same behind a@app.middleware("http"), which re-emits every response as a chunked one. http.response.pathsendis now handled as the end of the response body, soFileResponse's background task no longer deletes the served file before the server reads it.
Fixes
- A plain
db()nested insidedb(multi_sessions=True)kept resolvingdb.sessionto the enclosing session: the nested block committed an empty session while its writes rode on — and were rolled back with — the outer one. db.sessionsilently ignored a contextpool_timeoutin multi-session mode, parking on the engine-wide deadline instead. It now refuses, pointing atdb.connection()/db.gather().session_args={"expire_on_commit": ...}or{"class_": ...}raisedTypeError: got multiple values for keyword argument.db.gather()leaked a never-awaited coroutine when entering the managed context failed, and ignored a contextpool_timeoutwhenmax_concurrentwas unset.- Multi-session cleanup keys off
Taskobjects rather thanid(task), and creates its cleanup task directly instead of viacall_soon. - Exceptions are exported from the package root, as the docs already claimed.
DBSessionMetais exposed as a typing Protocol, sodb.session/db.connection()/db.gather()type-check and autocomplete (#18).
Docs
A full documentation site: https://h0rn3t.github.io/fastapi-async-sqlalchemy/ — including new guides on health checks and pool saturation, HTTP load and background tasks, concurrency, and streaming responses.
Full Changelog: 0.7.1.post3...0.8.0