On July 1, ClickHouse’s Managed Postgres team published how they pushed PgBouncer from 87,000 to 336,000 transactions per second—a 4x gain—without replacing the pooler, without adding hardware, and without changing a line of client code. The technique uses a Linux kernel feature called SO_REUSEPORT that has existed since 2013 and is almost never deployed with PgBouncer. If you’re running Postgres at any meaningful scale, this is worth understanding.
PgBouncer Has a Single-Threaded Ceiling
PgBouncer is single-threaded. That’s not a bug or an oversight—it was a deliberate design choice that made the pooler lightweight and predictable. It uses libevent for async I/O and handles thousands of connections efficiently on one core. The problem is that it only uses one core. On a 16-vCPU server, 15 cores sit idle while PgBouncer saturates the remaining one.
In ClickHouse’s benchmark on a 16-vCPU c7i.4xlarge instance, a single PgBouncer process peaked at 87,000 TPS and consumed just 7.7% of total CPU—approximately one core. Under high load with 256 concurrent clients, however, it didn’t hold steady; it degraded to 76,893 TPS. The database itself wasn’t the bottleneck. The door to the database was. As the ClickHouse team put it: “the pooler becomes the bottleneck before Postgres reaches capacity.”
Related: pgrust Hits 100%: The AI-Built Postgres Rewrite in Rust
The Fix: SO_REUSEPORT and a Fleet of Processes
The solution is to run multiple PgBouncer processes on the same port. Linux’s SO_REUSEPORT socket option—available since kernel 3.9—allows exactly this. Multiple processes bind to the same IP:port, and the kernel distributes incoming connections across all of them using a hash of the connection’s 4-tuple (source IP, source port, destination IP, destination port). Clients connect to one endpoint; consequently, the kernel is load-balancing across the entire fleet without any client-side changes required.
ClickHouse sizes the fleet to available CPU cores. Their 16-process fleet on the same test machine reached 336,469 TPS at 256 concurrent clients—CPU utilization jumped from 7.7% to 48.9%, meaning the fleet put roughly 8 cores to work. Moreover, unlike the single process, the fleet didn’t degrade under load; throughput stayed flat as connections climbed. Single PgBouncer process: 87,000 peak TPS, degrading to 76,893 under load, at 7.7% CPU. Fleet of 16 processes: 336,000 peak TPS, holding at 336,469 under load, at 48.9% CPU. The improvement isn’t a rounding error—it’s a 4.4x difference on hardware you already own.
The Part Nobody Mentions: Query Cancellations
There’s a non-obvious failure mode with the multi-process approach. PostgreSQL cancel requests arrive on a separate connection carrying only a 64-bit cancel key. With SO_REUSEPORT, the kernel may route that cancel to a different process than the one managing the original session. As a result, the cancel lands at a process that doesn’t know about the query, and nothing happens—queries that should be cancelled continue running.
ClickHouse solved this with a peering mechanism. Processes maintain awareness of each other’s active sessions. When a misdirected cancel arrives, the receiving process forwards it to the correct one. Furthermore, this is the implementation detail that makes the architecture production-safe, not just a benchmark curiosity. Without peering, the approach would silently fail in a specific but consequential way. Connection budgets are handled similarly: max_client_conn and max_db_connections are divided across the fleet so the total never oversubscribes Postgres.
PgBouncer vs. Alternatives: When to Apply This Fix
Multi-threaded pooler alternatives exist. PgCat, built by Instacart in Rust using Tokio, handles high concurrency natively in a single process and adds sharding support. Odyssey, developed by Yandex, handles 50k+ connections in production with a multi-threaded coroutine-based architecture. Go co-creator Brad Fitzgerald made the case bluntly in the Hacker News thread: “I find it ridiculous that PgBouncer even needs to exist. Postgres should be doing this internally after 20+ years.” He’s not wrong in principle. In practice, however, teams aren’t waiting for Postgres to solve connection pooling.
The SO_REUSEPORT approach makes sense if you’re already running PgBouncer in production and hitting its ceiling. It’s a low-disruption change—no migration, no re-testing a new pooler, no operational unknowns. For greenfield projects, or teams already evaluating their stack, PgCat or Odyssey are worth serious consideration. Either way, the core finding holds: at high connection counts, PgBouncer’s single-threaded design becomes the limiting factor before Postgres does. If your monitoring shows PgBouncer CPU maxed on one core while Postgres is comfortable, you’ve found your bottleneck—and now you know how to fix it.
Key Takeaways
- PgBouncer’s single-threaded design caps throughput to one CPU core regardless of server size—at scale, it becomes the bottleneck before Postgres does
- Running multiple PgBouncer processes on the same port with
SO_REUSEPORTenables kernel-level load balancing with no client-side changes; ClickHouse measured 4.4x throughput improvement on a 16-vCPU machine - Query cancellation requires a peering mechanism between processes—without it, cancel requests silently fail when routed to the wrong process
- For existing PgBouncer deployments hitting the ceiling, this technique is the lowest-disruption path; for greenfield projects, PgCat and Odyssey offer native multi-threaded scaling













