Blog Post

Microsoft Blog for PostgreSQL
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

1 Comment

  • subhashkv77's avatar
    subhashkv77
    Copper Contributor

    Shashikant_Shakya​ - 

    Thanks for the great post—very useful for Azure Database for PostgreSQL users. I have a question about applying the default_pool_size guidance in a more typical Azure deployment.

    In our environment, we run Azure Database for PostgreSQL and evaluating how to configure PgBouncer , hosting 10+ databases on a single server. There is a strict 1:1 mapping between database and role, resulting in roughly 10 (database, user) pools.

    The challenge is that workloads are not uniform: a few databases are highly transactional with high concurrency, while others are low‑traffic configuration/reference databases.

    Given this, using the formula mentioned in the post:

    default_pool_size = max_concurrent_ops / number_of_pools

     

    seems to assume evenly distributed load across pools, which doesn’t reflect real‑world Azure workloads. Since default_pool_size applies uniformly to all (db, user) pools and cannot be tuned per pool in Azure PgBouncer, how should this calculation be interpreted for such mixed‑workload scenarios?

    Specifically:

    • How do you recommend sizing default_pool_size when only a subset of databases requires high concurrency?
    • Is the expected Azure‑recommended approach to rely more on max_db_connections, reserve pools, or even multiple PgBouncer deployments to handle uneven workloads?

    Any Azure‑specific guidance or best practices on adapting this formula would be greatly appreciated.