Introduction
PostgreSQL is one of the most capable open-source databases in the world, but it has a well-known scaling quirk: every active client connection runs a dedicated backend process. Forking that process is cheap on paper, but every backend consumes memory for its catalog caches, per-connection state, and a number of shared resources. Multiply that by a few thousand application servers behind a load balancer, and the database grinds to a halt long before your CPU is saturated.
This is exactly the problem PgBouncer was built to solve. PgBouncer is a lightweight, single-binary connection pooler that sits between your application and PostgreSQL. Instead of opening a real PostgreSQL connection per client, your application connects to PgBouncer, and PgBouncer maintains a small, controlled pool of real PostgreSQL connections that are recycled across many application clients.
In this guide, you will learn how PgBouncer actually works under the hood, how to choose the right pool mode, how to install and configure it for a real production deployment, and how to operate it once it is live. We will cover transaction pooling, session pooling, statement pooling, prepared statements, monitoring, failover, and the most common mistakes engineers make when they first deploy PgBouncer.
This is not a "copy-paste this config and you're done" article. By the end, you should be able to reason about connection pooling the way a database engineer does, choose the right settings for your workload, and debug the issues that inevitably show up when pooling is introduced between an application and a database.
Table of Contents
- Introduction
- Core Concepts
- Architecture Overview
- Step-by-Step Guide
- Real-World Examples
- Production Code Examples
- Comparison Table
- Best Practices
- Common Mistakes
- Performance Tips
- Security Considerations
- Deployment Notes
- Debugging Tips
- FAQ
- Conclusion
Core Concepts
Before you deploy anything, you need a clear mental model of the moving pieces. Connection pooling is one of those topics where people wire things up first and only later realize they did not understand what they were configuring.
What Is a Database Connection
A PostgreSQL connection is a TCP socket over the PostgreSQL wire protocol, plus a backend process on the server. The server forks a backend when a client authenticates, and that backend lives for the lifetime of the connection. The backend holds state like session-level settings, prepared statements, temporary tables, advisory locks, and the active transaction.
This per-connection state is the reason naive horizontal scaling breaks. You cannot just raise max_connections to 10,000 and expect things to work, because every backend reserves memory, participates in cache invalidation, and competes for locks.
What Is Connection Pooling
Connection pooling is the practice of keeping a small set of long-lived database connections open and assigning them to clients on demand. From the application's point of view, it is still "opening a connection." But under the hood, the pooler is reusing real PostgreSQL backends instead of opening a new one for every client.
PgBouncer does this at the protocol level. It speaks the PostgreSQL wire protocol on both sides: it accepts client connections, pretends to be PostgreSQL, and forwards queries to real PostgreSQL backends. To PostgreSQL, PgBouncer looks like a regular client.
Pool Modes
PgBouncer supports three pool modes, and choosing the right one is the single most important configuration decision you will make.
- Session pooling: a client holds its backend for the entire duration of its connection, until the client disconnects. This is the safest mode because the application can rely on full session-level features: temporary tables,
SET,LISTEN/NOTIFY, advisory locks, and prepared statements. The trade-off is that pooling is weak: idle clients still occupy real backends. - Transaction pooling: a client holds its backend only for the duration of a transaction. When the transaction commits or rolls back, the backend is returned to the pool and can be handed to another client. This is the mode most production systems use because it provides real multiplexing while still being compatible with most application code.
- Statement pooling: a client holds its backend only for the duration of a single statement. This is the most aggressive pooling mode and is incompatible with multi-statement transactions. It is rarely used in production and mostly exists for very specific use cases like simple key-value workloads.
Prepared Statements and Pooling
Prepared statements are tied to a specific backend. In transaction pooling mode, when your application prepares a statement, that prepared statement is associated with the specific backend that handled the prepare call. If the next query lands on a different backend, the prepared statement will not be there, and PostgreSQL will return an error.
This is the most common reason teams hit errors the moment they turn on transaction pooling. The fix is either to disable server-side prepared statements in your driver, or to enable PgBouncer's max_prepared_statements feature, which makes prepared statements backend-agnostic at the cost of some performance.
Architecture Overview
A production PgBouncer deployment typically looks like this: your application servers connect to a PgBouncer endpoint, which lives either on the database host itself or on a small set of dedicated pooler hosts. PgBouncer maintains its own small pool of PostgreSQL connections, all routed to the same backend database server or, in more advanced setups, to a primary plus read replicas.
Single-Tier Pooling
The simplest architecture is a single PgBouncer instance fronting a single PostgreSQL server. This is fine for many deployments and dramatically reduces the number of backends PostgreSQL has to manage. The risk is that PgBouncer is now a single point of failure, so you should run at least two instances and load-balance between them.
Multi-Tier Pooling
Larger systems often run PgBouncer on each application host (or container sidecar) in addition to a central PgBouncer in front of PostgreSQL. The local sidecar pools connections within the application host, and the central PgBouncer pools connections to the database itself. This protects the database from connection storms during deploys and gives you more control over per-host behavior.
High Availability Topologies
For high availability, you typically deploy PgBouncer with a virtual IP or a DNS-based failover mechanism such as Consul, keepalived, or a cloud provider's load balancer. PgBouncer itself does not replicate state between instances, but because the underlying connection pool is small and ephemeral, this is rarely a problem. After a failover, clients reconnect, PgBouncer rebuilds its pool, and operations resume.
What PgBouncer Does Not Do
It is worth being clear about what PgBouncer is not. It is not a query-level load balancer, it is not a SQL firewall, it is not a read/write splitter, and it is not a replacement for proper connection management in your application. It is a connection multiplexer, period. The clearer you are about that boundary, the better your architecture will be.
Step-by-Step Guide
This section walks you through a real production setup on Ubuntu. The same steps work on any Linux distribution, with minor differences in package names.
Step 1: Install PgBouncer
On Ubuntu, you can install PgBouncer from the official apt repository. The version available in the default Ubuntu repository is usually recent enough for production work, but if you need the very latest version, use the PostgreSQL apt repository, which always ships a matching PgBouncer release.
sudo apt-get updatesudo apt-get install pgbouncerVerify the installation by checking the version and the binary path. PgBouncer is a single binary, so there is very little surface area to manage.
pgbouncer --versionStep 2: Create a PgBouncer User in PostgreSQL
PgBouncer does not require a special user, but it is good practice to give it its own role that is restricted to the databases it will front. Create the role and grant only the access it needs.
CREATE USER pgbouncer WITH PASSWORD 'strong_password_here';GRANT CONNECT ON DATABASE app_production TO pgbouncer;Step 3: Write the Configuration
The configuration lives in /etc/pgbouncer/pgbouncer.ini. The structure of the file is divided into sections. The [databases] section defines which databases clients can connect to, and the [pgbouncer] section controls the global behavior.
[databases]app_production = host=127.0.0.1 port=5432 dbname=app_productionapp_replica = host=127.0.0.1 port=5433 dbname=app_production[pgbouncer]listen_addr = 0.0.0.0listen_port = 6432auth_type = scram-sha-256auth_file = /etc/pgbouncer/userlist.txtadmin_users = pgbouncer_adminstats_users = pgbouncer_statspool_mode = transactionmax_client_conn = 4000default_pool_size = 20min_pool_size = 5reserve_pool_size = 5reserve_pool_timeout = 3max_db_connections = 100max_user_connections = 100server_idle_timeout = 600query_wait_timeout = 30query_timeout = 0client_idle_timeout = 0application_name_add_host = 1Step 4: Create the Auth File
The userlist.txt file lists the users that can connect to PgBouncer. The passwords are stored as SCRAM-SHA-256 hashes, the same hash format PostgreSQL uses internally.
psql -U postgres -c "SELECT rolname, rolpassword FROM pg_authid WHERE rolname='app_user';"Copy the stored SCRAM hash and paste it into /etc/pgbouncer/userlist.txt.
"app_user" "SCRAM-SHA-256$4096:salt$storedhash:anotherhash""pgbouncer_admin" "SCRAM-SHA-256$4096:salt$storedhash:anotherhash"Lock the file down so only PgBouncer can read it. This file is a credential file and should never be world-readable.
sudo chown pgbouncer:pgbouncer /etc/pgbouncer/userlist.txtsudo chmod 0600 /etc/pgbouncer/userlist.txtStep 5: Start and Enable PgBouncer
Enable PgBouncer to start on boot and start the service.
sudo systemctl enable pgbouncersudo systemctl start pgbouncersudo systemctl status pgbouncerStep 6: Test the Connection
Connect to PgBouncer using the same client tools you would use for PostgreSQL. The only difference is the port and, optionally, the host.
psql -h 127.0.0.1 -p 6432 -U app_user -d app_productionIf everything is wired up correctly, you will see the standard PostgreSQL prompt. From here, you can run SHOW POOLS; to inspect the pool state and SHOW STATS; to see traffic counters.
Step 7: Point Your Application at PgBouncer
Update your application connection string to point at the PgBouncer host and port. Most drivers will work without code changes, but you may need to disable server-side prepared statements, which we cover later in this guide.
postgresql://app_user:password@pgbouncer.internal:6432/app_productionReal-World Examples
Connection pooling is one of those things that looks the same in every tutorial but behaves very differently depending on the application. Here are three real-world scenarios and how you would configure PgBouncer for each.
Example 1: A Rails Application with 50 App Servers
A typical Rails app uses a connection pool per process, often configured to 5 or 10 connections. With 50 app servers and 4 Puma workers each, that is up to 2,000 application-side connections. PostgreSQL would happily accept them, but it would not be happy about the resulting memory pressure. A central PgBouncer with default_pool_size = 50 and transaction pooling reduces the actual PostgreSQL backend count to 50, which the database can handle comfortably.
Example 2: A Serverless Application on AWS Lambda
Serverless functions are the worst-case scenario for naive connection management: every cold start opens a new connection, and the database sees massive connection spikes during traffic bursts. The standard solution is a sidecar container running PgBouncer in the same Lambda execution environment, or a centralized PgBouncer in a VPC. Transaction pooling with a generous max_client_conn and a modest default_pool_size is the typical configuration.
Example 3: A Microservices Architecture Sharing One Database
When many services share a single database, each service often maintains its own connection pool. Without a global pooler, the sum of those pools can easily exceed the database's capacity. A central PgBouncer with separate pool_size settings per service (configured in the [databases] section) gives you a single place to enforce per-service quotas and protect the database from runaway services.
Production Code Examples
These snippets are drawn from real production deployments. They assume you already have PgBouncer installed and a PostgreSQL database available.
Nginx Stream Load Balancing Two PgBouncers
If you have two PgBouncer instances, you can put a TCP load balancer in front of them. Nginx's stream module is the simplest option and avoids the overhead of an HTTP-aware proxy.
stream { upstream pgbouncer_backend { server 10.0.0.10:6432 max_fails=2 fail_timeout=10s; server 10.0.0.11:6432 max_fails=2 fail_timeout=10s; } server { listen 6432; proxy_pass pgbouncer_backend; proxy_connect_timeout 5s; proxy_timeout 60s; }}PgBouncer Admin Commands
The PgBouncer admin console is one of the most useful tools you have. Connect to it with psql using the configured admin_users account and the special pgbouncer database.
psql -h 127.0.0.1 -p 6432 -U pgbouncer_admin pgbouncerFrom the console, you can inspect pool state, pause and resume the pooler, reload configuration, and kill specific connections.
SHOW POOLS;SHOW STATS;SHOW SERVERS;SHOW CLIENTS;RELOAD;PAUSE;RESUME;DISABLE app_production;ENABLE app_production;Node.js with Disabled Prepared Statements
If you are using node-postgres (pg) with transaction pooling, you need to disable server-side prepared statements or PgBouncer will return errors when queries land on a different backend.
const { Pool } = require('pg');const pool = new Pool({ host: process.env.PGHOST, port: 6432, database: process.env.PGDATABASE, user: process.env.PGUSER, password: process.env.PGPASSWORD, max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, // Disable server-side prepared statements for PgBouncer transaction pooling statement_timeout: false, query_timeout: false,});// Use the simple query protocolconst result = await pool.query('SELECT id, email FROM users WHERE id = $1', [42]);Recent versions of node-postgres support a pg_types option or a client_min_messages setting that can be set via connection options. For deterministic behavior, prefer the simple query protocol when you know you are behind PgBouncer.
Python with psycopg
Python's psycopg (v3) driver makes it easy to disable prepared statements by setting prepare_threshold to None on the connection.
import psycopgconn = psycopg.connect( host="pgbouncer.internal", port=6432, dbname="app_production", user="app_user", password="secret", prepare_threshold=None,)with conn.cursor() as cur: cur.execute("SELECT id, email FROM users WHERE id = %s", (42,)) row = cur.fetchone()Prometheus Monitoring Script
You can scrape PgBouncer metrics using the pgbouncer_exporter from the Prometheus community, or by querying the admin console from a small script and exposing the values via the Prometheus text format.
#!/bin/bashPOOLS=$(psql -h 127.0.0.1 -p 6432 -U pgbouncer_stats -d pgbouncer -t -A -c "SHOW POOLS;")echo "$POOLS" | awk 'NR>2 { printf("pgbouncer_pool_cl_active{database=\"%s\",user=\"%s\"} %s", $1, $2, $4); printf("pgbouncer_pool_cl_waiting{database=\"%s\",user=\"%s\"} %s", $1, $2, $5);}'Comparison Table
Choosing the right pool mode is the most important configuration decision you will make. The table below compares the three pool modes against the workload types where they fit best.
| Pool Mode | Backend Held For | Multiplexing | Compatible With | Best For |
|---|---|---|---|---|
| Session | Entire client connection | Low | Prepared statements, temporary tables, LISTEN/NOTIFY, advisory locks | Admin tools, long-lived scripts, BI tools |
| Transaction | Duration of a single transaction | High | Most application code; requires disabling server-side prepared statements | Web apps, microservices, serverless functions |
| Statement | Duration of a single statement | Maximum | Simple, single-statement queries only | Key-value workloads, read-only caching layers |
Best Practices
These are the practices that consistently show up in well-run production deployments. They are not controversial; they are simply the result of operating PgBouncer at scale.
- Start in transaction pooling mode unless you have a specific reason to use session pooling. Transaction pooling is the right default for nearly every application workload.
- Disable server-side prepared statements in your application drivers, or enable PgBouncer's
max_prepared_statementssetting if you need them. - Set
server_reset_queryexplicitly. The defaultDISCARD ALLis good, but for some workloads a lighter reset likeRESET ALL; SET SESSION AUTHORIZATION DEFAULT;is faster and equally safe. - Use SCRAM-SHA-256 authentication, not MD5. SCRAM is the modern default in PostgreSQL and avoids the well-known MD5 weaknesses.
- Lock down the auth file with
chmod 0600and run PgBouncer as a dedicated system user. - Run at least two PgBouncer instances in production, behind a load balancer or a virtual IP, so a single host failure does not take down your database access.
- Monitor
cl_waitingfromSHOW POOLS;. A non-zero value means clients are waiting for a connection from the pool, which usually means the pool is too small for the workload. - Set
application_name_add_host = 1so that PostgreSQL sees the originating application host inpg_stat_activity. This makes debugging much easier.
Common Mistakes
Most PgBouncer problems in production come from a small set of recurring mistakes. If you avoid these, you will avoid most of the outages and subtle bugs that show up when pooling is introduced.
Mistake 1: Leaving Prepared Statements On
The single most common failure mode is enabling transaction pooling and immediately seeing "prepared statement does not exist" errors. The fix is to disable server-side prepared statements in the driver or to set max_prepared_statements = 100 in PgBouncer (which adds internal tracking and a small performance cost).
Mistake 2: Using Session Pooling for Everything
Session pooling defeats most of the benefit of using a pooler. If you find yourself needing session pooling everywhere, the pooler is not really doing anything useful, and you would be better off tuning PostgreSQL's max_connections instead.
Mistake 3: Oversizing the Default Pool Size
A large pool is not always a fast pool. If default_pool_size is set higher than the database can comfortably handle, you trade one set of problems for another. The right pool size is roughly the number of CPU cores you have, multiplied by a small constant for disk-bound workloads. Always measure.
Mistake 4: Forgetting Server Reset Query
When a connection is returned to the pool, session-level state from the previous client can leak into the next one. This causes confusing bugs, like the wrong user being reported in current_user or the wrong search_path being applied. The server_reset_query setting exists to prevent exactly this.
Mistake 5: Pointing the Application at PostgreSQL Directly
Once PgBouncer is in place, the application should never connect to PostgreSQL directly again, except for very specific administrative reasons. A single misconfigured connection string in a config file bypasses the pooler and reintroduces the very problem PgBouncer was deployed to solve.
Performance Tips
PgBouncer's overhead is small but real, and there are a few configuration changes that meaningfully improve throughput in production.
- Tune
default_pool_size. Start at 1 connection per CPU core and adjust based oncl_waitingand query latency. For most OLTP workloads, 20 to 50 is a sensible range. - Enable
server_reset_query_always = 0(the default). This means the reset query only runs when a connection is returned to the pool, not on every checkout. The trade-off is that session state can leak if you do not use proper transaction boundaries. - Set
query_wait_timeoutto a reasonable value (e.g. 30 seconds) so that clients do not queue indefinitely when the pool is exhausted. - Use Unix sockets between PgBouncer and PostgreSQL when they run on the same host. The latency savings are small but consistent, and you eliminate one TCP round trip per query.
- Set
tcp_keepalive = 1so dead connections are detected and removed from the pool promptly, instead of waiting for a query to fail. - Watch
SHOW STATS;counters over time. A sudden drop intotal_xact_countortotal_query_countusually means something is wrong, either with PgBouncer or with the application.
Security Considerations
A connection pooler is on the data path between your application and your database, so security needs to be a first-class concern, not an afterthought.
Authentication
Always use SCRAM-SHA-256, never MD5 or trust. The auth_file is a credential file and must be readable only by the PgBouncer process. On most systems, that means chown pgbouncer:pgbouncer and chmod 0600.
Network Isolation
PgBouncer should be on a private network, not exposed to the public internet. The port PgBouncer listens on is just as sensitive as the PostgreSQL port itself, because anyone who can reach it can attempt to log in.
TLS
PostgreSQL supports TLS for both client and server connections. PgBouncer supports TLS on both sides: from the application client to PgBouncer, and from PgBouncer to PostgreSQL. In a production deployment, both should be enabled. client_tls_sslmode = require is a reasonable minimum; verify-full is the strict option if you have a private CA.
Admin Console Access
The admin console can pause, resume, and reload the pooler, and can also kill arbitrary connections. The admin_users list should contain only the few accounts that absolutely need this access, and the admin console should be bound to a separate listen address if possible, so it is not reachable from the application network.
Auditing
PgBouncer logs connections, disconnections, and authentication failures to syslog by default. Make sure these logs are forwarded to your central logging system and that alerts are set on repeated authentication failures, which often indicate a brute-force attempt.
Deployment Notes
How you deploy PgBouncer matters as much as how you configure it. A few notes from real production deployments.
Package vs. Binary
For most Linux distributions, the package version is fine. If you need a newer version (for example, to pick up a specific bug fix), the PostgreSQL apt and yum repositories always ship a matching PgBouncer release alongside the PostgreSQL server version.
Container Deployments
PgBouncer runs well in containers. The official PgBouncer Docker image is a good starting point, but in production you will typically want to bake your own image with your pgbouncer.ini and userlist.txt baked in. The container should run as a non-root user, and the auth file should be mounted as a secret rather than committed to the image.
Sidecar vs. Central
A sidecar (running in the same pod or VM as the application) is great for isolating connection storms. A central PgBouncer in front of PostgreSQL is great for enforcing global quotas and protecting the database. Many large deployments run both: a sidecar per app instance and a central pooler in front of the database.
Rolling Restarts
When you restart PgBouncer, all client connections drop. In a Kubernetes or similar environment, this is usually fine because the application retries, but you should still restart one instance at a time. For a central PgBouncer behind a load balancer, drain the instance from the load balancer first, restart it, and then put it back into rotation.
Debugging Tips
When things go wrong with PgBouncer, the symptoms are usually one of: clients waiting for connections, queries failing with "prepared statement does not exist," or unexpected authentication errors. Here is how to triage each.
Clients Are Waiting
Run SHOW POOLS; and look at cl_waiting. If it is non-zero, your pool is too small. Either raise default_pool_size or find out why your application is holding connections for too long. SHOW CLIENTS; shows you which clients are connected and for how long, which is usually enough to find the culprit.
Prepared Statement Errors
These almost always mean server-side prepared statements are enabled in the driver. Either disable them in the driver or set max_prepared_statements in PgBouncer. The official PgBouncer documentation has a list of which drivers and versions are affected.
Authentication Errors
Check the syslog for PgBouncer. The error message will tell you whether the failure is at the PgBouncer level (wrong password in userlist.txt) or at the PostgreSQL level (PgBouncer could not authenticate to PostgreSQL with the configured credentials). They look similar but have completely different fixes.
Slow Queries
Slow queries are almost always a PostgreSQL problem, not a PgBouncer problem, but PgBouncer's query_wait_timeout and query_timeout settings will tell you whether clients are giving up before the database responds. If clients are timing out while waiting in the pool, that is a clear sign the pool is undersized for the workload.
Pausing the Pooler
The PAUSE; command stops PgBouncer from accepting new client connections while continuing to serve existing ones. It is invaluable when you need to drain a PgBouncer instance during a deploy or when you want to take the database offline for maintenance. RESUME; brings it back into service.
FAQ
What is PgBouncer and why do I need it?
PgBouncer is a lightweight connection pooler for PostgreSQL. It reduces the number of real PostgreSQL backends by multiplexing many client connections onto a small pool. You need it when your application opens more database connections than PostgreSQL can comfortably handle, which is most production deployments beyond a small scale.
Which pool mode should I use?
Use transaction pooling for almost all application workloads. Use session pooling only when you need session-level features like temporary tables, LISTEN/NOTIFY, or server-side prepared statements. Use statement pooling only for very specific single-statement workloads.
Does PgBouncer support read replicas?
PgBouncer can route different databases to different hosts using the [databases] section, so you can define one entry for the primary and another for a read replica, and let the application choose. PgBouncer itself does not split reads and writes automatically; that is the application's job.
How do I disable prepared statements?
Most PostgreSQL drivers accept a configuration option to disable server-side prepared statements. In Node.js with node-postgres, use the simple query protocol or set statement_timeout = false and avoid named prepared statements. In Python with psycopg v3, set prepare_threshold=None on the connection. The exact option depends on the driver; consult the driver's documentation.
Can I run PgBouncer on the same host as PostgreSQL?
Yes, and it is the most common deployment. Running on the same host eliminates one network hop and lets you use a Unix socket between PgBouncer and PostgreSQL. Just make sure you size the host's resources so that PostgreSQL's memory plus PgBouncer's memory plus the application memory do not exceed the host's capacity.
How does PgBouncer handle failover?
PgBouncer does not handle PostgreSQL failover itself. When the database fails over, PgBouncer's existing connections break, and PgBouncer reconnects to the new primary. During the reconnection window, clients see errors and should retry. For zero-downtime failover, pair PgBouncer with a PostgreSQL HA solution like Patroni, and consider running multiple PgBouncer instances behind a load balancer.
Is PgBouncer a load balancer?
No. PgBouncer is a connection multiplexer, not a query router. It does not decide which backend gets a query; it only decides which physical PostgreSQL connection a client uses. For read/write splitting or query routing, you need a different tool, such as pgpool-II or a smart client driver.
How do I monitor PgBouncer?
The most common approach is to run SHOW POOLS; and SHOW STATS; from the admin console on a regular schedule, expose the values via a metrics endpoint, and scrape them with Prometheus. The community-maintained pgbouncer_exporter does exactly this and is widely used in production.
What is the difference between PgBouncer and pgpool-II?
PgBouncer is a pure connection pooler: small, single-purpose, and very fast. pgpool-II is a much larger project that adds query-level load balancing, replication, and connection pooling. If you only need connection pooling, PgBouncer is the right choice because it is simpler and more battle-tested. If you need query-level load balancing or native replication features, pgpool-II is worth considering.
Can PgBouncer handle 10,000 clients?
Yes, with the right configuration. max_client_conn controls the maximum number of client connections PgBouncer will accept, and the default is 100. The recommended upper limit for a single PgBouncer instance is around 5,000 to 10,000 clients, depending on the host's resources. For larger deployments, run multiple instances behind a load balancer.
Conclusion
PgBouncer is one of the most boring, most reliable pieces of infrastructure you will ever deploy, and that is exactly the point. It does one thing, and it does it well: it reduces the number of real PostgreSQL backends your database has to manage, and it does so without changing the wire protocol your application already speaks.
If you take one thing away from this guide, let it be this: choose the right pool mode, disable server-side prepared statements when you are using transaction pooling, run at least two instances for redundancy, and monitor cl_waiting and sv_active from SHOW POOLS;. Those four practices will prevent the vast majority of PgBouncer-related incidents.
Connection pooling is not glamorous, but it is the difference between a PostgreSQL deployment that scales gracefully and one that falls over at the worst possible time. Set it up properly now, and your database will thank you later.
If you want to go deeper, start with the PgBouncer configuration documentation and the PostgreSQL connection management guide. Both are excellent, and both reward careful reading. And if you want a broader look at PostgreSQL performance beyond pooling, my PostgreSQL performance tuning checklist is a good next step.