This document describes a verified FastAPI concurrency issue in the API stack and recommends a two-phase remediation plan for maintainers.
The API uses synchronous SQLAlchemy sessions backed by psycopg. When those sessions are consumed from async def route handlers, blocking database work runs on the event loop thread if the handlers call synchronous ORM helpers directly. The lowest-risk immediate fix is to convert database-bound route handlers that do not perform asynchronous work into plain def. The longer-term fix is to introduce a real async SQLAlchemy stack and migrate the affected handlers and helpers incrementally.
FastAPI supports synchronous generator dependencies such as get_db_session(). The issue is not the dependency shape itself. The issue is that the injected object is a synchronous SQLAlchemy Session, and any async def route that consumes it while executing synchronous ORM queries directly will block the event loop thread.
In this configuration, FastAPI runs the async def route body on the event loop thread. If that body performs blocking database I/O through the synchronous session, the worker cannot make progress on other requests assigned to that event loop until the database call returns. A slow well query can therefore delay unrelated lightweight requests handled by the same worker.
This is a concurrency problem, not a correctness problem. The endpoints can still return correct data while reducing throughput and responsiveness under load.
db/engine.pycreatesdatabase_sessionmaker = sessionmaker(engine, expire_on_commit=False)andget_db_session()yields a regular synchronousSession.db/engine.pybuilds synchronouspostgresql+psycopgengines for both the default PostgreSQL path and the Cloud SQL path, confirming that the active database layer is synchronous.core/dependencies.pyinjects that session throughsession_dependency.services/well_details_helper.pyperforms synchronous ORM operations such assession.scalars(...).all()and related query chains.api/thing.pycontains representative database-backed routes that pass the synchronous session into helper functions such asget_db_things(...)andget_well_details_payload(...).api/asset.pyshows a contrasting safe pattern for non-database blocking work by wrapping synchronous GCS calls inrun_in_threadpool(...).- The short-term fix described in this ADR converts database-bound routes from
async deftodefwhere they do not needawait, but the helper/query layer remains synchronous until a real async session stack is introduced.
The short-term fix is to convert database-bound route handlers from async def to def when they do not actually perform asynchronous work.
This lets FastAPI offload the entire route function to a worker thread instead of running its synchronous database calls on the event loop thread. It does not require changing the current database engine, dependency, query helpers, or response schemas.
- Convert any route handler that:
- receives
session: session_dependency, - performs synchronous ORM work directly or through helpers, and
- does not require
awaitfor other operations in the route body.
- receives
- Prioritize the highest-value endpoints first:
- high-traffic list and detail endpoints,
- endpoints known to run expensive joins or eager-loads,
- endpoints that affect warmup or perceived application responsiveness.
- Keep route behavior unchanged:
- do not change paths, status codes, payloads, or auth dependencies as part of this phase.
- Avoid mixed patterns:
- do not leave a route as
async defif it still calls synchronous SQLAlchemy code directly.
- do not leave a route as
- Use
run_in_threadpool(...)only when a route must remainasync deffor a separate reason, such as mixing in another async operation, and only for isolated blocking helpers rather than as a blanket wrapper for all DB access.
- Lower risk than a full async migration.
- No intended HTTP contract changes.
- Better worker responsiveness because blocking DB work moves off the event loop thread.
The long-term fix is to add a real async database stack and migrate selected API areas to it incrementally.
This phase should introduce an explicit async path rather than trying to reuse the current synchronous dependency. Importing async SQLAlchemy primitives is not enough; the repo needs a working async engine, async sessionmaker, async dependency, and async query/helper layer for migrated endpoints.
- Add an
AsyncEngineconfigured for the intended async driver. - Add an
async_sessionmakerthat yieldsAsyncSessioninstances. - Add a dedicated async dependency such as
get_async_db_session()rather than overloadingget_db_session(). - Update migrated handlers and helper functions to use async database access:
await session.execute(...)await session.scalars(...)- other
AsyncSession-compatible patterns as needed
- Migrate by subsystem, not all at once.
- Start with a bounded route/helper cluster where the query patterns are understood.
- Keep sync and async paths separate during migration to avoid ambiguous dependencies and accidental sync calls from async routes.
- Treat helper-layer migration as part of the work. Converting route signatures alone is insufficient if the helper functions still expect synchronous sessions.
- Do not claim the repo already has a working async DB session path unless one is actually implemented and used.
- Do not treat “switch everything to async” as a trivial refactor.
- Do not mix
AsyncSessionroute code with synchronous helper/query internals.
The recommended order is:
- Convert database-bound
async defroutes that do not useawaitinto plaindef. - Validate behavior and measure the effect on responsiveness.
- Introduce a dedicated async DB stack.
- Migrate selected route/helper subsystems incrementally to
AsyncSession.
This sequence delivers immediate concurrency improvement with limited risk, while preserving a clear path to a full async architecture later.
- Targeted API tests continue to pass after
async deftodefconversions. - HTTP behavior is unchanged:
- same routes,
- same auth requirements,
- same status codes,
- same payload shapes.
- Concurrency smoke checks or request-timing instrumentation show that DB-heavy requests no longer block the event loop thread for that worker in the same way they do today.
- Migrated endpoints pass the existing API test coverage for their subsystem.
- The async session lifecycle is correct for successful and failing requests.
- Migrated
async defroutes do not call synchronous session helpers. - Before/after measurements are captured for latency and concurrency so the migration can be evaluated against real behavior rather than assumptions.
- This document is written for maintainers and assumes familiarity with FastAPI and SQLAlchemy internals.
- The document is self-contained and does not require code changes to be useful.
- The recommended short-term action is intentionally conservative and does not prescribe a file-by-file rollout sequence.
- The recommended long-term action is a staged migration, not a flag-day rewrite.