Environment
- pgdog: 0.1.41
- PostgreSQL: 17 (Stolon-managed, async replication)
- Client driver:
lib/pq (Go), same behaviour expected from pgx and JDBC
- OS: Debian Trixie x86_64
pooler_mode: transaction
prepared_statements: reproduced with extended (default), extended_anonymous, and full
Summary
Any query sent via the Postgres extended query protocol (Parse → Bind → Execute) inside an explicit BEGIN / COMMIT block fails with driver: bad connection after a ~10 s hang when pgdog is in transaction pooling mode. Simple-protocol queries (psql interactive, no parameters) against the same endpoint work correctly.
Minimal reproducer
A Go probe exercising five escalating protocol patterns against pgdog at :6432 via lib/pq:
[1/5] ping (simple protocol, no params, no tx) … ok (160ms)
[2/5] SELECT 1 (simple protocol, no params, no tx) … ok (22ms)
[3/5] SELECT $1::int (extended protocol, params, no tx) … ok (40ms)
[4/5] BEGIN; SELECT $1::int; ROLLBACK (extended in tx) … FAIL (10.04s)
driver: bad connection
[5/5] (not reached)
Step 4 is deterministic. The smallest reproducer is:
BEGIN;
SELECT $1::int; -- bound parameter via extended protocol
ROLLBACK;
lib/pq sends this as:
Query("BEGIN") — simple protocol
Parse("", "SELECT $1::int") → Bind(params) → Execute → Sync — extended protocol
Query("ROLLBACK") — simple protocol
The same sequence issued directly against the Stolon primary (bypassing pgdog) succeeds immediately, confirming pgdog is the variable.
Expected behaviour
In transaction pooling mode, once BEGIN is received and the backend returns ReadyForQuery with status T (in transaction), the backend should remain pinned to that client until COMMIT or ROLLBACK. All subsequent messages — including extended-protocol Parse/Bind/Execute — should be forwarded to the same pinned backend.
Actual behaviour
The connection hangs for ~10 seconds then returns driver: bad connection. Our hypothesis is that pgdog releases the backend back to the pool after the BEGIN / ReadyForQuery(T) exchange (or between Parse and Bind/Execute), so the subsequent extended-protocol messages are routed to a different backend that has no knowledge of the open transaction, causing a protocol-level error or timeout.
What we tried
All three non-default prepared_statements modes were tested — extended_anonymous, full (including 0.1.41's fix for full rewrites of extended protocol), and the default extended — none resolved the issue. This suggests the root cause is in transaction-state tracking / backend pinning rather than in prepared-statement rewriting.
Workaround: switching to pooler_mode = "session" eliminates the error, but at the cost of per-transaction R/W split and correct failover routing.
Impact
Any Go (or other) application using parameterised queries inside explicit transactions — which is the normal pattern for database/sql with BeginTx — is completely unable to use pgdog in transaction mode.
Environment
lib/pq(Go), same behaviour expected frompgxand JDBCpooler_mode:transactionprepared_statements: reproduced withextended(default),extended_anonymous, andfullSummary
Any query sent via the Postgres extended query protocol (Parse → Bind → Execute) inside an explicit
BEGIN/COMMITblock fails withdriver: bad connectionafter a ~10 s hang when pgdog is intransactionpooling mode. Simple-protocol queries (psqlinteractive, no parameters) against the same endpoint work correctly.Minimal reproducer
A Go probe exercising five escalating protocol patterns against pgdog at
:6432vialib/pq:Step 4 is deterministic. The smallest reproducer is:
lib/pqsends this as:Query("BEGIN")— simple protocolParse("", "SELECT $1::int")→Bind(params)→Execute→Sync— extended protocolQuery("ROLLBACK")— simple protocolThe same sequence issued directly against the Stolon primary (bypassing pgdog) succeeds immediately, confirming pgdog is the variable.
Expected behaviour
In
transactionpooling mode, onceBEGINis received and the backend returnsReadyForQuerywith statusT(in transaction), the backend should remain pinned to that client untilCOMMITorROLLBACK. All subsequent messages — including extended-protocol Parse/Bind/Execute — should be forwarded to the same pinned backend.Actual behaviour
The connection hangs for ~10 seconds then returns
driver: bad connection. Our hypothesis is that pgdog releases the backend back to the pool after theBEGIN/ReadyForQuery(T)exchange (or between Parse and Bind/Execute), so the subsequent extended-protocol messages are routed to a different backend that has no knowledge of the open transaction, causing a protocol-level error or timeout.What we tried
All three non-default
prepared_statementsmodes were tested —extended_anonymous,full(including 0.1.41's fix for full rewrites of extended protocol), and the defaultextended— none resolved the issue. This suggests the root cause is in transaction-state tracking / backend pinning rather than in prepared-statement rewriting.Workaround: switching to
pooler_mode = "session"eliminates the error, but at the cost of per-transaction R/W split and correct failover routing.Impact
Any Go (or other) application using parameterised queries inside explicit transactions — which is the normal pattern for
database/sqlwithBeginTx— is completely unable to use pgdog intransactionmode.