Why I put Postgres over QUIC (and why you might too)
Breaking the TCP bottleneck in database pooling with multiplexed streams.
Why I put Postgres over QUIC (and why you might too)

Breaking the TCP bottleneck in database pooling with multiplexed streams.
We all know the drill: Postgres is synchronous. One connection, one query at a time. To get around this, we use connection poolers like PgBouncer or PgCat. They’re great, but they still operate on a fundamental limitation of TCP — one connection per “slot.”
I recently experimented with running the Postgres protocol over QUIC (UDP) instead of the traditional TCP. The goal? To see if we could achieve better throughput and lower latency by leveraging QUIC’s native multiplexing.
The Problem: The TCP Connection Wall
Imagine an edge server handling thousands of requests. If each request needs a database query, you’d traditionally need hundreds or thousands of TCP connections. This doesn’t scale well. You eventually hit limits like “too many open files,” and the overhead of managing that many active TCP connections starts to eat into your performance.
While a pooler helps by reusing connections, it still leaves you with a lot of “waiting” time. A roundtrip from a client to a pooler might take 100ms, while the pooler-to-DB jump is only 1ms. That connection is effectively locked for the entire duration, even if it’s just sitting idle for 99% of the time.
The Solution: Multiplexing with QUIC
I modified PgCat to support QUIC. Instead of establishing a new TCP connection for every client, we use a single QUIC connection and open a new stream for each client.
QUIC is an application-level protocol built on top of UDP. It allows us to create “virtual” connections (streams) over a single physical connection. We aren’t changing the Postgres protocol itself — it’s still synchronous — but we’re packing many more of these synchronous interactions into a much smaller footprint.
The Results: The Real Numbers
I ran a comparison: 500 TCP connections vs. 50 QUIC connections with 40 streams each. As the query rate ramped up from 100 to 5,000 QPS over 30 seconds, the difference became undeniable.

At peak load, here’s how they stacked up:
- Latency: QUIC maintained a respectable 232ms average query time, while TCP spiraled out of control, hitting an average of 7,088ms. That’s a 30x improvement in responsiveness under pressure.
- Throughput: QUIC managed to process over 7,500 operations per second, nearly double the 4,000 operations per second that the TCP-based setup could handle before falling behind.
- Active Queries: While the TCP setup saw a massive backlog of over 40,000 active/queued queries, QUIC kept things moving smoothly with just a fraction of that backlog.
Why QUIC?
You might ask, “Why not just use HTTP/2 or another multiplexing layer over TCP?” You could, but QUIC offers two massive advantages:
- Eliminating Head-of-Line (HOL) Blocking: In TCP, if one packet is lost, the entire connection halts until it’s recovered. With QUIC, each stream is independent. If one stream drops a packet, the others keep moving.
- 0-RTT Connection Establishment: QUIC can re-establish connections much faster than TCP. For a pool of connections that are frequently opened and closed, this is a game-changer.
Is This Always Necessary?
It’s important to note that this isn’t a one-size-fits-all solution. This approach primarily improves throughput and latency for high-latency or long distance connections.
If your client and database are in the same availability zone with sub-millisecond latency, or if your compute and storage resources are already shallow (meaning the hardware isn’t the bottleneck), switching to QUIC might be unnecessary. The real power of QUIC here is in managing the “waiting” time that comes with distance and scale.
Conclusion: A Simpler Drop-in?
Integrating QUIC was surprisingly straightforward. It feels like a natural fit for the “Client <-> Pooler” relationship. While it’s not a magic bullet that makes Postgres itself asynchronous, it effectively removes the transport-layer bottleneck that has plagued large-scale deployments for years.
If you’re hitting the limits of traditional TCP pooling, it might be time to look at the transport layer. Multiplexing over single connections isn’t just a “nice to have” — it’s the logical next step for high-concurrency database access.
Check out the experiment at https://github.com/ash-hashtag/postgres-with-quic and the modified PgCat here: https://github.com/ash-hashtag/pgcat
메타데이터
- post_id
- 4cea7d0e0fd8
- slug
- why-i-put-postgres-over-quic-and-why-you-might-too-4cea7d0e0fd8
- url
- https://medium.com/@tenesripranav/why-i-put-postgres-over-quic-and-why-you-might-too-4cea7d0e0fd8
- canonical_url
- https://medium.com/@tenesripranav/why-i-put-postgres-over-quic-and-why-you-might-too-4cea7d0e0fd8
- author_url
- https://medium.com/@tenesripranav
- status
- ok
- fetched_at
- 2026-06-21 07:44:09