Concurrency Scaling: PostgreSQL vs. MySQL
When PostgreSQL or MySQL serves many clients, its open-connection count does not show how much work it can handle. Some clients are…
When a service connects to PostgreSQL, the database opens a server process to handle that client’s queries. PostgreSQL’s max_connections setting limits how many clients can connect at once. In PostgreSQL 18, the documented default is typically 100, though system constraints can require a lower value. That ceiling does not tell you how many connections your database can handle efficiently: the useful number depends on the workload and the resources available.
PgBouncer is a connection pooler: services connect to it, and it shares a smaller set of PostgreSQL connections among those clients. It helps when many clients need connections but only some need to run database work at the same time. Measure your database under peak load, then consider PgBouncer if idle connections or bursts consume too much capacity, or if a smaller number of active database connections improves performance.
PostgreSQL accepts connections up to max_connections, but raising that setting does not guarantee better performance. The setting takes effect when the server starts, and PostgreSQL allocates more resources as the limit rises. PostgreSQL also reserves connection slots for privileged access: the documented defaults are three superuser-reserved slots and zero additional reserved slots. When ordinary connections fill all the slots that are not reserved, PostgreSQL refuses new ordinary connections. See the connection settings.
Each PostgreSQL client connection uses a backend process, a server process that handles that connection. An idle connection waits for a command but still uses a connection and a process. In AWS testing on an older RDS PostgreSQL instance with 2 vCPUs and 8 GB of memory, 100 idle connections used about 1,450 MB, or roughly 14.5 MB per connection on average. Across its test scenarios, AWS measured about 1.5 to 14.5 MB per connection. Those measurements describe that particular version and setup, not a fixed cost for every PostgreSQL server. The AWS idle-connection test shows why a large idle connection count deserves attention.
Active queries also affect memory use. PostgreSQL’s work_mem setting applies to individual query operations, such as sorting rows or building a hash table, a structure used to look up values. A query can use more than one such operation, and several sessions can run them at once, so many concurrent queries can multiply memory demand. Raising the connection limit without checking memory and workload can therefore increase pressure rather than capacity. PostgreSQL documents this behavior in its resource-consumption settings.
PgBouncer separates client connections from server connections. A client connection runs from an application to PgBouncer; a server connection runs from PgBouncer to PostgreSQL. PgBouncer can accept more clients than PostgreSQL has backend processes because it assigns its server connections to clients over time. That works when clients are not all using the database simultaneously. The PgBouncer feature documentation describes how the pooling modes handle that sharing.
In session pooling, PgBouncer keeps a PostgreSQL connection assigned to a client for the entire session. This preserves PostgreSQL session behavior, but an idle client continues to hold a server connection, so the mode offers little sharing when sessions stay open.
In transaction pooling, PgBouncer assigns a server connection for a transaction and returns it to the pool when the transaction ends. This lets more client sessions share fewer server connections. However, some session-level features do not work as clients expect in this mode, including session-level SET and RESET, LISTEN, SQL PREPARE and DEALLOCATE, and session-level advisory locks. Check the compatibility table before switching an application to transaction pooling.
In statement pooling, PgBouncer returns a server connection after each statement and disallows multi-statement transactions. Because it changes how clients can use transactions, it is the most restrictive mode.
PgBouncer limits client connections separately from server connections. Its max_client_conn setting limits incoming clients, while default_pool_size sets a default server-connection pool size per user/database pair. The total number of server connections therefore depends on how many such pools exist and whether settings override the default. A pooler also needs enough capacity for its clients and file descriptors, which are operating-system handles for open resources such as network connections. The documented 2 kB default memory requirement per PgBouncer connection does not include PostgreSQL backend memory or all deployment resources.
Consider PgBouncer when a service could open far more client connections than PostgreSQL can use to run queries at once. This can happen when many service processes each keep their own connection pool open. PgBouncer can also help when clients connect and disconnect often, many connections sit idle, or traffic spikes threaten to use all of PostgreSQL’s connection slots. Its configuration supports client limits and query-wait timeouts, so operators can set limits on how many clients connect and how long they wait for a query. See the PgBouncer configuration options.
A smaller pool of PostgreSQL connections can improve throughput for some workloads. Clients wait when all pooled connections are busy. In an AWS pgbench test on a 2-vCPU RDS instance, a PgBouncer pool capped at 20 PostgreSQL connections processed about 427 transactions per second, compared with about 378 transactions per second with a cap of 100. In the smaller-pool test, 80 of 100 client connections waited for a server connection. That result supports testing different pool sizes on your own workload; it does not make 20 a general recommendation. The AWS PgBouncer benchmark reports results for its specific test setup.
PgBouncer does not make PostgreSQL execute unlimited work at once. A transaction still needs a server connection while it runs. If nearly all clients keep transactions active, pooling has little opportunity to reuse connections. A smaller pool makes clients wait longer. It limits how many transactions run at once, which can protect PostgreSQL from too much concurrent work. A pooler leaves slow queries and lock contention (queries waiting for access to the same data) unresolved. It also cannot add CPU or storage capacity or reduce excessive work per transaction. Adding a pooler also adds another service whose availability and capacity need attention.
Start by checking how many PostgreSQL backends are active (running a query), idle, or idle in a transaction. An idle in transaction session has started a transaction but is not currently running a query, so it can hold resources and affect other work. PostgreSQL’s pg_stat_activity view provides one row per server process and reports connection state. A simple count is:
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state
ORDER BY count(*) DESC;
Filter by database, user, or application name when you need to isolate a source of connections. The PostgreSQL monitoring documentation describes pg_stat_activity.
Compare those counts with max_connections, accounting for reserved slots, and observe the database during peak traffic. Track CPU, memory, query latency, throughput, and connection errors. If PgBouncer is already in use, also track the number of waiting clients. Then test several server-pool sizes with representative traffic. A pool that is too large can preserve database contention. If it is too small, it can add queueing and latency. Choose a size based on measured throughput and latency, rather than copying a generic limit.
If you plan to use transaction pooling, first check whether the application relies on session state or any feature restricted by that mode. If the service already keeps a modest number of connections, PostgreSQL has spare capacity, and monitoring shows no connection pressure or frequent new connections and disconnections, PgBouncer may add complexity without solving a problem.
Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.
Take a look at vroni.com