PgBouncer Setup
Set up connection pooling with PgBouncer to reduce PostgreSQL connection overhead. Learn the core concepts, step-by-step configuration, hands-on practice, and troubleshooting.
Focus: set up connection pooling with pgbouncer
Your PostgreSQL server is silently choking on connection overhead. Every new connection spawns a heavyweight backend process, consuming memory and CPU cycles. By default, PostgreSQL only allows 100 concurrent connections, and if you're running a modern Python application with async frameworks like FastAPI or Django, you'll hit that ceiling fast. The result: angry FATAL: sorry, too many clients already errors and degraded performance. But there's a battle-tested solution used by production teams everywhere: PgBouncer. This lightweight connection pooler sits between your app and PostgreSQL, reusing connections and keeping your database calm under load. By the end of this lesson, you'll know how to set up connection pooling with PgBouncer—from installation to production-ready configuration—and you'll have the skills to eliminate connection bottlenecks once and for all.
The problem this lesson solves
PostgreSQL's process-per-connection model is robust but expensive. Each new client connection forks a backend process that can consume tens of megabytes of RAM. When your application opens a new connection for every request—a common pattern in Python with psycopg2 or SQLAlchemy—you quickly exhaust resources. The default max_connections of 100 is a hard limit, and exceeding it produces FATAL: sorry, too many clients already.
Connection pooling solves this by reusing a small set of database connections across many client sessions. Instead of your app negotiating a new backend every time, it borrows a connection from a pool, uses it, and returns it. PgBouncer is the most popular pooler for PostgreSQL because it's lightweight (less than 1MB), fast, and supports multiple pooling modes. This lesson is your practical guide to setting it up correctly.
Core concept / mental model
Think of PgBouncer as a valet parking service for database connections. Without it, every driver (your app's request) has to find their own parking spot (connection) and pay the parking fee (connection overhead). With PgBouncer, a valet takes your car, parks it in a small, well-managed lot, and returns it exactly when you need it. The lot is your pool of persistent connections, and the valet is PgBouncer.
PgBouncer operates in two main modes:
- Session pooling: The pool assigns a PostgreSQL connection to a client for the entire client session. The connection is released only when the client disconnects. This matches PostgreSQL's native behavior but doesn't help reduce connections for short-lived sessions.
- Transaction pooling: The pool assigns a connection only for the duration of a single transaction. After the transaction commits or rolls back, the connection returns to the pool. This mode allows dozens or hundreds of clients to share a handful of connections, offering the biggest scalability win.
In this lesson, we'll focus on transaction pooling, which is the recommended default for most web applications.
Pro tip: While PgBouncer is a separate daemon, it's not a literal PostgreSQL proxy—it understands the PostgreSQL wire protocol and can even route based on the database name in the connection request.
How it works step by step
Setting up PgBouncer involves these logical steps:
- Install PgBouncer on the same host as your PostgreSQL server (or on a dedicated machine), using your package manager or official binaries.
- Configure the key files: the main config
pgbouncer.iniand the authentication fileuserlist.txt. - Define a pool size that matches your app's concurrency needs without overwhelming your database.
- Point your application at PgBouncer's port (usually 6432) instead of PostgreSQL's 5432.
- Start and verify: launch the daemon, check logs, and test with
psql.
Each step has its details and pitfalls, which we'll cover hands-on next.
Hands-on walkthrough
Let's set up PgBouncer from scratch. We'll use Ubuntu/Debian commands, but the process is similar on other systems.
Step 1: Install PgBouncer
sudo apt update
sudo apt install pgbouncer
For other platforms, you can compile from source or use Docker (see variations below).
Step 2: Edit the main configuration
The default config is at /etc/pgbouncer/pgbouncer.ini. Open it and adjust these key sections:
# /etc/pgbouncer/pgbouncer.ini
[databases]
myapp = host=127.0.0.1 port=5432 dbname=myapp
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 50
min_pool_size = 10
reserve_pool_size = 5
logfile = /var/log/pgbouncer/pgbouncer.log
pidfile = /var/run/pgbouncer/pgbouncer.pid
[databases]maps a database name that clients will use to the actual PostgreSQL backend. In this case, clients connecting tomyappwill be redirected to the local PostgreSQL on port 5432.listen_addrandlisten_portdefine where PgBouncer listens (port 6432 is the standard).auth_type = md5means usernames/passwords are checked against the auth file.pool_mode = transactionenables transaction pooling.
Step 3: Create the auth file
printf '"myapp" "secret"\n' > /etc/pgbouncer/userlist.txt
sudo chown postgres:postgres /etc/pgbouncer/userlist.txt
sudo chmod 600 /etc/pgbouncer/userlist.txt
The file contains plaintext passwords (or md5 hashes). For security, set tight file permissions.
Step 4: Start PgBouncer
sudo systemctl enable pgbouncer
sudo systemctl start pgbouncer
Step 5: Test with psql
psql -h 127.0.0.1 -p 6432 -U myapp -d myapp
You should see a normal PostgreSQL prompt. To verify pooling works, check PgBouncer's stats:
SHOW POOLS;
This command returns a table showing active clients vs server connections—proof that many clients share few actual connections.
Step 6: Point your Python app at PgBouncer
In Python with psycopg2, just change the port:
import psycopg2
conn = psycopg2.connect(
host="localhost",
port=6432,
dbname="myapp",
user="myapp",
password="secret"
)
Your connection now uses the pool. That's it!
Compare options / when to choose what
You have several connection pooling options. The table below compares PgBouncer with alternatives.
| Feature / Tool | PgBouncer | Pgpool-II | Application-level pooling (psycopg2 pool / SQLAlchemy) |
|---|---|---|---|
| Type | External daemon | External daemon | In-process library |
| Pooling modes | Session, transaction, statement | Session, transaction, load balancing | Custom (session/re-use) |
| Overhead | Very low (~1MB) | Higher (full proxy) | In-process, but each connection still hits PostgreSQL |
| Complexity | Simple ini config | More features (replication, failover) | Minimal config, but doesn't reduce backend connections if not used carefully |
| Best for | High connection counts, simple deployments | High availability, read scaling | Simple apps, low concurrency |
When to choose what:
- Choose PgBouncer when you need a lightweight, battle-tested pooler with minimal overhead and you're comfortable with a separate daemon.
- Choose Pgpool-II if you need advanced features like query load balancing or automatic failover.
- Choose application-level pooling (e.g.,
psycopg2.pool.ThreadedConnectionPool) only if you have a few hundred queries per second and can manage connections in code—but remember, the backend process limit still applies.
Variations: You can also run PgBouncer in a Docker container (official image), or use a cloud-managed pooler like Amazon RDS Proxy, which sits between you and RDS PostgreSQL.
Troubleshooting & edge cases
Even with a simple config, you'll hit issues. Here are the most common ones and how to fix them.
FATAL: sorry, too many clients already: This tells you themax_client_connis too low, ordefault_pool_sizeis too small. Increase them in the config and reload. But remember, the total number of server connections isdefault_pool_sizeplusreserve_pool_size, so keep it belowmax_connectionsin postgresql.conf.auth failederrors: The username/password inuserlist.txtdoesn't match what PostgreSQL expects, or theauth_typedoesn't match (e.g., you usescram-sha-256in PostgreSQL butmd5in PgBouncer). For Postgres 14+, switchauth_typetoscram-sha-256and generate the correct hash.- Transaction pooling breaks session-scoped features: If your app uses
SET search_pathor advisory locks, these don't work well with transaction pooling because the connection might change between transactions. Usesessionpooling for those apps, or restructure the app to set config per transaction usingSET LOCAL. - Prepared statements fail with "does not exist": Prepared statements are tied to a session. In transaction pooling, different clients may hit different backends. Use server-side prepared statements sparingly, or enable
prepared_statements_per_session = 0(which disables them). - Connections hang or timeout: Check
server_idle_timeoutandclient_idle_timeout—too low values can drop idle connections prematurely.
Pro tip: Use
SHOW STATS;andSHOW POOLS;to debug in real time. These give you a wealth of information about connection reuse and wait times.
What you learned & what's next
You've mastered the core idea of connection pooling and can now set up PgBouncer for your PostgreSQL database. You know how to install it, configure transaction pooling, point your Python app at it, and troubleshoot common pitfalls. You can explain the trade-offs between PgBouncer and alternatives like Pgpool-II or app-level pooling.
In the next lesson, we'll dive into B-tree index internals — understanding how PostgreSQL's default index structure performs and when to choose other index types. This knowledge complements your pooling setup by making your queries even faster, so you can handle more traffic with fewer resources.
Key takeaway: Connection pooling with PgBouncer is a low-lift, high-impact way to scale your PostgreSQL-backed Python applications. You're now ready to apply it in production.
Practice recap
Try this: install PgBouncer locally (or via Docker), configure it for a test database, and then run a Python script that opens 200 concurrent connections using threads. Verify with SHOW POOLS that only a handful of server connections are used. Change the pool mode to session and observe the difference.
Common mistakes
- Forgetting to update
auth_typeto match PostgreSQL's password encryption (e.g., usingmd5when PostgreSQL usesscram-sha-256), causing authentication failures. - Setting
default_pool_size+reserve_pool_sizehigher than PostgreSQL'smax_connections, leading to connection exhaustion. - Using transaction pooling with session-dependent features like
SET search_pathor advisory locks, causing unpredictable behavior. - Pointing your app to the wrong port (5432 instead of 6432) and bypassing the pool entirely.
- Not securing the
userlist.txtfile with proper permissions, exposing passwords.
Variations
- Use Docker to run PgBouncer:
docker run -d --name pgbouncer -v /path/to/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini -p 6432:6432 edoburu/pgbouncer - Use a cloud-managed pooler like Amazon RDS Proxy for managed PostgreSQL instances, which offers automatic failover and IAM authentication.
- Enable
statementpooling mode if you need even finer-grained connection distribution (though it's rarely needed).
Real-world use cases
- A Django REST API with hundreds of concurrent users hits PostgreSQL's default 100 connection limit; adding PgBouncer with transaction pooling allows 1000+ clients using only 50 backend connections.
- A FastAPI app with async SQLAlchemy opens dozens of connections per second; PgBouncer reduces startup latency by reusing warm connections, improving API response times.
- A batch-processing job that uses multiprocessing to parallelize queries would otherwise exhaust connections; PgBouncer pools connections across processes and prevents 'too many clients' errors.
Key takeaways
- PgBouncer is a lightweight external pooler that reduces PostgreSQL connection overhead and scales beyond
max_connections. - Transaction pooling is the recommended mode for web apps, reusing connections per transaction.
- Configuration involves the
pgbouncer.inifile and auserlist.txtauth file; test withpsqland monitor withSHOW POOLS;. - Always keep total pool sizes below PostgreSQL's
max_connectionsto avoid exhaustion. - Be aware of session-scoped features that break under transaction pooling; switch to session mode or adjust app code.
- Use the diagram-like mental model: PgBouncer as a valet for database connections.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.