Dev & EngARTICLE

Polar Proxy Eliminates Lock Contention on the Critical Path of Read Connections

Article · Dev & Eng
Reading
7 min
In short

A ProxySQL fork adapted for PolarDB shows how to take the connection pool out of each query's critical path, reusing read replicas with exact matching and no lock contention.

Anyone who operates a proxy in front of PostgreSQL read replicas knows the problem: opening a new connection is expensive (TCP handshake, TLS, authentication, startup parameter negotiation), so every serious proxy keeps a pool of already-open connections to reuse. The hard part isn't holding idle connections, it's deciding when two connections are truly interchangeable. A post published on November 8, 2026 on the Awide Labs blog, authored by Vadim Iatsenko and syndicated by Planet PostgreSQL, describes how Polar Proxy (a codebase derived from ProxySQL, adapted for PolarDB) reorganized this pool to take per-query overhead off the critical path.

The Exact-Match Problem

Two connections are only safe to reuse if they were opened with the same user, the same database, the same session options, and, in PolarDB's case, the same startup profile, which decides whether the connection negotiates LSN reporting and in-band waits. A connection that negotiates these extensions also carries a client startup identity (today the endpoint, with TLS fields reserved in the same key), which enters the comparison.

This matters because any reader who has operated read-after-write routing over streaming replication replicas knows the devil lives in this detail: reusing a connection that doesn't track LSN to serve a client that requested causal consistency breaks the guarantee without producing any visible error. Polar Proxy solves this with a four-part match key: startup profile generation, user/database hash, client startup identity, and startup options. Any mismatch forces a new connection.

Where Generic ProxySQL Wastes Time

The source is direct in pointing out the bottlenecks of upstream ProxySQL's generic pool, which wasn't designed for LSN-sensitive routing and transaction metadata. Four points stand out:

  • Shallow matching: the generic pool can't see PolarDB's state dimensions, so a connection lacking the required state is indistinguishable from one that has it.
  • Repeated work per query: on every read, the upstream path scans connection lists, rebuilds a temporary candidate array, and re-acquires the hostgroup-manager lock, even when this work was already done moments earlier.
  • Contention at points unrelated to routing: query digest statistics are updated in a shared map behind a single per-process lock on every query, and the logger flush is checked on every event loop pass even when logging is off.
  • Per-packet copying: large results are forwarded to the client by copying each packet through an intermediate buffer.

A Single Owner for Read Connections

The core change is organizational: the central connection pool becomes the sole owner of every reusable read connection. The read pool no longer holds its own connections; it only decides which server to use and which policy to apply (LSN target, lag ceiling, warmup). Even a connection that a worker is briefly holding for local reuse remains accounted for in the central pool's USED list, the worker's cache is a reuse handle, not a second owner.

Each per-server connection list carries the four-part key plus an exact-match index, and connections move between FREE and USED under a single per-server mutex, with the list, index, and counters updated together. This eliminates the ambiguity the generic pool had: either the connection matches exactly what the read requires, or it isn't reused.

Local Reuse and Power-of-Two-Choices Selection

Two micro-adjustments cut the per-query cost of finding a connection. The first is worker-local reuse: after choosing a server, the worker stores the used connection in a thread-local cache, by default only until the end of that event loop pass. The next read on the same server reuses it without touching the shared pool.

The second is power-of-two-choices (P2C) selection. When more than two readers are competing, the proxy compares only two candidates, and only those two's load counters are read.

With exactly two readers configured, there's nothing to sample: a worker-local weighted sequence alternates between them, and a read with an LSN target shifts the choice to whichever one has already replicated up to it. With three or more equal-weight replicas and no restrictions (no lag ceiling, no excluded endpoint), the proxy samples a random pair and stops there, picking the less loaded one, when both are usable and, in the case of a read with an LSN target, both have already reached that target; otherwise, it expands to the full list, reusing the pair already evaluated instead of re-evaluating it.

Power-of-two-choices picks a good server by comparing two candidates' load; worker-local reuse then serves repeat reads without touching the shared pool.

Vadim Iatsenko, Awide Labs

Waiting Instead of Spinning

When every compatible connection is in use and there's no free slot to create another, upstream ProxySQL spins and rescans the lists. Polar Proxy instead registers the pending request, along with its match key, read plan, and wait target, and parks it: the event loop keeps serving other sessions while the request waits for a targeted wake-up or a bounded retry deadline.

When a connection is returned or created, it goes straight to the pending request, and only the responsible worker is woken up, via a byte written to that event loop's wakeup pipe. Background warmup (pre-opening connections so that reads split across a transaction don't get stuck on a cold reader) runs on a dedicated thread, outside the hostgroup-manager lock.

Taking Locks Off the Hot Path

Three separate changes attack contention that, at first glance, doesn't even seem related to the connection pool, but was competing for the same CPU time:

  1. Worker-local digests: each worker accumulates query digest statistics in its own small map and only syncs with the shared map when the local map hits its entry limit, during periodic housekeeping, or when someone reads the counters.
  2. Logger skipped when off: the worker first reads the two "logfile open" flags; if both are closed, it returns without reading the clock, without a shared-context lookup, and without locking a buffer.
  3. Batched counters: each backend connection accumulates byte and query counts and only passes them to the server's shared counter in batches, every 64 queries or when the pending byte volume exceeds 64 KiB in either direction.

Zero-Copy Forwarding

The response path gained two optimizations to stop copying bytes unnecessarily. A modified libpq can identify and detach contiguous runs of row packets from a result, using the PSpeekRowRun and PSdetachRowRun routines. This fast path, called pgsql-polardb_result_fast_forward, transfers ownership of the entire buffer instead of copying packet by packet, but it's opt-in and ships disabled by default.

The second optimization is enabled by default: when the batch in the output queue accumulates at least two packets (or one packet too large for the classic buffer) and the frontend is a plain socket with no TLS and no partial packet in flight, the proxy builds an iovec directly from the queued result packets and sends them with a single scatter-gather sendmsg call, capped at 64 packets per call within a byte budget.

What Changes (and What Doesn't) for Replica Operators

The source is emphatic on a point worth repeating for anyone evaluating this engineering: no consistency guarantee changes. This is a pure performance layer on top of the same LSN-based routing and the same safe fallback rules that already existed.

For database administrators, the practical takeaway is different: Polar Proxy is specific to PolarDB, the distributed variant of Postgres, and it depends on the LSN target concept that platform exposes natively. Anyone routing reads today with generic ProxySQL, PgBouncer, or PgCat doesn't get this feature for free just by reading the article.

What's exportable, regardless of stack, are the patterns: exact match keys instead of a generic pool when the application depends on replica consistency; worker-local caching to avoid touching shared structures on every request; P2C instead of scanning every server; request parking instead of spinning when capacity is short; and separating accounting (digests, logs, counters) from the query execution path, syncing in batches.

These are design decisions that any team maintaining their own proxy, or evaluating why a connection pool is competing with the very work of answering queries, can demand from any candidate tool before switching proxies in production.

Source 1: Planet PostgreSQL (https://postgr.es/p/9Bi)

"The Server-First Reader Pool: Exact Connection Reuse and Hot-Path Performance", Awide Labs Blog.

Translated from the Brazilian Portuguese original · Read the original