Repository navigation
Description
Versions: sqlalchemy-foundation-kit 0.4.0 (same code on master, adcd645), SQLAlchemy 2.0.54, asyncpg 0.31.0, Python 3.13.12
Two of the three pool kinds the configuration guide lists cannot build an engine. _apply_pool_settings (base/engine.py:237-247) passes pool_size, max_overflow and pool_timeout to create_async_engine for every pool class, and SQLAlchemy rejects them for NullPool and StaticPool. The docstring above it (base/engine.py:227-229) says a hasattr check keeps them away from "pools like NullPool or StaticPool"; there is no such check. PoolSettings.size, .max_overflow and .timeout are not optional (contrib/settings/postgres.py:58-66), so every PoolSettings carries all three. AsyncSessionManager(url, pool_settings=...) without a poolclass fails the same way, because the constructor's default is "null" (session/manager.py:229).
The advice on which pool to choose does not run either. The AsyncSessionManager docstring says Use "queue" for production, "null" for testing. (session/manager.py:247-248). The three examples in the AsyncSessionManagerBuilder docstring use .with_pool("queue") (session/builder.py:35, 44, 55; also with_pool's argument doc at 103). "queue" resolves to the sync QueuePool (base/engine.py:52), and SQLAlchemy refuses it on an asyncio engine. docs/agents.md:371-374 correctly says "async_adapted_queue". The docstrings say otherwise.
Reproduction
No database is needed, because building the engine is enough:
import asyncio
from pydantic import SecretStr
from sqlalchemy_foundation_kit import AsyncSessionManager, AsyncSessionManagerBuilder, create_async_session_manager
from sqlalchemy_foundation_kit.contrib.settings import BasePostgresConfig, ConnectionSettings, PoolSettings
URL = "postgresql+asyncpg://user:secret@localhost:5432/app" # nothing connects: building the engine is enough
async def attempt(label: str, build) -> None:
try:
manager = build()
except Exception as exc:
print(f"{label:62} {type(exc).__name__}: {str(exc).splitlines()[0]}")
else:
print(f"{label:62} ok, pool = {type(manager.engine.pool).__name__}")
await manager.aclose()
def from_settings(kind: str):
config = BasePostgresConfig(
connection=ConnectionSettings(password=SecretStr("secret"), database="app"),
application_name="probe",
pool=PoolSettings(kind=kind, max_overflow=0),
use_orjson_serialization=False,
)
return lambda: create_async_session_manager(config)
async def main() -> None:
for kind in ("async_adapted_queue", "null", "static"):
await attempt(f'PoolSettings(kind="{kind}", max_overflow=0)', from_settings(kind))
await attempt("AsyncSessionManager(url)", lambda: AsyncSessionManager(URL))
await attempt("AsyncSessionManager(url, pool_settings=PoolSettings())",
lambda: AsyncSessionManager(URL, pool_settings=PoolSettings()))
await attempt('AsyncSessionManager(url, poolclass="queue")', lambda: AsyncSessionManager(URL, poolclass="queue"))
await attempt('AsyncSessionManagerBuilder(url).with_pool("queue").build()',
lambda: AsyncSessionManagerBuilder(URL).with_pool("queue").build())
asyncio.run(main())Actual
PoolSettings(kind="async_adapted_queue", max_overflow=0) ok, pool = AsyncAdaptedQueuePool
PoolSettings(kind="null", max_overflow=0) TypeError: Invalid argument(s) 'pool_size','max_overflow','pool_timeout' sent to create_engine(), using configuration PGDialect_asyncpg/NullPool/Engine. Please check that the keyword arguments are appropriate for this combination of components.
PoolSettings(kind="static", max_overflow=0) TypeError: Invalid argument(s) 'pool_size','max_overflow','pool_timeout' sent to create_engine(), using configuration PGDialect_asyncpg/StaticPool/Engine. Please check that the keyword arguments are appropriate for this combination of components.
AsyncSessionManager(url) ok, pool = NullPool
AsyncSessionManager(url, pool_settings=PoolSettings()) TypeError: Invalid argument(s) 'pool_size','max_overflow','pool_timeout' sent to create_engine(), using configuration PGDialect_asyncpg/NullPool/Engine. Please check that the keyword arguments are appropriate for this combination of components.
AsyncSessionManager(url, poolclass="queue") ArgumentError: Pool class QueuePool cannot be used with asyncio engine (Background on this error at: https://sqlalche.me/e/20/pcls)
AsyncSessionManagerBuilder(url).with_pool("queue").build() ArgumentError: Pool class QueuePool cannot be used with asyncio engine (Background on this error at: https://sqlalche.me/e/20/pcls)
Why
-
base/engine.py:237-247copies every non-Nonepool setting, whateverpoolclassis. The comment at 244-246 says pools that do not support them "will ignore them or raise TypeError".NullPoolandStaticPoolraise. -
The
PoolSettingsvalidator (contrib/settings/postgres.py:68-73) only rejectsstaticwithmax_overflow > 0. So the combination the guide recommends,kind="static", max_overflow=0(docs/guide/configuration.md:208-215), passes validation and then fails when the engine is built. -
Filtering the kwargs would not make
kind="static"what the guide describes. The guide calls it a "Fixed-size pool with no overflow" (docs/guide/configuration.md:184) and shows it at 208 as "High-throughput: static pool" withsize=50. SQLAlchemy'sStaticPoolis "A Pool of exactly one connection, used for all requests", andsizehas no meaning for it. On an async engine, concurrent sessions share that one asyncpg connection:engine = create_async_engine(url, poolclass=StaticPool) # what kind="static" builds once the kwargs are filtered sessions = async_sessionmaker(engine) async def request() -> int: async with sessions() as s: return await s.scalar(text("SELECT pg_backend_pid() FROM pg_sleep(0.1)")) print("sequential:", [await request() for _ in range(3)]) for result in await asyncio.gather(*(request() for _ in range(3)), return_exceptions=True): ...
sequential: [98, 98, 98] concurrent: InterfaceError: (sqlalchemy.dialects.postgresql.asyncpg.InterfaceError) <class 'asyncpg.exceptions._base.InterfaceError'>: cannot perform operation: another operation is in progress concurrent: 98 concurrent: InterfaceError: (sqlalchemy.dialects.postgresql.asyncpg.InterfaceError) <class 'asyncpg.exceptions._base.InterfaceError'>: cannot use Connection.transaction() in a manually started transactionA fixed-size pool is
async_adapted_queuewithmax_overflow=0. -
PoolRegistry(base/engine.py:50-57) andPoolClassStrinclude"queue"and"singleton_thread". Both are sync pools and raiseArgumentError: ... cannot be used with asyncio enginein a library that is async-only.
Proposal
- In
_apply_pool_settings, passpool_size,max_overflowandpool_timeoutonly whenissubclass(poolclass, QueuePool). That coversAsyncAdaptedQueuePool,FallbackAsyncAdaptedQueuePool, registered subclasses and the classinstrument_pool_classbuilds. EveryPoolacceptspool_recycleandpool_pre_ping. Then fix the docstring at 227-229 to describe what the code does. - Document
staticas one shared connection (for tests), and drop the "High-throughput" example. For a fixed-size pool, point toasync_adapted_queuewithmax_overflow=0. The validator could then rejectkind="static"withsize > 1rather than with overflow. - In the
AsyncSessionManagerand builder docstrings, replace"queue"with"async_adapted_queue". Either drop"queue"and"singleton_thread"fromPoolClassStrand the built-in registry, or resolve"queue"toAsyncAdaptedQueuePool. Nothing else can run on these engines. - Consider making
"async_adapted_queue"the defaultpoolclassofAsyncSessionManagerand the builder (session/builder.py:70). TodayAsyncSessionManager(url)quietly opens a new connection for every session, and until item 1 lands, addingpool_settings=without also settingpoolclasscrashes.create_async_session_manageralready ends up onasync_adapted_queuethroughPoolSettings.kind. If the"null"default stays (docs/agents.md:371documents it as "for tests"), the docstrings should say so instead of recommending"queue".
Activity
AlexeyShalaev commentedon Oct 16, 2026
Reproduced on master with the script above: same seven lines.
Plan:
_apply_pool_settingspassespool_size,max_overflowandpool_timeoutonly whenissubclass(poolclass, QueuePool). That covers the async queue pools, registered subclasses and the classinstrument_pool_classbuilds.pool_recycleandpool_pre_pingstill go to every pool. The docstring will say what the code does.kind="static"gets documented as what SQLAlchemy builds: one connection shared by every session, which is fine for tests and wrong for anything concurrent. The "high-throughput" example goes. A fixed-size pool isasync_adapted_queuewithmax_overflow=0. The validator stops checkingmax_overflowforstatic, since the setting doesn't apply there, and rejectssize > 1with a message pointing atasync_adapted_queue. That waykind="static", size=50fails at config load and not asanother operation is in progressunder load. It can't break a working config: until now noPoolSettingswithkind="static"could build an engine.AsyncSessionManager(url, pool_settings=...)withoutpoolclasstakes the pool frompool_settings.kind. The protocol already carrieskind, andcreate_async_session_manageralready does this. Without the change, the kwargs filter would turn today's crash into a silentNullPoolthat ignores the sizes. The signature default becomesNone, which still means"null"when nopool_settingsare given.AsyncSessionManager/builder docstrings,"queue"becomes"async_adapted_queue", and"null"is described as one connection per checkout, for tests and scripts.Things I'm not changing:
"queue"and"singleton_thread"stay inPoolRegistry/PoolClassStr. The registry andresolve_pool_classare public and can serve a sync engine too (a migrations engine built from the samePoolSettings), so making"queue"resolve toAsyncAdaptedQueuePoolwould break that. On the async manager, SQLAlchemy already rejects them at construction with a clearArgumentError. The trap was the docstrings recommending them, and those get fixed."null"default ofAsyncSessionManagerand the builder stays. Pytest suites that build a manager per test (and per event loop) depend on it, because a pooled asyncpg connection can't cross loops. Making the defaultasync_adapted_queuewould break them for an aesthetic gain.AlexeyShalaev commentedon Oct 16, 2026
Fix is in #61.
PoolSettings(kind="null")andkind="static", size=1now build an engine and run queries (integration test on PostgreSQL 17 throughcreate_async_session_manager).AsyncSessionManager(url, pool_settings=...)withoutpoolclasspicks uppool_settings.kind. The docstrings recommend"async_adapted_queue".What you might need to change: a config with
kind="static"andsize > 1now fails validation, with a message pointing atasync_adapted_queue+max_overflow=0. That config could never build an engine, so it only turns a crash at engine creation into one at config load."queue"stays in the registry for sync engines, as described in the plan above.