Description
Incrementally migrate Airflow's FastAPI endpoints to async database I/O. Each conversion should let database waits yield the event loop while preserving the endpoint's behavior. This is a tracking issue; individual endpoints or small cohesive groups should land in separate PRs.
The async engine, session factory, AsyncSessionDep, create_session_async, and async pagination helpers already exist. The work is to adopt them throughout each request path, including dependencies and domain helpers. Changing a route to async def while it still calls blocking helpers directly would make matters worse.
Motivation and limits
Synchronous FastAPI routes occupy a threadpool worker during blocking database calls. Native async database calls allow other requests to progress while those calls wait. That is the intended scheduling improvement; a production throughput increase must be measured separately.
#36504 explored async database access in the Airflow 2 architecture. Its compatibility and driver-testing concerns remain useful. In Airflow 3, worker and trigger execution code should use the Task SDK/Execution API boundary, rather than gaining direct metadata database access. SDK async connection lookup and async Execution API secrets transport already exist; server-side secret resolution still needs work.
The earlier experiments are exploratory evidence, not a production performance guarantee. High-concurrency results were inconclusive. Keep benchmark implementation and reports separate from endpoint migration PRs, and do not infer a CPU, GIL, or pool bottleneck from throughput and active-connection counts alone.
Definition of done per endpoint
For the contributors' convenience, the migration steps are encoded as an agent SKILL in #73405
- Trace the complete request path: route, auth/team dependencies, domain helpers, external I/O, and serialization. Use
AsyncSessionDep and await database operations. Handle remaining blocking I/O explicitly; document any thread-offloaded compatibility bridge.
- Preserve the HTTP contract, permissions, team scope, query/count/order semantics, locking, rowcount branches, and transaction behavior. Let the session owner commit/roll back; do not add commits to route handlers. A pure implementation conversion does not require an Execution API version bump.
- Preserve existing contract assertions and run supported API-version tests. Update coroutine mocks and fixture setup as needed. Add focused regressions for risks introduced by the conversion, including relevant failure paths; do not require test source to remain byte-identical.
- Match async-engine lifetime to test event-loop lifetime. Reuse an appropriate shared fixture where available. The heartbeat's engine-reconfiguration fixture is a local workaround, not a rule to replicate unconditionally. Broader test-loop changes belong in separate work, as raised in the review discussion.
- Exercise database-dependent behavior on SQLite, PostgreSQL, and MySQL using current drivers. Record exact backend/driver and results. SQLite does not verify row locks or all production-driver semantics. Where SQL, driver, or connection configuration changes, also validate affected overrides and PgBouncer configurations.
- Pass applicable formatting, lint, typing, and static checks. Keep unrelated endpoints, synthetic routers, and benchmark infrastructure out of the conversion.
Cross-cutting work
- Server-side async secrets resolution: support native async metastore reads, compatible offloading for existing synchronous custom/provider backends, then migrate the Variable and Connection value-read endpoints. Preserve configured backend order, legacy overrides, team propagation, cache hits/misses, and authoritative access denials. Native async cloud clients can follow independently; every provider need not migrate before core support lands. Coordinate with #72329, which proposes SDK async Variable methods.
- Test engine lifecycle: evaluate a reusable engine/loop strategy as migration grows. Do not mask lifecycle failures or change global loop scope merely to make one route pass.
- Deployment configuration: verify actual sync and async pool settings and their multiplication across workers/replicas. Budget configured overflow and other DB clients as well as steady pool sizes. Avoid a universal connection-count prescription based on the POC.
- Performance follow-up: if benchmarking adoption, use a controlled workload, record client/server resources and driver/pool settings, and profile unexplained results. Performance experiments are not prerequisites to parity-focused conversions.
Execution API migration inventory
Paths below are relative to /execution. Checked means the conversion is merged, not merely present in a local branch. Snapshot checked against 45bad9f54d plus the local variable-keys WIP; recheck current code and open PRs before claiming a route. These are candidates, not promises that every handler is a mechanical session swap. Core API endpoints need their own call-path inventory when selected.
The inventory accounts for all 57 production route declarations in this checkout: 55 are listed below, including the older-version Dag-run route. The two health routes are excluded from migration: GET /health returns a static response without database access, and GET /health/ping already awaits service pings. The six experimental benchmark routes are excluded as development-only code.
Task instances — routes/task_instances.py
Dag runs — routes/dag_runs.py
XComs — routes/xcoms.py
Asset state store — routes/asset_state_store.py
Assets and asset events
Task state store — routes/task_state_store.py
Human-in-the-loop, Dags, reschedules, and variables
Connection tests and callbacks
Value resolution and Variable writes
These use model/domain helpers rather than a route-level session swap:
References
Are you willing to submit a PR?
Code of Conduct
Description
Incrementally migrate Airflow's FastAPI endpoints to async database I/O. Each conversion should let database waits yield the event loop while preserving the endpoint's behavior. This is a tracking issue; individual endpoints or small cohesive groups should land in separate PRs.
The async engine, session factory,
AsyncSessionDep,create_session_async, and async pagination helpers already exist. The work is to adopt them throughout each request path, including dependencies and domain helpers. Changing a route toasync defwhile it still calls blocking helpers directly would make matters worse.Motivation and limits
Synchronous FastAPI routes occupy a threadpool worker during blocking database calls. Native async database calls allow other requests to progress while those calls wait. That is the intended scheduling improvement; a production throughput increase must be measured separately.
#36504 explored async database access in the Airflow 2 architecture. Its compatibility and driver-testing concerns remain useful. In Airflow 3, worker and trigger execution code should use the Task SDK/Execution API boundary, rather than gaining direct metadata database access. SDK async connection lookup and async Execution API secrets transport already exist; server-side secret resolution still needs work.
The earlier experiments are exploratory evidence, not a production performance guarantee. High-concurrency results were inconclusive. Keep benchmark implementation and reports separate from endpoint migration PRs, and do not infer a CPU, GIL, or pool bottleneck from throughput and active-connection counts alone.
Definition of done per endpoint
For the contributors' convenience, the migration steps are encoded as an agent SKILL in #73405
AsyncSessionDepand await database operations. Handle remaining blocking I/O explicitly; document any thread-offloaded compatibility bridge.Cross-cutting work
Execution API migration inventory
Paths below are relative to
/execution. Checked means the conversion is merged, not merely present in a local branch. Snapshot checked against45bad9f54dplus the local variable-keys WIP; recheck current code and open PRs before claiming a route. These are candidates, not promises that every handler is a mechanical session swap. Core API endpoints need their own call-path inventory when selected.The inventory accounts for all 57 production route declarations in this checkout: 55 are listed below, including the older-version Dag-run route. The two health routes are excluded from migration:
GET /healthreturns a static response without database access, andGET /health/pingalready awaits service pings. The six experimental benchmark routes are excluded as development-only code.Task instances —
routes/task_instances.pyPATCH /task-instances/{task_instance_id}/run—ti_runPATCH /task-instances/{task_instance_id}/state—ti_update_statePATCH /task-instances/{task_instance_id}/skip-downstream—ti_skip_downstreamPUT /task-instances/{task_instance_id}/heartbeat—ti_heartbeat(#67800)PUT /task-instances/{task_instance_id}/rtif—ti_put_rtifPATCH /task-instances/{task_instance_id}/rendered-map-index—ti_patch_rendered_map_indexGET /task-instances/{task_instance_id}/previous-successful-dagrun—get_previous_successful_dagrun(#73403)GET /task-instances/count—get_task_instance_count(#73966)GET /task-instances/previous/{dag_id}/{task_id}—get_previous_task_instance(#73403)GET /task-instances/states—get_task_instance_states(#73966)GET /task-instances/breadcrumbs—get_task_instance_breadcrumbs(#73403)GET /task-instances/{task_instance_id}/validate-inlets-and-outlets—validate_inlets_and_outletsDag runs —
routes/dag_runs.pyGET /dag-runs/{dag_id}/previous—get_previous_dagrun_compat(older API versions)GET /dag-runs/{dag_id}/{run_id}—get_dag_runPOST /dag-runs/{dag_id}/{run_id}—trigger_dag_runPOST /dag-runs/{dag_id}/{run_id}/clear—clear_dag_runGET /dag-runs/{dag_id}/{run_id}/state—get_dagrun_state(#73403)GET /dag-runs/count—get_dr_count(#73403)GET /dag-runs/previous—get_previous_dagrunXComs —
routes/xcoms.pyGET /xcoms/{dag_id}/{run_id}/{task_id}/{key:path}/item/{offset}—get_mapped_xcom_by_indexGET /xcoms/{dag_id}/{run_id}/{task_id}/{key:path}/slice—get_mapped_xcom_by_sliceHEAD /xcoms/{dag_id}/{run_id}/{task_id}/{key:path}—head_xcomGET /xcoms/{dag_id}/{run_id}/{task_id}/{key:path}—get_xcomPOST /xcoms/{dag_id}/{run_id}/{task_id}/{key:path}—set_xcomDELETE /xcoms/{dag_id}/{run_id}/{task_id}/{key:path}—delete_xcomAsset state store —
routes/asset_state_store.pyGET /store/asset/by-name/value—get_asset_state_store_by_namePUT /store/asset/by-name/value—set_asset_state_store_by_nameDELETE /store/asset/by-name/value—delete_asset_state_store_by_nameDELETE /store/asset/by-name/clear—clear_asset_state_store_by_nameGET /store/asset/by-uri/value—get_asset_state_store_by_uriPUT /store/asset/by-uri/value—set_asset_state_store_by_uriDELETE /store/asset/by-uri/value—delete_asset_state_store_by_uriDELETE /store/asset/by-uri/clear—clear_asset_state_store_by_uriAssets and asset events
GET /asset-events/by-asset—get_asset_event_by_asset_name_uriGET /asset-events/by-asset-alias—get_asset_event_by_asset_aliasGET /assets/by-name—get_asset_by_name(#73403)GET /assets/by-uri—get_asset_by_uri(#73403)GET /assets/by-alias—get_assets_by_aliasTask state store —
routes/task_state_store.pyGET /store/ti/{task_instance_id}/{key:path}—get_task_state_storePUT /store/ti/{task_instance_id}/{key:path}—set_task_state_storeDELETE /store/ti/{task_instance_id}/{key:path}—delete_task_state_storeDELETE /store/ti/{task_instance_id}—clear_task_state_storeHuman-in-the-loop, Dags, reschedules, and variables
POST /hitlDetails/{task_instance_id}—upsert_hitl_detailPATCH /hitlDetails/{task_instance_id}—update_hitl_detailGET /hitlDetails/{task_instance_id}—get_hitl_detail(#73403)GET /dags/{dag_id}—get_dagGET /task-reschedules/{task_instance_id}/start_date—get_start_date(#73403)GET /variables/keys—get_variable_keys(WIP, not merged) (#73407)Connection tests and callbacks
GET /connection-tests/{connection_test_id}/connection—get_connection_test_connection(locks and changes state despite using GET)PATCH /connection-tests/{connection_test_id}—patch_connection_testPATCH /callbacks/{callback_id}/run—run_callback(single-use token exchange)Value resolution and Variable writes
These use model/domain helpers rather than a route-level session swap:
GET /variables/{variable_key:path}— async secrets resolution, thenget_variableGET /connections/{connection_id}— async secrets resolution, thenget_connectionPUT /variables/{variable_key:path}—put_variable: secrets conflict checks, database writes, and cache invalidation require a separate transaction-aware conversionDELETE /variables/{variable_key:path}—delete_variable: database deletion and cache invalidation; does not require external async secrets lookupReferences
Are you willing to submit a PR?
Code of Conduct