how connection pooler state leaks (a tiny experiment)
Connection poolers leak session state because they hand dirty database sockets to new clients, skipping costly cleanup queries to preserve performance.
By Poppy Lin
Sparked by Why we built yet another Postgres connection pooler · discussion

A friend and I were chatting recently about how terrifying it is when connection pooler state leaks! This came up because we were reading the PgDog announcement post and the ensuing Hacker News thread about it. In that thread, developers were discussing a nightmare scenario where a session setting like SET statement_timeout can magically bleed into a completely different application's queries.
If an analytics dashboard sets a strict timeout and drops its connection, a critical payment service might suddenly start failing because it inherited that exact same timeout. The conversation made me want to understand how that state bleed actually functions at the bare socket level.
Instead of just reading the documentation, let's imagine setting up a bare-minimum Docker instance with PgBouncer sitting in front of a Postgres database. To guarantee that clients would share connections immediately, you could explicitly set the pooler to transaction pooling (pool_mode = transaction with default_pool_size = 1).
To see what's going on under the hood, we'd really only need one Postgres function: SELECT pg_backend_pid(). I love this function because it just returns the literal process ID of the Postgres server process attached to your current session. No theory, just raw system truth!
Here is what the terminal output would look like if you ran a simple 15-line Python script that connects as "Client A", changes the configuration, disconnects, and then connects immediately again as "Client B":
[Client A] Connected. Postgres PID: 1234
[Client A] Running: SET statement_timeout = 1
[Client A] Disconnected.
[Client B] Connected. Postgres PID: 1234
[Client B] Running 2-second sleep...
psycopg2.errors.QueryCanceled: canceling statement due to statement timeout
So what just happened in this scenario? Client A connects to the database via the pooler and asks Postgres for its current process ID. The server responds with 1234. Client A then does something kind of rude: it mutates the connection state by setting the statement timeout to a single millisecond. The pooler sees that the transaction is done, so Client A disconnects and goes away.
But then Client B connects to the pooler. To the Python script running Client B, this looks like a completely fresh, pristine database connection. Client B asks for the process ID, and the server responds with 1234. IT'S THE EXACT SAME POSTGRES PROCESS!
Client B inherited the timeout configuration because "connection pooling" in this mode just means PgBouncer hands the raw, already-open network socket directly over to the next connection in the queue. And because the Postgres process serving PID 1234 still has that aggressive one-millisecond timeout burned into its memory from the previous user, Client B attempts to run a perfectly normal two-second sleep query and crashes immediately. The state has completely leaked across the application boundary.
If a connection pooler just hands over dirty sockets, what else leaks across these invisible state buckets? It turns out you can run into exactly three main categories of weird bugs:
- Temporary tables: A temporary table created by one client for an intermediate calculation is suddenly visible to the next client that inherits the process.
- Prepared statements: Client B might crash because it tries to declare a prepared statement name that Client A already registered on this exact same socket.
- Session variables: Application time zones, memory limits, and locale settings altered by one query will silently apply to the next user in line.
Anyway, why doesn't the pooler just clean this up? Postgres actually provides a dedicated command called DISCARD ALL that safely clears everything, destroying temporary tables and resetting the session to a pristine default state.
But there is a massive performance trade-off! PgBouncer intentionally skips running this cleanup query by default in transaction mode because executing it between every single transaction adds a costly network round trip. People in that Hacker News thread were discussing how you can mitigate this by changing the server_reset_query defaults to force the pooler to run the cleanup command, though doing so fundamentally changes the latency profile of your database traffic.
I certainly have a ton left to learn about PgBouncer's advanced reset strategies (how do you even tune that reset query for a massive application without destroying your database performance??), and very likely I missed some nuance about how pooling works in other modes. But I'm seriously amazed that we can reason through this terrifying state-leak bug conceptually with just a hypothetical script and a simple PID check :)