You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue originally tracked "provide PgBouncer-safe defaults for the async metadata engine (asyncpg).". However, discussion on #67800 reached a cleaner conclusion: rather than hardening asyncpg, switch the default async Postgres driver to psycopg3 (postgresql+psycopg), which is PgBouncer-safe out of the box. This issue is re-scoped accordingly.
Why psycopg3 as the default
asyncpg was selected as the async driver only because Airflow was pinned to SQLAlchemy 1.4. That constraint is gone (airflow-core now requires sqlalchemy[asyncio]>=2.0.48), and psycopg3 serves both sync and async from a single driver.
Airflow recommends PgBouncer for all production Postgres (see airflow-core/docs/howto/set-up-database.rst). asyncpg uses named server-side prepared statements that break under transaction-mode PgBouncer; the only safe asyncpg configuration disables prepared-statement caching, which removes most of asyncpg's advantage and still requires per-deployment tuning. psycopg3 is safe with zero configuration.
A safe default beats a documentation note that operators miss.
Performance context
Driver benchmark (https://github.com/Dev-iL/pgbench, localhost, concurrency 10): asyncpg keeps an edge on row-returning queries even with prepared statements disabled (e.g. pg_type 1466 vs 764 qps), but on the trivial round-trip closest to single-row OLTP the gap is ~12% (9945 vs 8836 qps), and psycopg3-async has markedly lower latency variance. Two caveats on reading those numbers:
It is a tight localhost loop with no network hop, which isolates and maximizes raw driver overhead; behind a real network / PgBouncer hop the per-query delta becomes a small fraction of total latency.
The harness runs async drivers as coroutines on one event loop and sync drivers via a thread pool, so sync-vs-async rows are not directly comparable. asyncpg vs psycopg3-async (both async) is the clean comparison, and the one that matters for the default.
For the high-QPS single-row OLTP routes being migrated (heartbeat, #67800), psycopg3 is a sound default.
Bulk caveat: in the same localhost benchmark psycopg3-async was ~30x slower on batch INSERT and ~3x on COPY. Today's async routes are single-row OLTP, so this does not affect the switch — but bulk-write paths must be validated (and may warrant per-route asyncpg) before they migrate to async (#67799).
Work
Change the default async driver mapping in airflow-core/src/airflow/settings.py (AIO_LIBS_MAPPING["postgresql"] from asyncpg to psycopg). This affects only the postgresql+... async URL derivation; the aiosqlite / aiomysql mappings are unchanged.
Packaging: psycopg3 (psycopg[binary]) currently ships only as an opt-in extra in providers/postgres, while asyncpg is a hard dependency. Make psycopg3 available by default and downgrade asyncpg to the opt-in extra. (Decide hard-dep vs. extra.)
Keep asyncpg fully supported and documented as a throughput opt-in: sql_alchemy_conn_async = postgresql+asyncpg://..., with the PgBouncer-safe connect_args recipe added in Use async DB session for Execution API task-instance heartbeat #67800 — {"statement_cache_size": 0, "prepared_statement_cache_size": 0}, both in connect_args (verified against PG14: the default leaves named __asyncpg_stmt__ statements on the backend connection, the recipe leaves none).
Important
This issue originally tracked "provide PgBouncer-safe defaults for the async metadata engine (asyncpg).". However, discussion on #67800 reached a cleaner conclusion: rather than hardening asyncpg, switch the default async Postgres driver to psycopg3 (
postgresql+psycopg), which is PgBouncer-safe out of the box. This issue is re-scoped accordingly.Why psycopg3 as the default
airflow-corenow requiressqlalchemy[asyncio]>=2.0.48), and psycopg3 serves both sync and async from a single driver.airflow-core/docs/howto/set-up-database.rst). asyncpg uses named server-side prepared statements that break under transaction-mode PgBouncer; the only safe asyncpg configuration disables prepared-statement caching, which removes most of asyncpg's advantage and still requires per-deployment tuning. psycopg3 is safe with zero configuration.Performance context
Driver benchmark (https://github.com/Dev-iL/pgbench, localhost, concurrency 10): asyncpg keeps an edge on row-returning queries even with prepared statements disabled (e.g.
pg_type1466 vs 764 qps), but on the trivial round-trip closest to single-row OLTP the gap is ~12% (9945 vs 8836 qps), and psycopg3-async has markedly lower latency variance. Two caveats on reading those numbers:For the high-QPS single-row OLTP routes being migrated (heartbeat, #67800), psycopg3 is a sound default.
Bulk caveat: in the same localhost benchmark psycopg3-async was ~30x slower on batch
INSERTand ~3x onCOPY. Today's async routes are single-row OLTP, so this does not affect the switch — but bulk-write paths must be validated (and may warrant per-route asyncpg) before they migrate to async (#67799).Work
airflow-core/src/airflow/settings.py(AIO_LIBS_MAPPING["postgresql"]fromasyncpgtopsycopg). This affects only thepostgresql+...async URL derivation; theaiosqlite/aiomysqlmappings are unchanged.psycopg[binary]) currently ships only as an opt-in extra inproviders/postgres, whileasyncpgis a hard dependency. Make psycopg3 available by default and downgrade asyncpg to the opt-in extra. (Decide hard-dep vs. extra.)sql_alchemy_conn_async = postgresql+asyncpg://..., with the PgBouncer-safe connect_args recipe added in Use async DB session for Execution API task-instance heartbeat #67800 —{"statement_cache_size": 0, "prepared_statement_cache_size": 0}, both inconnect_args(verified against PG14: the default leaves named__asyncpg_stmt__statements on the backend connection, the recipe leaves none).Acceptance criteria
sql_alchemy_conn_async, documented with the PgBouncer-safe recipe.Are you willing to submit PR?
Code of Conduct