Connection Pooling: HikariCP, pgBouncer, and ProxySQL

Learn connection pool sizing, HikariCP, pgBouncer, and ProxySQL, timeout settings, idle management, and when pooling helps or hurts performance.

published: reading time: 25 min read author: GeekWorkBench

Connection Pooling: HikariCP, pgBouncer, and ProxySQL

Database connections are not free. That TCP handshake, the authentication round-trip, the session initialization — add them together and you’re looking at 20-50ms before the first query runs. Connection pooling keeps connections warm and ready, so requests can borrow one without going through setup again.

The trouble is that pooling has its own pitfalls. Get the pool size wrong, mismanage timeouts, or skip health checks and you’ll wish you had not bothered. A poorly tuned pool can make things worse instead of better.

Why Connection Pooling Matters

Without pooling, every request opens a new connection and closes it when done. Under load, you spend more time opening and closing connections than running queries.

Request 1: Open (30ms) → Query (5ms) → Close (5ms) = 40ms
Request 2: Open (30ms) → Query (5ms) → Close (5ms) = 40ms
Request 3: Open (30ms) → Query (5ms) → Close (5ms) = 40ms

With pooling, requests reuse existing connections:

Request 1: Get (0.1ms) → Query (5ms) → Return (0.1ms) = 5.2ms
Request 2: Get (0.1ms) → Query (5ms) → Return (0.1ms) = 5.2ms
Request 3: Get (0.1ms) → Query (5ms) → Return (0.1ms) = 5.2ms

The connection is already open. You save the setup time on every request.

How Connections Flow Through a Pool

Applications acquire a connection from the pool, run their query, then return it. With pgBouncer in transaction mode, connections are only borrowed for the duration of each transaction — this is how a single pool serves hundreds of application instances without exhausting max_connections.

Connection Pool Sizing

Pool size is a balance: too small wastes throughput, too large wastes resources and can overwhelm the database.

The Formula

A common starting point from PostgreSQL docs:

pool_size = (number_of_cores * 2) + effective_spindle_count

For SSDs (no spindle contention), this simplifies to:

pool_size = (number_of_cores * 2) + 0 = 2 * cores

Factors That Affect Pool Size

CPU-bound queries need smaller pools. I/O-bound queries can use larger pools. More clients need larger pools. Each connection uses memory, so the pool can’t exceed what the database can handle. High network latency favors larger pools to keep pipes full.

Real-World Example

For a 4-core database server with an SSD:

pool_size = (4 * 2) + 0 = 8 connections

But if you have 100 concurrent clients, you’ll need to queue requests or increase the pool — some contention is inevitable.

HikariCP Configuration

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://db-server:5432/mydb");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30000);  // 30 seconds
config.setIdleTimeout(600000);       // 10 minutes
config.setMaxLifetime(1800000);      // 30 minutes
config.setPoolName("myapp-pool");

HikariDataSource ds = new HikariDataSource(config);

Key HikariCP Settings

  • maximumPoolSize — maximum connections in the pool. Set based on the formula above.
  • minimumIdle — minimum connections to keep idle. Set lower than maximumPoolSize for variable load.
  • connectionTimeout — how long to wait for a connection before throwing an exception.
  • idleTimeout — how long to keep an idle connection before closing it.
  • maxLifetime — maximum lifetime of a connection, regardless of idle.

HikariCP Deep Dive

HikariCP is the de facto standard for Java connection pooling. It’s known for minimal overhead and fast performance.

Why HikariCP Is Fast

HikariCP uses several techniques. It instruments bytecode via Javassist to generate optimized connection handling. It minimizes network frames to keep them small. It aggressively prunes dead connections. And it uses concurrent skip lists for lock-free data structures.

config.setMaximumPoolSize(10);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
config.setLeakDetectionThreshold(60000);  // Detect leaks after 60 seconds

Monitoring HikariCP

// Get pool metrics
HikariPoolMXBean pool = ds.getHikariPoolMXBean();
int activeConnections = pool.getActiveConnections();
int idleConnections = pool.getIdleConnections();
int totalConnections = pool.getTotalConnections();
int threadsAwaitingConnection = pool.getThreadsAwaitingConnection();

pgBouncer

pgBouncer is a connection pooler for PostgreSQL. Unlike application-level pools, pgBouncer sits between the application and the database as a proxy.

Why pgBouncer?

pgBouncer provides database-level pooling as a single pool for all applications connecting to a database. It supports transaction pooling mode where connections are only held during transactions, not sessions. It caches authentication to reduce overhead. And it’s lightweight, written in C with minimal resource usage.

Installation

# Ubuntu/Debian
apt-get install pgbouncer

# Or from source
./configure --prefix=/usr/local && make && make install

Basic Configuration

[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction  # or session
max_client_conn = 100
default_pool_size = 20

Pool Modes

pgBouncer supports three pool modes:

# Session mode: connection is returned to pool when client disconnects
pool_mode = session

# Transaction mode: connection is returned after transaction commits
# This allows more clients than connections
pool_mode = transaction

# Statement mode: connection is returned after each statement
# Not compatible with prepared statements or multi-statement transactions
pool_mode = statement

Transaction mode is the most efficient but has limitations:

  • No session-level features (SET LOCAL, prepared statements across transactions)
  • Transactions must be explicit (no BEGIN blocks for multi-statement)

PgBouncer with HikariCP

Use both together for layered pooling:

Application (HikariCP pool=10) → pgBouncer (pool=20) → PostgreSQL (max_connections=100)

pgBouncer handles connections to the database; HikariCP handles connections to pgBouncer.

ProxySQL

ProxySQL is a more sophisticated proxy that handles both MySQL and PostgreSQL with advanced features.

Why ProxySQL?

ProxySQL supports MySQL, PostgreSQL, and MariaDB. It can route queries to replicas or primaries based on query type. It has built-in query caching and throttling. It also supports traffic mirroring for test systems.

ProxySQL Configuration

-- Add MySQL servers
INSERT INTO mysql_servers (hostname, port, weight, comment) VALUES ('db-primary', 3306, 100, 'Primary');
INSERT INTO mysql_servers (hostname, port, weight, comment) VALUES ('db-replica', 3306, 100, 'Replica');

-- Create monitoring user on MySQL
CREATE USER 'monitor'@'%' IDENTIFIED BY 'monitor_password';
GRANT REPLICATION CLIENT ON *.* TO 'monitor'@'%';

-- Configure user
INSERT INTO mysql_users (username, password, active, default_hostgroup) VALUES ('app_user', 'app_password', 1, 0);

-- Load configuration
LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL USERS TO RUNTIME;

Query Routing with ProxySQL

-- Route reads to replica, writes to primary
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (1, 1, '^SELECT.*', 1, 1);  -- Reads go to hostgroup 1 (replicas)

INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (2, 1, '^INSERT|^UPDATE|^DELETE', 0, 1);  -- Writes go to hostgroup 0 (primary)

Connection Timeout Settings

Poor timeout configuration causes problems. Set them deliberately.

Application Timeouts

// HikariCP
config.setConnectionTimeout(30000);  // Wait 30s for connection
config.setValidationTimeout(5000);  // 5s to validate connection

Database-Level Timeouts

-- PostgreSQL: statement timeout (8 seconds)
SET statement_timeout = '8s';

-- MySQL: wait timeout
SET GLOBAL wait_timeout = 28800;

Load Balancer Timeouts

# HAProxy
timeout client          30s
timeout server          30s
timeout connect          5s
timeout queue           60s

Idle Connection Management

Idle connections waste resources. Configure pools to close unused connections.

HikariCP Idle Timeout

config.setMinimumIdle(2);     // Keep at least 2 idle
config.setIdleTimeout(600000); // Close idle after 10 minutes

PgBouncer Idle Management

server_idle_timeout = 600  # seconds

When to Keep Connections Alive

Keep connections alive for high-frequency queries (keep warm), when connection setup is expensive (SSL, authentication), and when the database is on a high-latency network.

When to Close Idle Connections

Close idle connections when many databases share a server, when each connection consumes significant memory, and when queries are infrequent.

When Connection Pooling Helps

Pooling helps when request volume is high (many short requests benefit most from connection reuse), when authentication is expensive (connection setup takes 20ms+), when applications are latency-sensitive, and when the database max_connections is low.

When Connection Pooling Hurts

Pooling can hurt when queries run for minutes (long-lived connections see little benefit), when prepared statements are used across connections, when applications rely on session state (SET variables, temp tables) since transaction pooling breaks this, and when pools are oversized (too many connections cause context switching and memory pressure).

Transaction Pooling Gotcha

With pgBouncer in transaction mode:

-- This breaks: SET LOCAL only lasts for the transaction
BEGIN;
SET LOCAL app.setting = 'value';  -- Connection returned to pool after COMMIT, setting lost
COMMIT;
-- This also breaks: prepared statements are connection-specific
PREPARE myplan AS SELECT * FROM orders WHERE id = $1;
-- Different connection after COMMIT, prepared statement gone

Connection Pool Comparison

FeatureHikariCPpgBouncerProxySQL
LayerApplication-levelDatabase proxyDatabase proxy
LanguageJavaCC++
Transaction poolingN/A (app-level)YesYes
Query routingNoNoYes (read/write split)
Connection multiplexingNoYesYes
MySQL supportYesNoYes
PostgreSQL supportYesYesYes
Set up complexityLowMediumHigh
Memory footprintPer-appSingle processSingle process
Best forJava apps, per-instance poolingPostgreSQL at scaleMulti-database routing, read replicas

Monitoring Connection Pools

HikariCP Metrics

// Micrometer metrics (Spring Boot)
hikaripool.mysql = { ... }
metrics:
  - hikaricp.connections.active
  - hikaricp.connections.idle
  - hikaricp.connections.pending
  - hikaricp.connections.max
  - hikaricp.connections.min

pgBouncer Monitoring

# Show pools
psql -h localhost -p 6432 -U pgbouncer -c 'SHOW POOLS;'

# Show clients
psql -h localhost -p 6432 -U pgbouncer -c 'SHOW CLIENTS;'

# Show servers
psql -h localhost -p 6432 -U pgbouncer -c 'SHOW SERVERS;'

# Show usage
psql -h localhost -p 6432 -U pgbouncer -c 'SHOW STATS;'

Common Production Failures

Pool exhaustion causing timeouts: You set maximumPoolSize too low for your concurrency. Requests start queuing up, connectionTimeout fires, and your API starts returning 503s. Under load, this cascades. Monitor threadsAwaitingConnection in HikariCP or SHOW POOLS in pgBouncer — if it is consistently above zero, your pool is too small.

Leaked connections not detected: An application bug holds a connection open without returning it to the pool. maximumPoolSize connections leak, subsequent requests block, and eventually the pool is exhausted. HikariCP’s leakDetectionThreshold catches this — set it to something shorter than your p99 query time so leaks are detected before the pool starves.

pgBouncer transaction mode breaking session state: You deploy pgBouncer in transaction mode but your application uses SET LOCAL, temporary tables, or prepared statements that persist across statements. SET LOCAL values vanish after commit because the connection is returned to the pool. Either switch pgBouncer to session mode or audit your application for session-level features.

Pool oversized for database max_connections: You set HikariCP maximumPoolSize = 100 on 50 application instances connecting to PostgreSQL with max_connections = 100. The math fails — 50 x 100 = 5,000 required backend connections. Either use pgBouncer in front of PostgreSQL, or ensure maximumPoolSize * instances <= max_connections.

Idle connections exceeding database limits: Your application runs on a serverless platform that scales instances to zero between requests. Each cold start opens connections up to maximumPoolSize, and with many instances, you briefly exceed max_connections. Set minimumIdle = 0 in HikariCP for serverless workloads and let connections be created on demand.

Prepared statements not working across pgBouncer: You use prepared statements in PostgreSQL with pgBouncer in transaction mode. Prepared statements are connection-scoped, but pgBouncer returns your connection to the pool after each commit. Your next transaction gets a different connection and the prepared statement is gone. Either use session mode or execute statements without preparation.

Wrong pool mode causing connection pressure: You run pgBouncer in session mode when you should be in transaction mode. Each client holds a backend connection for the entire session, which means you can support fewer concurrent clients than max_connections allows. For high-concurrency OLTP, transaction mode is almost always the right choice.

Capacity Estimation: Pool Size Math

The pool size formula from PostgreSQL documentation: connections = (core_count * 2) + effective_spindle_count. For SSDs, effective_spindle_count is effectively 0, so the formula simplifies to 2 * cores. For a database server with 16 cores and spinning disks, that is roughly 32 backend connections per pool.

But that is only the starting point for a single application. When you have N application instances, the constraint becomes total_connections = pool_size * N. PostgreSQL’s max_connections is a hard ceiling. If you deploy 10 application instances each with pool_size=50, you need 500 backend connections. PostgreSQL default is 100. At that scale, you need pgBouncer in transaction mode between your applications and PostgreSQL — pgBouncer multiplexes hundreds of application connections onto a small number of backend connections.

Memory consumption per connection: a PostgreSQL backend typically uses 5-10 MB of memory at idle and can grow with complex queries. A pool of 50 connections can consume 250-500 MB of PostgreSQL server memory just for idle backends. At 500 connections, you are looking at 2.5-5 GB of reserved memory that cannot be used for shared_buffers or query execution. This is why oversized pools are a memory problem, not just a contention problem.

Real-World Case Study: PgBouncer at Stripe

Stripe runs one of the largest PostgreSQL deployments in the production software world, processing millions of transactions per day. Their database team has written extensively about their connection pooling architecture. The problem they faced: thousands of application servers, each running multiple worker processes, all connecting to PostgreSQL primaries and replicas. The connection count math was brutal — without pooling, they would have needed tens of thousands of backend connections.

Their solution was pgBouncer in transaction mode, deployed as a sidecar process on each application host. Each application process connects to its local pgBouncer, which multiplexes those connections down to a small number of actual PostgreSQL connections. This let them run thousands of application instances with predictable connection counts.

Key numbers from their setup: they typically run default_pool_size = 10 to 20 per pgbouncer instance, which feeds into a much smaller set of actual PostgreSQL connections. At their scale, PostgreSQL max_connections is tuned carefully and monitored aggressively — going over means immediate connection failures for payment processing.

The lesson: the formula 2 * cores applies when a single application is your only client. As soon as you have multiple application instances, pgBouncer becomes a multiplier for connection efficiency, not just a connection multiplexer. Without it, you either exhaust max_connections or you under-deploy application instances and leave throughput on the table.

Quick Recap Checklist

  • Connection pooling eliminates TCP handshake + auth overhead on every request
  • Pool size formula: 2 × cores for SSDs; (2 × cores) + effective_spindle_count for spinning disks
  • HikariCP: set maximumPoolSize, minimumIdle, connectionTimeout, idleTimeout, maxLifetime
  • PgBouncer in transaction mode: connection returned after each commit, not held for session
  • Transaction mode breaks: SET LOCAL, prepared statements, advisory locks, temp tables
  • pool_size × instances must stay below max_connections
  • Use pgBouncer to break the N × pool_size dependency on max_connections
  • ProxySQL: read/write split via query rules to hostgroups
  • HikariCP leak detection: set leakDetectionThreshold shorter than p99 query time
  • minimumIdle=0 for serverless workloads; connections created on demand

Interview Questions

1. Your application is a Node.js server handling 500 concurrent requests. Each request needs a database connection. Your PostgreSQL server has 8 CPU cores and max_connections = 100. How do you approach connection pooling?

With 500 concurrent requests and only 100 available connections, you cannot give each request its own connection. The solution is pooling, but a pool of 100 in each of N application instances still needs N × 100 connections total. The right architecture is application-level pooling (a small pool per application instance, say 10-20 connections) feeding into pgBouncer in transaction mode, which multiplexes onto the 100 PostgreSQL backends. Node.js is single-threaded but async, so a small pool handles the concurrency efficiently. The key constraint is pool_size × application_instances <= max_connections, and pgBouncer breaks that dependency.

2. A service starts throwing connection timeout errors after running fine for hours. You check and find that SHOW POOLS in pgBouncer shows zero available connections. What is happening?

Pool exhaustion typically means either the pool was sized too small for the actual concurrency, or connections are leaking (not being returned to the pool). HikariCP exposes threadsAwaitingConnection as a metric — if that is climbing, requests are queueing up faster than connections free up. In pgBouncer, SHOW STATS shows num_waited and num_timeout — non-zero values mean clients are waiting and timing out. Check for long-running transactions holding connections, connection leaks in the application, or a sudden traffic spike the pool was not designed for. The fix is either increase pool size (if the database can handle it), reduce transaction duration, or add retry logic for pool exhaustion errors.

3. You switch pgBouncer from session mode to transaction mode to handle more concurrent users. What breaks?

Transaction mode is the most efficient pool mode but it breaks anything that relies on session state. SET LOCAL values vanish when the transaction commits and the connection returns to the pool. Temporary tables are dropped when the session ends — with transaction pooling, that happens after every commit. Prepared statements are connection-scoped, so they disappear when you get a new connection after commit. PostgreSQL advisory locks are also connection-scoped and do not survive pooling. Audit your application for SET, SET LOCAL, PREPARE, advisory locks, and ON COMMIT actions before switching to transaction mode.

4. How do you decide between HikariCP, pgBouncer, and ProxySQL for a new application?

HikariCP is embedded in your application process — it pools connections per JVM instance. Use it when you are on the JVM and want minimal overhead for connection reuse within each application instance. PgBouncer sits between your application and the database — it pools at the database level. Use it when you have many application instances or processes and need to multiplex them onto fewer database connections. ProxySQL is a full database proxy with query routing, caching, and traffic shaping. Use it when you need read/write splitting across replicas, query caching, or multi-database routing. Most production PostgreSQL deployments end up using both HikariCP (per-instance) and pgBouncer (server-level) together.

5. Your HikariCP pool has maximumPoolSize=50, but under load you see connections timing out. When you check pg_stat_activity, PostgreSQL shows only 30 active connections. Why is HikariCP timing out when PostgreSQL appears not to be saturated?

pgBouncer in transaction mode between HikariCP and PostgreSQL is likely consuming connections differently than expected. If you have pgBouncer with default_pool_size=20 and HikariCP with maximumPoolSize=50 on 10 application instances, those 10 HikariCP pools can demand 500 connections from pgBouncer, but pgBouncer only maintains 20 connections to PostgreSQL. The extra demand queues in HikariCP and times out. Also check whether connections are being returned to the pool promptly — a connection leak (not returned after use) reduces effective pool availability. Enable HikariCP leak detection with leakDetectionThreshold and check threadsAwaitingConnection metric.

6. You deploy a new application instance and immediately see connection errors from all existing application instances. max_connections is not exceeded. What happened?

A new application instance started with a pool size larger than expected, or started multiple worker processes each with their own pool. If the new instance opens connections without waiting for existing instances to release theirs, it can cause a thundering herd where all pool connections are grabbed before any are returned. This happens when startup behavior opens connections eagerly without respecting pool size limits or when connection initialization is asynchronous and not properly gated. Check the pool configuration of the new deployment — specifically whether minimumIdle is too high or whether connection opening is deferred correctly.

7. HikariCP shows high threadsAwaitingConnection and you decide to increase maximumPoolSize. What is the main risk of doing this?

Each connection to PostgreSQL consumes 5-10 MB of memory even when idle. If you increase maximumPoolSize from 10 to 50 on 20 application instances, PostgreSQL needs 20 x 50 x 5MB = 5GB just for idle backends, which cannot be used for shared_buffers or query execution. Oversized pools cause memory pressure on the database server. Instead of increasing pool size, consider adding pgBouncer in transaction mode to multiplex a smaller number of actual connections. Alternatively, reduce pool size and add retry logic for connection exhaustion — a smaller pool with retry handles temporary load spikes better than a large pool that exhausts database memory.

8. PgBouncer in transaction mode causes intermittent failures for a specific feature. The feature uses advisory locks. What is the root cause?

Advisory locks are session-scoped in PostgreSQL — they persist until the session ends. With pgBouncer in transaction mode, your connection is returned to the pool after every transaction. The advisory lock acquired during Transaction 1 is held until the session ends, but the session ends when the connection returns to the pool — the lock is silently dropped. But worse, when you acquire the lock in Transaction 2 on a different physical connection, the advisory lock state is not there, so the application thinks it has the lock when it does not. Use session mode in pgBouncer for features that rely on advisory locks, or implement advisory locking at the application level using a dedicated table with FOR UPDATE.

9. A serverless function cold-starts and opens HikariCP connections to PostgreSQL. Concurrent cold-starts of 100 instances simultaneously exhaust max_connections. How do you prevent this?

Set minimumIdle=0 in HikariCP — serverless workloads should not maintain idle connections since instances scale to zero between invocations. Each cold start should create connections on demand, not pre-warm. Additionally, set maxLifetime to something shorter than the database's connection idle timeout (PostgreSQL default is 8 hours, but set it to 30 minutes to force connection recycling). If you have many concurrent cold starts, use a connection pooler like pgBouncer as a buffer between the serverless instances and PostgreSQL — pgBouncer accepts connections from the serverless instances and multiplexes them onto a small number of actual PostgreSQL connections.

10. You have 3 PostgreSQL replicas for read scaling and one primary for writes. How do you route reads to replicas and writes to the primary using ProxySQL?

Configure ProxySQL with the primary in hostgroup 0 and replicas in hostgroup 1. Create query rules: INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply) VALUES (1, 1, '^SELECT', 1, 1) to route SELECT to hostgroup 1 (replicas), and VALUES (2, 1, '^INSERT|^UPDATE|^DELETE', 0, 1) to route writes to hostgroup 0 (primary). Use read_only=1 on replicas so ProxySQL can detect which servers are replicas. Note that this requires application-level separation of read and write queries — ProxySQL routes by query pattern, not by transaction semantics. For transaction-aware routing (read after write consistency), use a connector that supports proxy-aware transactions.

Further Reading

11. You deploy HikariCP with maximumPoolSize=50 and minimumIdle=50. Under what conditions would minimumIdle waste resources?

minimumIdle keeps connections warm even during low-traffic periods. With minimumIdle=50 on an application that handles 5 concurrent requests on average, you are maintaining 50 idle connections consuming 5-10 MB each of PostgreSQL server memory (250-500 MB total) for no benefit. Set minimumIdle to a lower value (2-5) and let HikariCP scale up to maximumPoolSize under load. minimumIdle is most useful for applications with consistent moderate traffic where connection establishment latency is noticeable.

12. What is the relationship between HikariCP connection timeout and PostgreSQL statement_timeout? When would you set them differently?

HikariCP's connectionTimeout is how long a request waits for a connection from the pool. PostgreSQL's statement_timeout is how long a query runs before being cancelled. Set connectionTimeout longer than statement_timeout — if a query takes 30 seconds, you do not want the connection to be returned to the pool before the query finishes. connectionTimeout is application-level waiting for a pool connection; statement_timeout is database-level query execution limit. They serve different purposes and should be tuned independently.

13. How does pgBouncer's max_client_conn setting protect PostgreSQL from being overwhelmed?

max_client_conn is the maximum number of client connections pgBouncer will accept. Even if 1000 application instances each open 100 connections to pgBouncer, if max_client_conn=500, pgBouncer accepts only 500 and the rest wait. This prevents pgBouncer itself from being overwhelmed. However, max_client_conn does not limit the number of backend connections to PostgreSQL — that is controlled by default_pool_size and the pool_mode. Use both: max_client_conn to protect pgBouncer, default_pool_size to protect PostgreSQL.

14. What happens when HikariCP's leakDetectionThreshold is set too high?

leakDetectionThreshold is how long a connection can be checked out before HikariCP considers it a leak. If set too high (e.g., 5 minutes), connections that are genuinely slow but not leaked will not be flagged. A true connection leak (connection never returned) will hold the pool hostage for leakDetectionThreshold time before detection, starving other requests. Set leakDetectionThreshold to slightly longer than your p99 query time — if p99 is 2 seconds, set it to 5-10 seconds. Too short causes false positives; too long delays leak detection.

15. A connection pooler sits between application and database. What are the implications for prepared statement usage?

With pgBouncer in transaction mode, prepared statements do not persist across transactions because the connection is returned to the pool after each commit. A prepared statement on connection A is gone when your next transaction gets connection B. Workarounds: use session mode (less efficient), use the PostgreSQL protocol-level prepared statements with DEALLOCATE/EXECUTE, or use an application-level prepared statement cache that re-prepares on new connections. With HikariCP, each connection has its own prepared statements — this works fine as long as connections are not frequently cycled.

16. What is the difference between pool_mode = session and pool_mode = transaction in pgBouncer for application behavior?

In session mode, the connection to PostgreSQL is held for the entire client session — all transactions share the same connection until the client disconnects. In transaction mode, the connection is only held for the duration of a transaction, then returned to the pool. Session mode preserves session state (SET LOCAL, temp tables, advisory locks) but uses connections inefficiently. Transaction mode is more efficient but breaks anything requiring session state. Choose based on whether your application uses session-level features.

17. Your PostgreSQL server shows 500 idle connections but only 10 are running queries. Where are the other 490 connections?

Those 490 idle connections are in pgBouncer's pool — connections established to PostgreSQL but not currently executing queries. They are waiting for the next request from pgBouncer clients. This is normal for transaction pooling mode: pgBouncer maintains default_pool_size connections per database, and those connections show as idle in pg_stat_activity. If you have 10 application instances each with default_pool_size=50, you would see 500 idle connections even with no active queries.

18. What is "pool fatigue" and how does pgBouncer help prevent it?

Pool fatigue is when many application instances each have large pools, causing total required connections to exceed database's max_connections. Without pgBouncer, 50 application instances each with pool_size=20 need 1000 connections but PostgreSQL only allows 100. pgBouncer in transaction mode breaks this: each application instance connects to its local pgBouncer (10 connections), and all pgBouncers multiplex onto a small number of actual PostgreSQL connections. Pool fatigue is solved by proper pooling architecture, not by increasing max_connections.

19. How does connection pooling interact with PostgreSQL's idle_in_transaction_session_timeout setting?

idle_in_transaction_session_timeout terminates idle connections that have an open transaction but are not executing queries. With pgBouncer in transaction mode, connections are returned to the pool after each commit, so there are no idle in-transaction states. However, if a query takes a long time within a transaction (e.g., pgBouncer in session mode), the idle timer applies. Configure idle_in_transaction_session_timeout to auto-cleanup long-running queries that forget to commit — this prevents long-running transactions from holding locks and blocking autovacuum.

20. What is "statement batching" in the context of connection pooling, and how does it differ from pipelining?

Statement batching (like addBatch/executeBatch in JDBC) sends multiple statements in one network round trip, reducing round-trip overhead. Connection pooling enables batching by keeping connections warm — you reuse the same connection for multiple batches. Pipelining (PostgreSQL extended protocol) goes further: it sends multiple queries to the server without waiting for each response, overlapping network latency. Pipelining requires a dedicated connection (not pooled in transaction mode) because responses must be matched to requests. Pipelining can dramatically improve throughput for bulk inserts when combined with a persistent connection.

Conclusion

Category

Related Posts

Database Capacity Planning: A Practical Guide

Plan for growth before you hit walls. This guide covers growth forecasting, compute and storage sizing, IOPS requirements, and cloud vs on-prem decisions.

#database #capacity-planning #infrastructure

Database Monitoring: Metrics, Tools, and Alerting

Keep your PostgreSQL database healthy with comprehensive monitoring. This guide covers query latency, connection usage, disk I/O, cache hit ratios, and alerting with pg_stat_statements and Prometheus.

#database #monitoring #observability

Vacuuming and Reindexing in PostgreSQL

PostgreSQL's MVCC requires regular maintenance. This guide explains dead tuples, VACUUM vs VACUUM FULL, autovacuum tuning, REINDEX strategies, and how to monitor bloat with pg_stat_user_tables.

#database #postgresql #vacuum