A high-performance multithreaded PostgreSQL connection pooler built in Rust. Does one thing and does it well — pools connections so your PostgreSQL handles thousands of clients without breaking a sweat.
Drop-in replacement. No app changes. PgDoorman caches and remaps prepared statements transparently across server connections in transaction mode — just point your connection string at it and go. No DISCARD ALL, no DEALLOCATE, no driver hacks. PgBouncer added similar support in 1.21, but remains single-threaded; Odyssey added it in 1.3, but has known reliability issues in edge cases.
Battle-tested with real drivers. Two years of production use with Go (pgx), .NET (Npgsql), Python (asyncpg, SQLAlchemy), Node.js. Protocol edge cases — pipelined batches, async Flush, Describe flow, cancel requests over TLS — are covered by comprehensive multi-language BDD tests.
Natively multithreaded. PgBouncer is single-threaded. Running multiple instances via SO_REUSE_PORT leads to unbalanced pools: clients connect evenly but disconnect unpredictably, leaving some instances overloaded while others sit idle. PgDoorman uses a single shared pool across all worker threads, ensuring correct connection distribution at any scale.
Full extended query protocol support. Benchmarks show Odyssey is up to 61% slower with the extended query protocol in transaction mode. PgDoorman handles simple, extended, and prepared protocols equally well — including pipelined batches and async Flush flow that cause issues in other poolers.
Automated benchmarks on AWS Fargate (16 vCPU, pool size 40, pgbench 30s per test):
| Scenario | vs PgBouncer | vs Odyssey |
|---|---|---|
| Extended protocol, 500 clients + SSL | x3.5 | +61% |
| Prepared statements, 500 clients + SSL | x4.0 | +5% |
| Simple protocol, 10,000 clients | x2.8 | +20% |
| Extended + SSL + Reconnect, 500 clients | +96% | ~0% |
PgBouncer is single-threaded — these ratios reflect a single PgBouncer instance vs a single PgDoorman instance. Full benchmark results.
| PgDoorman | PgBouncer | Odyssey | |
|---|---|---|---|
| Multithreaded | Yes | No | Yes |
| Prepared statements in transaction mode | Yes | Since 1.21 | Since 1.3 |
| Full extended query protocol | Yes | Yes | Partial |
| Zero-downtime binary upgrade | Yes | Yes | Yes |
Deferred BEGIN (lazy server acquire) |
Yes | No | No |
| Auto-config from PostgreSQL | Yes | No | No |
| YAML / TOML config | Yes | No (INI) | No (own format) |
| Human-readable durations & sizes | Yes | No | No |
Native pg_hba.conf format |
Yes | Yes | Since 1.4 |
| Auth query (dynamic users) | Yes | Yes | Yes |
| PAM auth | Yes | Yes | Yes |
| LDAP auth | No | Since 1.25 | Yes |
| Prometheus metrics | Built-in | External | Built-in |
general:
host: "0.0.0.0"
port: 6432
admin_username: "admin"
admin_password: "change_me"
pools:
mydb:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
users:
- username: "app"
password: "md5..." # hash from pg_shadow / pg_authid
pool_size: 40Passthrough authentication (default): When
server_usernameandserver_passwordare omitted, PgDoorman reuses the client's cryptographic proof (MD5 hash or SCRAM ClientKey) to authenticate to PostgreSQL automatically. This is the recommended setup when the pool username matches the backend PostgreSQL user — no plaintext passwords in config needed.Set
server_username/server_passwordonly when the backend user differs from the pool user (e.g., username mapping) or for JWT authentication where there is no password to pass through.
Instead of listing every user in the config, pg_doorman can look up credentials directly from PostgreSQL. The query must return a column named passwd or password containing the MD5 or SCRAM hash. Any extra columns are ignored.
Quickstart — using pg_shadow directly (requires superuser):
pools:
mydb:
server_host: "127.0.0.1"
server_port: 5432
pool_mode: "transaction"
auth_query:
query: "SELECT passwd FROM pg_shadow WHERE usename = $1"
user: "postgres"
password: "postgres_password"By default auth_query runs in passthrough mode: each dynamic user gets their own backend pool and authenticates as themselves. To force all users through a single backend role, set server_user / server_password (dedicated mode).
Static users (defined in
users) are checked first. auth_query is only consulted when the username is not found among static users.
Production: don't use superuser for auth queries. Create a
SECURITY DEFINERfunction with a dedicated low-privilege role instead.
Or generate a config automatically:
pg_doorman generate --host your-db-host --output pg_doorman.yaml# Start
pg_doorman pg_doorman.yaml
# Connect — same as you would to PostgreSQL directly
psql -h localhost -p 6432 -U app mydbYour application connection string changes only the host and port:
postgresql://app:secret@localhost:6432/mydb
Pre-built binaries: Download from GitHub Releases.
# Ubuntu/Debian
sudo add-apt-repository ppa:vadv/pg-doorman && sudo apt-get install pg-doorman
# Fedora/RHEL/Rocky
sudo dnf copr enable vadvya/pg-doorman && sudo dnf install pg-doorman
# Docker
docker pull ghcr.io/ozontech/pg_doorman# Recommended: build with jemalloc tuning for optimal memory management
JEMALLOC_SYS_WITH_MALLOC_CONF="dirty_decay_ms:30000,muzzy_decay_ms:30000,background_thread:true,metadata_thp:auto" \
cargo build --release
# Binary will be at target/release/pg_doormanThis repository also includes patroni_proxy — a TCP proxy for Patroni-managed PostgreSQL clusters. Zero-downtime failover: existing connections are preserved during cluster topology changes.
- pg_doorman deploys on the same host as PostgreSQL — connection pooling and prepared statement caching benefit from low latency to the database
- patroni_proxy deploys as a sidecar in the application pod — TCP routing and role-based failover (leader/sync/async) with least-connections balancing
See patroni_proxy documentation for details.
Full documentation, configuration reference, and tutorials: ozontech.github.io/pg_doorman
make pull # pull test image
make test-bdd # run all integration tests (Docker-based, fully reproducible)See the Contributing Guide for details.
