Blog Post

Azure Database for PostgreSQL Blog
4 MIN READ

PgBouncer Best Practices in Azure Database for PostgreSQL – Part 1

Shashikant_Shakya's avatar
Oct 03, 2025

Introduction

Connection pooling is critical for scaling PostgreSQL workloads efficiently, especially in managed environments like Azure Database for PostgreSQL. PgBouncer, a lightweight connection pooler, helps manage thousands of client connections without overwhelming the database.
Connection pooling is very important when managing multiple concurrent database requests, as PostgreSQL uses a process-per-connection model, which means too many active connections can:

  • Increase context switching overhead
  • Consume excessive CPU/memory
  • Degrade performance under load

PgBouncer addresses this by limiting active server connections and queuing the additional client requests. However, misconfiguring key settings such as default_pool_size can still lead to CPU/memory pressure, connection bottlenecks, and degraded performance. Careful planning and tuning are essential to avoid these pitfalls.

Understanding connection pools

Before diving into tuning, it’s important to understand how PgBouncer organizes connections:

PgBouncer creates a separate pool for each unique (database, user) combination. For example:

  • If you have 2 application roles/users connecting to 2 databases. In this scenario, PgBouncer will allocate 4 pools.

Each pool maintains its own number of connections, determined by default_pool_size. So, the total number of potential server connections is:

number_of_pools × default_pool_size

This is why sizing default_pool_size correctly is critical.

Azure PgBouncer defaults

Azure Database for PostgreSQL comes with preconfigured PgBouncer settings optimized for most workloads. Understanding these defaults is essential before making any tuning changes:

  • pool_mode: TRANSACTION (default in Azure; best for most workloads)
  • default_pool_size: 50 (range: 1–4950)
  • max_client_conn: 5000 (range: 1–50000)

Transaction mode support for prepared statements

PgBouncer now enables support for PostgreSQL PREPARED STATEMENTS when combined together with TRANSACTION mode pooling.  Previously, in transaction mode cached plans were difficult to manage, as there was no way for PgBouncer to confirm whether a new connection allocated from the pool would benefit from any cached plans generated from prior PREPARED STATEMENT operations.  To work around this scenario, PgBouncer now provides a parameter which controls how many globally cached plan statements remain in memory for any pooled connection to leverage.  

  • max_prepared_statements: 200 (range: 0-5000)

PostgreSQL connection limits

For large tiers (e.g., 96 vCores), the default max_connections is 5000, with 15 reserved for system use. That means 4985 user connections are available.
For more details, see maximum connection.

Sizing best practices

Proper sizing ensures optimal performance and resource utilization. Here’s how to approach it:

1. Use transaction pooling

Start by confirming that pool_mode = TRANSACTION is enabled. This is already the Azure default and provides the best pooling efficiency for most web applications.  If your application is using prepared statements, ensure you configure max_prepared_statements accordingly.

2. Determine your maximum active concurrent database operations (max_concurrent_ops)

Next, you need to estimate how many total concurrent active PostgreSQL backends your instance can maintain:

  • For CPU-bound OLTP workloads: keep max_concurrent_ops near 1.5x -2x the number of CPU vCores.
  • For I/O-heavy workloads: stay slightly higher than vCore count.

Rule of thumb for 96 vCores:

max_concurrent_ops ≈ 144–192.

3. Divide across pools

Once you’ve estimated your max_concurrent_ops value, the next step is to distribute your capacity across all connection pools.

default_pool_size ≈ max_concurrent_ops / number_of_pools

Example:
max_concurrent_ops = 144
number_of_pools = 4
default_pool_size = 144 / 4 = 36

Sample configuration

To illustrate how these calculations translate into real-world settings, here’s a sample PgBouncer configuration tuned for a scenario with four pools and an Active_Backend_Target of 144.

pool_mode = transaction
default_pool_size = 36 ; tuned for 4 pools
max_client_conn = 5000
 

Quick reference table

For quick planning, the following table provides starting recommendations based on common Azure Database for PostgreSQL SKU sizes. Use these as a baseline and adjust according to your workload metrics.

SKU SizeMemoryDefault max_connectionsPoolsSuggested max_concurrent_opsStarting default_pool_size
8 vCores32 GiB3437212–166–12
16 vCores64 GiB5000224–3212–20
32 vCores128 GiB5000248–6430–40
48 vCores192 GiB5000272–9240–60
64 vCores256 GiB5000296–12850–70
96 vCores384–672 GiB50002144–19260–80

For all tiers ≥16 vCores, max_connections is capped at 5000 (with 15 reserved for system use).

Notes:

  • default_pool_size = max_concurrent_ops / number_of_pools
  • These values are starting recommendations. You should validate them against actual workload metrics and adjust gradually.
  • Always ensure: (number_of_pools × default_pool_size) < max_connections − 15 (reserved system slots)

Monitoring and tuning

After applying your configuration, continuous monitoring is key. Here’s how:

  • Use PgBouncer metrics in Azure Monitor to track active, idle, and waiting connections.
  • Run SHOW POOLS; for real-time stats; watch cl_waiting vs sv_idle. For detailed monitoring and management, visit the Admin Console.

Recommended Alerts:

  • Alert if waiting client connections > 0 while idle server connections = 0 (indicates pool exhaustion—consider increasing default_pool_size).
  • Alert if active server connections approach the configured default_pool_size (may indicate need for tuning).
  • Alert if max_client_conn utilization exceeds 80% (risk of client-side connection errors).

Tip:
If waiting client connections grow while idle server connections are zero, increase default_pool_size cautiously.
Review performance regularly and adjust gradually.

Common pitfalls

Avoid these mistakes when configuring PgBouncer:

  • Changing pool mode to SESSION by default: transaction pooling is better for most apps.  Session mode will not release connections until the session is ended.
  • Ignoring pool count: multiplying a large default_pool_size by many pools can exhaust connections.
  • Confusing max_client_conn with Postgres capacity: PgBouncer can accept many more clients than the server concurrent processes can support, any client connections not being processed will be waiting for resources.
  • Tuning without data: always review metrics before changes.

Conclusion

Choosing the right default_pool_size in Azure Database for PostgreSQL with PgBouncer is about balancing performance and resource efficiency. With built-in PgBouncer in Flexible Server, you can enable connection pooling with a single parameter making it easy to get started quickly. The default settings are optimized for most workloads, and as your requirements grow, you can further tune parameters like default_pool_size and max_connections to suit your needs. By understanding your workload, estimating an active concurrent operations, dividing it across pools while respecting PostgreSQL limits, and continuously monitoring and adjusting based on real data, you can achieve a stable, scalable, and cost-effective environment.

Further reading

For more in-depth guidance and real-world scenarios on PgBouncer configuration and tuning in Azure Database for PostgreSQL, explore the following resources:

  1. Leverage built-in PgBouncer in Flexible Server
  2. Monitoring PgBouncer in Azure PostgreSQL Flexible Server
  3. Identify and solve connection performance in Azure Postgres
  4. Not all Postgres connection pooling is equal
  5. Connection handling best practice with PostgreSQL
Updated Oct 03, 2025
Version 1.0
No CommentsBe the first to comment