Skip to content

Commit 99fa906

Browse files
committed
Switch the default async Postgres driver from asyncpg to psycopg3
The derived async metadata-DB URL now uses postgresql+psycopg_async://, which is safe behind transaction-mode PgBouncer with no configuration. asyncpg moves to an opt-in extra of the postgres provider and remains fully supported via an explicit sql_alchemy_conn_async URL.
1 parent 5bc8796 commit 99fa906

12 files changed

Lines changed: 366 additions & 262 deletions

File tree

‎airflow-core/docs/howto/set-up-database.rst‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,42 @@ We recommend using the ``psycopg2`` driver and specifying it in your SqlAlchemy
206206
207207
postgresql+psycopg2://<user>:<password>@<host>/<db>
208208
209+
Async SQLAlchemy engine and the async driver
210+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
211+
212+
In addition to the synchronous engine, Airflow maintains an async SQLAlchemy engine for the metadata database (used e.g. by async API endpoints).
213+
When ``[database] sql_alchemy_conn_async`` is not set, its URL is derived from ``sql_alchemy_conn`` using ``psycopg`` (psycopg3) as the async driver:
214+
215+
.. code-block:: text
216+
217+
postgresql+psycopg_async://<user>:<password>@<host>/<db>
218+
219+
psycopg3 is the default because it is safe behind transaction-mode PgBouncer (recommended for all production Postgres installations, see the note below) with no extra configuration.
220+
psycopg3 `defers statement preparation <https://www.psycopg.org/psycopg3/docs/advanced/prepare.html>`_ behind a threshold (default 5 executions) and lets you disable it entirely with ``prepare_threshold=None``.
221+
222+
If you need the extra throughput of ``asyncpg``, opt in by installing the ``apache-airflow-providers-postgres[asyncpg]`` extra and setting the async URL explicitly:
223+
224+
.. code-block:: ini
225+
226+
[database]
227+
sql_alchemy_conn_async = postgresql+asyncpg://<user>:<password>@<host>/<db>
228+
229+
asyncpg uses `named server-side prepared statements <https://magicstack.github.io/asyncpg/current/faq.html#why-am-i-getting-prepared-statement-errors>`_, which break under transaction-mode PgBouncer.
230+
If you run asyncpg behind transaction-mode PgBouncer, you must disable its prepared-statement caching by pointing ``sql_alchemy_connect_args_async`` at a dictionary defined in your ``airflow_local_settings.py``:
231+
232+
.. code-block:: python
233+
234+
# airflow_local_settings.py
235+
connect_args_async = {
236+
"statement_cache_size": 0,
237+
"prepared_statement_cache_size": 0,
238+
}
239+
240+
.. code-block:: ini
241+
242+
[database]
243+
sql_alchemy_connect_args_async = airflow_local_settings.connect_args_async
244+
209245
Also note that since SqlAlchemy does not expose a way to target a specific schema in the database URI, you need to ensure schema ``public`` is in your Postgres user's search_path.
210246

211247
If you created a new Postgres account for Airflow:
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
Default async Postgres driver changed from asyncpg to psycopg3
2+
3+
When ``[database] sql_alchemy_conn_async`` is not set, Airflow derives the async metadata database URL from ``sql_alchemy_conn``. For PostgreSQL, the derived URL now uses psycopg3 (``postgresql+psycopg_async://``) instead of asyncpg (``postgresql+asyncpg://``).
4+
5+
**Why:** Airflow recommends running PgBouncer in front of PostgreSQL in production. asyncpg uses named server-side prepared statements, which break under transaction-mode PgBouncer unless prepared-statement caching is explicitly disabled. psycopg3 is safe behind transaction-mode PgBouncer with no extra configuration, so the default async engine now works out of the box in recommended production deployments.
6+
7+
The SQLite (``aiosqlite``) and MySQL (``aiomysql``) async URL derivations are unchanged, and an explicitly configured ``sql_alchemy_conn_async`` is never rewritten.
8+
9+
**Packaging:** the ``apache-airflow-providers-postgres`` distribution now installs ``psycopg`` (psycopg3) by default, and ``asyncpg`` is no longer installed by default. Instead, it moved to the ``asyncpg`` optional extra.
10+
11+
**To keep using asyncpg**, install the extra and configure the async URL explicitly:
12+
13+
.. code-block:: bash
14+
15+
pip install 'apache-airflow-providers-postgres[asyncpg]'
16+
17+
.. code-block:: ini
18+
19+
[database]
20+
sql_alchemy_conn_async = postgresql+asyncpg://<user>:<password>@<host>/<db>
21+
22+
When running behind transaction-mode PgBouncer, also disable asyncpg's prepared-statement caching by pointing ``sql_alchemy_connect_args_async`` at a dict defined in ``airflow_local_settings.py``:
23+
24+
.. code-block:: python
25+
26+
# airflow_local_settings.py
27+
connect_args_async = {"statement_cache_size": 0, "prepared_statement_cache_size": 0}
28+
29+
.. code-block:: ini
30+
31+
[database]
32+
sql_alchemy_connect_args_async = airflow_local_settings.connect_args_async

‎airflow-core/src/airflow/settings.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def load_policy_plugins(pm: pluggy.PluginManager):
241241

242242

243243
def _get_async_conn_uri_from_sync(sync_uri):
244-
AIO_LIBS_MAPPING = {"sqlite": "aiosqlite", "postgresql": "asyncpg", "mysql": "aiomysql"}
244+
AIO_LIBS_MAPPING = {"sqlite": "aiosqlite", "postgresql": "psycopg_async", "mysql": "aiomysql"}
245245
"""Mapping of sync scheme to async scheme."""
246246

247247
scheme, rest = sync_uri.split(":", maxsplit=1)

‎airflow-core/tests/unit/core/test_settings.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,36 @@ def test_sqlite_relative_path(value, expectation):
434434
settings.configure_orm()
435435

436436

437+
@pytest.mark.parametrize(
438+
("sync_uri", "expected_async_uri"),
439+
[
440+
("postgresql://user:pass@host/db", "postgresql+psycopg_async://user:pass@host/db"),
441+
("postgresql+psycopg2://user:pass@host/db", "postgresql+psycopg_async://user:pass@host/db"),
442+
("sqlite:////root/airflow.db", "sqlite+aiosqlite:////root/airflow.db"),
443+
("mysql://user:pass@host/db", "mysql+aiomysql://user:pass@host/db"),
444+
("mssql://user:pass@host/db", "mssql://user:pass@host/db"),
445+
],
446+
)
447+
def test_get_async_conn_uri_from_sync(sync_uri, expected_async_uri):
448+
from airflow import settings
449+
450+
assert settings._get_async_conn_uri_from_sync(sync_uri) == expected_async_uri
451+
452+
453+
def test_explicit_sql_alchemy_conn_async_is_not_rewritten():
454+
from airflow import settings
455+
456+
from tests_common.test_utils.config import conf_vars
457+
458+
explicit_async_uri = "postgresql+asyncpg://user:pass@host/db"
459+
try:
460+
with conf_vars({("database", "sql_alchemy_conn_async"): explicit_async_uri}):
461+
settings.configure_vars()
462+
assert explicit_async_uri == settings.SQL_ALCHEMY_CONN_ASYNC
463+
finally:
464+
settings.configure_vars()
465+
466+
437467
class TestDisposeOrm:
438468
"""Tests for dispose_orm() async engine disposal."""
439469

‎generated/provider_dependencies.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,9 +1580,10 @@
15801580
"apache-airflow-providers-common-compat>=1.12.0",
15811581
"apache-airflow-providers-common-sql>=1.32.0",
15821582
"apache-airflow>=2.11.0",
1583-
"asyncpg>=0.30.0",
15841583
"psycopg2-binary>=2.9.10; python_version >= '3.13'",
1585-
"psycopg2-binary>=2.9.9; python_version < '3.13'"
1584+
"psycopg2-binary>=2.9.9; python_version < '3.13'",
1585+
"psycopg[binary]>=3.2.9; python_version < '3.14'",
1586+
"psycopg[binary]>=3.3.3; python_version >= '3.14'"
15861587
],
15871588
"devel-deps": [],
15881589
"plugins": [],
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
93831555f2a141e481c81c147142aeb860c34ea860163ca130d045e5ecd0a83b
1+
7398ed4e1177a69a7e8d09ee3dc07771aa525bf18d3366606281e7c1b0a13793

‎providers/postgres/README.rst‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ PIP package Version required
5858
``apache-airflow-providers-common-sql`` ``>=1.32.0``
5959
``psycopg2-binary`` ``>=2.9.9; python_version < "3.13"``
6060
``psycopg2-binary`` ``>=2.9.10; python_version >= "3.13"``
61-
``asyncpg`` ``>=0.30.0``
61+
``psycopg[binary]`` ``>=3.2.9; python_version < "3.14"``
62+
``psycopg[binary]`` ``>=3.3.3; python_version >= "3.14"``
6263
========================================== ======================================
6364

6465
Cross provider package dependencies

‎providers/postgres/docs/changelog.rst‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@
2727
Changelog
2828
---------
2929

30+
.. note::
31+
Breaking change (upcoming major release): ``asyncpg`` is no longer installed by default —
32+
it moved to the ``asyncpg`` optional extra, and ``psycopg`` (psycopg3) is now installed by
33+
default as the async metadata-database driver. The default derived async connection URL
34+
changed from ``postgresql+asyncpg://`` to ``postgresql+psycopg_async://``, which is safe
35+
behind transaction-mode PgBouncer with no extra configuration. To keep using asyncpg,
36+
install ``apache-airflow-providers-postgres[asyncpg]`` and set
37+
``[database] sql_alchemy_conn_async = postgresql+asyncpg://...`` explicitly.
38+
3039
6.8.0
3140
.....
3241

‎providers/postgres/docs/index.rst‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ PIP package Version required
106106
``apache-airflow-providers-common-sql`` ``>=1.32.0``
107107
``psycopg2-binary`` ``>=2.9.9; python_version < "3.13"``
108108
``psycopg2-binary`` ``>=2.9.10; python_version >= "3.13"``
109-
``asyncpg`` ``>=0.30.0``
109+
``psycopg[binary]`` ``>=3.2.9; python_version < "3.14"``
110+
``psycopg[binary]`` ``>=3.3.3; python_version >= "3.14"``
110111
========================================== ======================================
111112

112113
Cross provider package dependencies

‎providers/postgres/pyproject.toml‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,12 @@ dependencies = [
6262
"apache-airflow>=2.11.0",
6363
"apache-airflow-providers-common-compat>=1.12.0",
6464
"apache-airflow-providers-common-sql>=1.32.0",
65+
# psycopg2 remains the sync driver for now; its removal and the sync migration to
66+
# psycopg3 are tracked at https://github.com/apache/airflow/issues/68453
6567
"psycopg2-binary>=2.9.9; python_version < '3.13'",
6668
"psycopg2-binary>=2.9.10; python_version >= '3.13'",
67-
"asyncpg>=0.30.0",
69+
"psycopg[binary]>=3.2.9; python_version < '3.14'",
70+
"psycopg[binary]>=3.3.3; python_version >= '3.14'",
6871
]
6972

7073
# The optional dependencies should be modified in place in the generated file
@@ -73,6 +76,9 @@ dependencies = [
7376
"amazon" = [
7477
"apache-airflow-providers-amazon>=2.6.0",
7578
]
79+
"asyncpg" = [
80+
"asyncpg>=0.30.0",
81+
]
7682
"microsoft.azure" = [
7783
"apache-airflow-providers-microsoft-azure>=12.8.0"
7884
]
@@ -87,10 +93,6 @@ dependencies = [
8793
"polars" = [
8894
"polars>=1.26.0"
8995
]
90-
"psycopg" = [
91-
"psycopg[binary]>=3.2.9; python_version < '3.14'",
92-
"psycopg[binary]>=3.3.3; python_version >= '3.14'",
93-
]
9496
"sqlalchemy" = [
9597
"sqlalchemy>=1.4.54"
9698
]
@@ -108,6 +110,7 @@ dev = [
108110
# Additional devel dependencies (do not remove this line and add extra development dependencies)
109111
"apache-airflow-providers-common-sql[pandas]",
110112
"apache-airflow-providers-common-sql[polars]",
113+
"apache-airflow-providers-postgres[asyncpg]",
111114
"apache-airflow-providers-postgres[sqlalchemy]"
112115
]
113116

0 commit comments

Comments
 (0)