Skip to content

The pgbouncer-safe defaults cannot connect through PgBouncer: jit and search_path are startup parameters it rejects, or silently drops #23

Description

@AlexeyShalaev

Measured on 0.2.1 against PostgreSQL 17 and PgBouncer 1.25.2 in transaction mode, PgBouncer at its defaults. create_async_session_manager(BasePostgresConfig(...)) with nothing but host, credentials and an application name:

--- sqlalchemy-foundation-kit, its pgbouncer-safe settings ---
  PgBouncer transaction mode, defaults     ok=0    errors={'ProtocolViolationError': 800}
      first error: unsupported startup parameter: jit

Every connection is refused. BasePostgresConfig.jit defaults to "off" and create_async_session_manager sends it as an asyncpg server_settings entry, which arrives at PgBouncer as a startup parameter, and PgBouncer only passes through the handful it tracks (client_encoding, datestyle, timezone, standard_conforming_strings, application_name). The configuration guide says jit="off" # Required for pgbouncer; measured, it is the one setting that makes the connection impossible through PgBouncer.

The usual admin-side workaround makes it worse. With ignore_startup_parameters = jit,search_path in PgBouncer the connections succeed, and PgBouncer drops both parameters on the floor:

  direct to PostgreSQL                                  jit='off' search_path='app'            application_name='probe'
  PgBouncer, ignore_startup_parameters=jit,search_path  jit='on'  search_path='"$user", public' application_name='probe'

So db_schema is silently not applied and every query lands in public. That is the dangerous half: a setting the library accepted and the database ignored.

For context, the reason this configuration existed is gone on current PgBouncer. Plain SQLAlchemy + asyncpg with the driver's default statement cache runs 800 of 800 statements through PgBouncer 1.25 in transaction mode without an error, because max_prepared_statements has defaulted to 200 since 1.22. Forcing max_prepared_statements=0 brings the classic failure back (320 of 800: prepared statement "__asyncpg_stmt_346__" does not exist), so the zero statement caches and AsyncCConnection still earn their place for older PgBouncers; jit never did.

What I think it needs: jit should default to None and never be sent as a startup parameter unless asked for; db_schema should not be delivered as a search_path startup parameter either, because under transaction pooling the only places a per-role setting can live are ALTER ROLE ... SET / ALTER DATABASE ... SET on the server, or PgBouncer's track_extra_parameters (1.18+), and inside a transaction as SET LOCAL. The unit of work owns the transaction, so SET LOCAL search_path at the start of each transaction() / query() block is the one form that is correct through a transaction pooler; that or schema-qualified metadata, documented as the choice. The configuration guide's PgBouncer section needs rewriting around what was measured: the statement caches, the connection class, and the parameters PgBouncer will and will not carry.

Scripts: pgbouncer_lab.py and jit_probe.py in https://github.com/bedrock-python/bedrock-python.github.io/tree/docs/production-python-series/docs/blog/lab/2026-09-07-pgbouncer-async-sqlalchemy.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions