PostgreSQL Connection Exhaustion: How to Fix 'Too Many Connections' with PgBouncer

    Yash AminBy Yash Amin
    Last Updated: May 16, 2026•12 min read
    PostgreSQL connection pool exhaustion and PgBouncer architecture diagram

    ' Every new database connection attempt fails. Your application returns 500 errors or hangs waiting for a connection to become available.

    The on-call engineer gets paged, checks the database server — CPU at 20%, memory fine, disk fine — and wonders why PostgreSQL is refusing connections when it appears perfectly healthy. The answer lies in how PostgreSQL handles connections at the architecture level.

    Unlike MySQL's thread-based model, PostgreSQL spawns a dedicated OS process for every database connection. At 300 connections, you have 300 server processes running.

    At 500 connections, you are consuming approximately 5 GB of RAM purely in connection overhead — before a single query runs. The max_connections limit exists because the server will degrade severely and eventually crash if allowed to spawn unlimited processes.

    PgBouncer solves this by sitting as a lightweight proxy between your application and PostgreSQL, multiplexing hundreds of application connections onto a small, stable pool of actual database connections. This guide covers why the problem occurs, how to diagnose it, and how to deploy PgBouncer correctly.

    Getting connection errors right now? Book a 30-min diagnostic →

    Why PostgreSQL's Connection Architecture Creates This Problem

    PostgreSQL uses a process-per-connection model: each client connection maps to a dedicated backend process on the server. This design gives PostgreSQL strong isolation and simplifies the kernel scheduling model, but it imposes a real cost at connection scale. Each backend process consumes approximately 5–10 MB of memory at idle, plus the process spawn overhead when the connection is first established.

    At 200 connections, that baseline is 1–2 GB. 5–5 GB — purely from connection overhead, not from any query work. The max_connections default of 100 in PostgreSQL exists because the process-per-connection model genuinely degrades server performance beyond a few hundred connections.

    The typical production recommendation is to keep max_connections under 200–400 depending on instance memory and RAM, and to use connection pooling for everything beyond that. The problem becomes acute with modern application deployment patterns: containerised microservices each maintaining their own connection pools, serverless functions establishing new connections on every invocation, and ORM frameworks with connection pools configured independently per application instance. A Kubernetes deployment with 20 application pods, each running a connection pool of 10, is attempting to maintain 200 permanent connections against PostgreSQL before any user traffic arrives.

    -- Check current connection count and limits
    SELECT
      max_conn,
      used,
      res_for_super,
      max_conn - used - res_for_super AS available
    FROM (
      SELECT
        (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_conn,
        count(*) AS used,
        (SELECT setting::int FROM pg_settings WHERE name = 'superuser_reserved_connections') AS res_for_super
      FROM pg_stat_activity
    ) t;
    
    -- See active connections by application and state
    SELECT
      application_name,
      state,
      count(*)
    FROM pg_stat_activity
    WHERE state IS NOT NULL
    GROUP BY application_name, state
    ORDER BY count DESC;

    Related: what's new in PostgreSQL 18

    How to Confirm Connection Exhaustion Is Your Problem

    Before deploying PgBouncer, confirm that connection exhaustion is actually the issue. The clearest signal is the FATAL error in your application logs combined with a connection count approaching max_connections in pg_stat_activity. Check the CloudWatch DatabaseConnections metric if you are on RDS — a flat line at or near max_connections during incident periods is diagnostic.

    Check your application connection pool metrics: if the pool is at maximum size and connections are queuing, the pool is exhausted. Examine the wait_event and wait_event_type columns in pg_stat_activity: connections waiting on ClientRead are idle connections held open by the application; a large number of these means your application is holding connections longer than necessary. Also check for connections in the 'idle in transaction' state — these are connections inside an open transaction that is not currently executing a query.

    A large count of idle-in-transaction connections indicates the application is not committing or rolling back transactions promptly, holding connections and locks simultaneously.

    -- Connections by state — look for large 'idle' and 'idle in transaction' counts
    SELECT
      state,
      wait_event_type,
      wait_event,
      count(*) AS conn_count
    FROM pg_stat_activity
    WHERE pid != pg_backend_pid()
    GROUP BY state, wait_event_type, wait_event
    ORDER BY conn_count DESC;
    
    -- Connections idle for longer than 5 minutes (candidates for pgbouncer's server_idle_timeout)
    SELECT
      pid,
      application_name,
      state,
      now() - state_change AS idle_duration,
      usename
    FROM pg_stat_activity
    WHERE state IN ('idle', 'idle in transaction')
      AND now() - state_change > INTERVAL '5 minutes'
    ORDER BY idle_duration DESC;

    Related: why your API is slow even when the database looks fine, the N+1 query problem

    What PgBouncer Does and the Three Pooling Modes

    PgBouncer is a lightweight connection pooler that sits between your application and PostgreSQL. Your application connects to PgBouncer as if it were the database. PgBouncer maintains a small pool of actual PostgreSQL backend connections and multiplexes application connections across them.

    When an application connection sends a query, PgBouncer assigns it a free backend connection from the pool, routes the query, and returns the backend connection to the pool when done. From PostgreSQL's perspective, it sees only the pooler's N backend connections — never the potentially hundreds of application connections behind it.

    • Session pooling: assigns a backend connection to each application connection for its entire lifetime — effectively the same as direct PostgreSQL connections, with no multiplexing benefit during query execution

    • Transaction pooling: assigns a backend connection for the duration of each transaction — the highest-multiplexing mode and what most teams need: a backend connection is only held while work is in progress, not during the time between transactions when the application connection is idle

    • Statement pooling: assigns a connection per individual statement — incompatible with multi-statement transactions and rarely used

    For most production workloads, transaction pooling with a pool_size of 10–20 backend connections per database-user pair handles hundreds of application connections with no connection errors.

    Transaction mode's biggest historical limitation was prepared statements: because a client's protocol-level PREPARE could be routed through a different backend connection on the next execute, older PgBouncer versions silently broke them. PgBouncer 1.21 added prepared statement support in transaction mode behind the max_prepared_statements setting, and as of PgBouncer 1.24.1 that setting defaults to 200 and is enabled out of the box, so current versions correctly handle the protocol-level prepared statements your driver issues automatically. The one thing PgBouncer still can't see is a raw SQL PREPARE statement you type yourself in a query — that support only covers prepares issued through the wire protocol.

    How AWS and Google Cloud Set max_connections by Default

    Cloud providers derive max_connections from instance memory rather than picking a fixed number, and the exact math is public for AWS. 5 MB of instance memory per connection slot, with a hard ceiling of 5,000 connections no matter how large the instance is. The usable count in practice is often a little lower than the formula suggests, because RDS reserves some memory for its own management processes before running the calculation.

    Google Cloud SQL for PostgreSQL follows the same logic without publishing an exact formula: max_connections scales with machine type and available memory, but not linearly. The smallest tier, db-f1-micro, is capped at 25 connections by default, and larger tiers scale up from there.

    Step-by-Step: Installing and Configuring PgBouncer

    PgBouncer is a single binary with a plain-text configuration file. On Debian/Ubuntu, install with apt. On Amazon Linux, use yum or compile from source.

    txt (authentication credentials). ini [databases] section defines connection targets — each entry maps a logical database name (what the application connects to) to the actual PostgreSQL connection parameters. The [pgbouncer] section sets the listen address and port, the pooling mode, pool sizing, authentication method, and operational limits.

    Place PgBouncer on the same server as your application (sidecar pattern) or on a dedicated proxy host in the same subnet as PostgreSQL. For Kubernetes deployments, running PgBouncer as a sidecar container in the application pod eliminates network latency and keeps the proxy close to the consumer. For RDS, PgBouncer on an EC2 instance or ECS task in the same VPC and subnet achieves sub-millisecond proxy overhead.

    # pgbouncer.ini — transaction pooling configuration
    [databases]
    # Maps logical name → actual PostgreSQL connection
    myapp = host=prod-rds.cluster.us-east-1.rds.amazonaws.com port=5432 dbname=myapp
    
    [pgbouncer]
    listen_addr = 0.0.0.0
    listen_port = 5432
    auth_type = scram-sha-256
    auth_file = /etc/pgbouncer/userlist.txt
    
    # Pooling mode
    pool_mode = transaction
    
    # Core sizing
    max_client_conn = 2000        # max application connections to PgBouncer
    default_pool_size = 20        # backend connections per db-user pair
    min_pool_size = 5             # keep N connections warm at all times
    reserve_pool_size = 5         # extra connections for spikes
    reserve_pool_timeout = 3      # seconds to wait before using reserve pool
    
    # Timeouts
    server_idle_timeout = 600     # close idle backend connections after 10 min
    client_idle_timeout = 0       # never close idle client connections
    query_wait_timeout = 30       # queue application queries for up to 30 sec
    server_connect_timeout = 15   # fail fast if PostgreSQL is unreachable
    
    # Logging
    log_connections = 0           # disable in production (verbose)
    log_disconnections = 0
    stats_period = 60
    
    # userlist.txt (scram-sha-256 or md5 passwords)
    # "username" "password"

    Still Piecing This Together Yourself?

    A senior engineer looks at your actual setup, not a generic checklist, and tells you exactly what's wrong and how to fix it.

    Book a Discovery Call

    Calculating the Right pool_size

    The correct pool_size is not a fixed number — it depends on your PostgreSQL instance's CPU count, your workload's query duration, and your target concurrency. The formula commonly used is: pool_size = (CPU cores × 2) + effective_io_concurrency. 2xlarge with 8 vCPUs, that gives a baseline of 17–24 backend connections per database-user pair.

    In practice, start at 15–20 for OLTP workloads where queries complete in milliseconds, and benchmark upward. The pool_size determines how many queries can execute simultaneously against PostgreSQL. If you set it too low, application connections queue behind PgBouncer waiting for a free backend slot — you trade connection errors for queue latency.

    If you set it too high, you push PostgreSQL's backend process count past its efficient operating range. Monitor the PgBouncer stats table (SHOW STATS, SHOW POOLS) to see average query wait time and pool utilisation. If cl_waiting (clients waiting for a pool connection) is consistently above zero, increase pool_size.

    If sv_idle (idle backend connections) is consistently high relative to sv_used, decrease pool_size.

    -- Connect to PgBouncer admin interface and check pool status
    -- psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer
    
    -- See pool utilisation per database-user pair
    SHOW POOLS;
    -- Key columns:
    -- cl_active: clients connected and executing
    -- cl_waiting: clients queued waiting for a server connection (want this at 0)
    -- sv_active: server connections in use
    -- sv_idle: server connections idle (available)
    -- sv_used: server connections recently used
    
    -- Overall stats
    SHOW STATS;
    -- avg_query_time: average query duration through the pooler (microseconds)
    -- avg_wait_time: average time clients spent waiting for a server connection
    -- total_query_count: total queries routed since last reset

    Related: our AWS RDS performance tuning guide

    Serverless Functions and Connection Exhaustion — A Special Case

    Serverless functions (AWS Lambda, Vercel Edge Functions, Cloudflare Workers) are the worst-case scenario for direct PostgreSQL connections. Each function invocation establishes a new connection because there is no persistent process between invocations to maintain a connection pool. A Lambda function that executes 500 times per minute creates up to 500 connection establishment attempts per minute.

    Even with connection reuse across warm invocations — which is unreliable and depends on invocation frequency — the connection count scales linearly with concurrent executions. For Lambda and similar runtimes, two solutions exist: AWS RDS Proxy (a managed PgBouncer equivalent integrated with IAM authentication, useful when you are already on RDS and want zero infrastructure to manage) and a PgBouncer instance on a persistent compute layer (EC2, ECS, or a small container) that the serverless functions connect to. 015 per vCPU-hour on the underlying instance.

    small EC2 instance costs approximately $15/month and handles tens of thousands of application connections with sub-millisecond proxy overhead. large instance — before RDS Proxy or PgBouncer is in the picture at all. RDS Proxy addresses this by borrowing a connection from its own warm pool for each Lambda invocation and returning it when the invocation finishes, so Lambda's concurrency scales independently of the connection count PostgreSQL actually sees.

    # AWS RDS Proxy — managed connection pooling for Lambda
    # Create via AWS CLI (requires IAM role and Secrets Manager secret)
    aws rds create-db-proxy \
      --db-proxy-name myapp-proxy \
      --engine-family POSTGRESQL \
      --auth '[{"AuthScheme":"SECRETS","SecretArn":"arn:aws:secretsmanager:...","IAMAuth":"REQUIRED"}]' \
      --role-arn arn:aws:iam::123456789:role/rds-proxy-role \
      --vpc-subnet-ids subnet-abc123 subnet-def456 \
      --vpc-security-group-ids sg-abc123
    
    # Lambda environment: connect to Proxy endpoint (not RDS directly)
    # DB_HOST = myapp-proxy.proxy-abc123.us-east-1.rds.amazonaws.com
    
    # For self-managed PgBouncer on EC2:
    # Application → EC2 PgBouncer (port 5432) → RDS PostgreSQL
    # PgBouncer maintains 20 backend connections
    # Lambda can have 500+ concurrent connections to PgBouncer safely

    Monitoring PgBouncer in Production

    PgBouncer exposes operational metrics through its admin interface — a virtual database named pgbouncer accessible on the pooler's listen port. SHOW POOLS gives per-pool utilisation with client and server connection counts split by state. SHOW STATS gives aggregate throughput metrics including average query time, average wait time, and total requests.

    SHOW CLIENTS and SHOW SERVERS give per-connection detail useful during incident investigation. For continuous monitoring, pgbouncer_exporter is a Prometheus exporter that scrapes the admin interface and exposes all metrics for Grafana dashboards.

    • cl_waiting: should be zero or near-zero at steady state

    • avg_wait_time: should stay under 5ms for OLTP workloads

    • sv_idle: should stay above zero — if it reaches zero, increase pool_size before clients start queuing

    Set alerting on cl_waiting greater than 10 for more than 30 seconds — that is the leading indicator of connection starvation before it manifests as application errors.

    -- Prometheus alerting rules for PgBouncer
    # Alert if clients are waiting (connection starvation starting)
    - alert: PgBouncerClientsWaiting
      expr: pgbouncer_pools_cl_waiting > 5
      for: 30s
      labels:
        severity: warning
      annotations:
        summary: "PgBouncer clients waiting for connections"
        description: "{{ $value }} clients waiting in pool {{ $labels.database }}/{{ $labels.user }}"
    
    # Alert if average wait time exceeds 10ms
    - alert: PgBouncerHighWaitTime
      expr: pgbouncer_stats_avg_wait_time_seconds > 0.01
      for: 1m
      labels:
        severity: warning
    
    # Alert if no idle server connections (pool fully saturated)
    - alert: PgBouncerPoolSaturated
      expr: pgbouncer_pools_sv_idle == 0 and pgbouncer_pools_sv_active > 0
      for: 30s
      labels:
        severity: critical

    Related: our PostgreSQL performance optimization service

    How PgBouncer Compares to Pgpool-II, Supavisor, and PgCat

    PgBouncer is not the only pooler, and it is not always the right one. It is a small, single-threaded C daemon — recent versions can use multiple cores via SO_REUSEPORT, but it still has no concept of read replicas, sharding, or multi-tenancy.

    • Pgpool-II: an older, more feature-complete proxy that adds load balancing across read replicas, query caching, and automatic failover on top of connection pooling. Setup is more complex than PgBouncer and it does more per-query work, but it's worth evaluating if you need read/write splitting without changing application code.

    • Supavisor: Supabase's Elixir/BEAM-based pooler, open-sourced in 2023 and the default pooler on Supabase since early 2024. It's multi-tenant by design — a single cluster can pool thousands of separate databases, route by tenant ID, and survive individual node failures — and it supports named prepared statements in transaction mode more completely than PgBouncer. The trade-off is weight: a Supavisor node uses roughly 200-400MB of memory versus PgBouncer's single-digit-MB footprint, which is more infrastructure than a single application in front of a single Postgres cluster actually needs.

    • PgCat: a Rust-based pooler built specifically for sharded PostgreSQL deployments. PgBouncer has no concept of shards at all; PgCat's entire reason to exist is proxy-side sharding and query routing across shards, a use case most single-database applications will never hit.

    For a single application talking to a single PostgreSQL database or cluster, none of these three replace PgBouncer's simplicity. They earn their extra weight only when you need what they specifically add: read-replica routing, multi-tenant pooling at platform scale, or shard-aware routing.

    Get a Straight Answer, Not a Sales Pitch

    Tell us what you're running into. We'll tell you directly what's causing it and what it takes to fix, before you sign anything.

    Book a Discovery Call

    What Changed in PgBouncer 1.23 and 1.24

    PgBouncer is actively maintained, and the last two release cycles fixed real operational gaps. 0 (July 2024) added user name maps for cert and peer authentication, so the connecting OS or certificate user no longer has to match the database user name exactly. It also changed SIGTERM's default behavior: instead of shutting down immediately, PgBouncer now performs a 'super safe shutdown' that waits for clients to disconnect on their own, which is what makes rolling restarts of multiple PgBouncer processes behind a load balancer or a shared SO_REUSEPORT port actually safe.

    Replication connections can now pass through PgBouncer as well, which previously required bypassing the pooler entirely for logical replication setups.

    • PgBouncer 1.24.0 (January 2025): added the ability to track and limit connection counts per user and per database independently, rather than one global max_client_conn, and cut the performance overhead of RELOAD substantially on TLS-enabled setups. The reserve_pool setting was renamed reserve_pool_size (the old name still works as an alias).

    • PgBouncer 1.24.1 (April 2025): fixed CVE-2025-2291, a security issue where PgBouncer did not check a user's password VALID UNTIL expiration when authenticating through auth_query — meaning an expired password could still authenticate after it should have stopped working. Anyone running auth_query-based authentication on an older 1.24.x build should upgrade.

    Configuring Your ORM to Work With PgBouncer, Not Against It

    PgBouncer sits below your application's own connection pool, not instead of it, and the two need to be sized and configured together or they end up fighting each other. Prisma is the clearest example: its pgbouncer=true connection string parameter doesn't change pool sizing at all, it just tells Prisma to format queries in a way that's compatible with a pooler running in transaction mode. The actual pool size is still controlled separately by connection_limit in the same connection string, and that number should stay well below PgBouncer's own pool_size, otherwise Prisma's pool will happily open more connections than PgBouncer has backend slots to give it, and you're back to queuing.

    There's a second, easy-to-miss catch: Prisma's schema engine, which runs migrations and introspection, is designed to use a single direct connection and doesn't support running through PgBouncer in transaction mode at all, which is why most Prisma-on-PgBouncer setups keep a separate, non-pooled connection string specifically for migrations and CLI commands alongside the pooled one the running application uses.

    • Django: defaults to keeping a connection open for the lifetime configured by CONN_MAX_AGE, which breaks transaction-mode pooling because the connection never gets returned to PgBouncer between requests. Set CONN_MAX_AGE = 0 when running behind PgBouncer in transaction mode, and set DISABLE_SERVER_SIDE_CURSORS = True, since server-side cursors aren't supported in transaction mode either.

    • SQLAlchemy: should have its own pool sized down to match what PgBouncer is fronting, not left at its defaults; running SQLAlchemy's full default pool size on every application instance on top of PgBouncer's own pool just recreates the same over-provisioning problem PgBouncer exists to solve. Disabling SQLAlchemy's own pooling entirely (NullPool) and letting PgBouncer do all of the pooling is a common, simpler pattern once PgBouncer is already in place.

    The general rule holds across every framework: PgBouncer's pool_size is the real ceiling on concurrent queries against PostgreSQL, and every application-layer pool sitting in front of it should be sized as a fraction of that ceiling, not as if it were talking to PostgreSQL directly. Miss this and adding PgBouncer doesn't remove the connection pressure, it just relocates it one layer up, from PostgreSQL's max_connections to PgBouncer's own client and pool limits.

    PostgreSQL connection exhaustion is one of the most disruptive production database incidents — and one of the most preventable. The root cause is rarely the database itself and almost always connection management at the application layer: too many persistent connections from too many application instances, serverless functions creating connections per invocation, or ORM pools configured independently per pod. PgBouncer solves this with minimal operational overhead: a single binary, a simple configuration file, and transaction pooling mode that multiplexes hundreds of application connections onto 15–25 backend connections.

    Most teams can go from connection errors to stable production in an afternoon. The key configuration decisions are pool_mode = transaction, default_pool_size sized to 2× your vCPU count, and monitoring on cl_waiting as the leading indicator of pool saturation. If you are on RDS and want managed pooling without running your own infrastructure, RDS Proxy is the AWS-native equivalent — slightly more expensive, zero infrastructure to manage.

    Frequently Asked Questions

    What causes 'too many clients already' in PostgreSQL?

    PostgreSQL uses a process-per-connection model — each connection spawns a dedicated OS process consuming 5–10 MB of memory. When connections reach the max_connections limit (default 100), PostgreSQL rejects all new connection attempts with 'FATAL: sorry, too many clients already.' The fix is connection pooling with PgBouncer, which multiplexes hundreds of application connections onto a small pool of actual backend connections that PostgreSQL can handle.

    What is PgBouncer and how does it work?

    PgBouncer is a lightweight connection pooler that sits between your application and PostgreSQL. Applications connect to PgBouncer as if it were the database. PgBouncer maintains a small pool of real PostgreSQL connections and routes queries from application connections through them. From PostgreSQL's perspective, it only sees the pooler's connections — never the potentially hundreds of application connections behind it.

    Should I use session mode or transaction mode in PgBouncer?

    Use transaction pooling (pool_mode = transaction) for most production workloads. Transaction mode assigns a backend connection only for the duration of each transaction, not the entire application session — providing the highest multiplexing ratio. Session mode holds a backend connection for the full application session lifetime, providing no multiplexing benefit. Avoid statement mode unless your workload uses no multi-statement transactions.

    What pool_size should I set in PgBouncer?

    Start with pool_size = (CPU cores × 2) + effective_io_concurrency. For a db.r6g.2xlarge with 8 vCPUs, that is 17–24 backend connections. Monitor cl_waiting in SHOW POOLS — if it is consistently above zero, increase pool_size. If sv_idle is consistently high, decrease it. For most OLTP workloads, 15–20 backend connections per database-user pair handles hundreds of application connections without errors.

    Does AWS RDS Proxy replace PgBouncer?

    AWS RDS Proxy is a managed connection pooler built for RDS that requires no self-managed infrastructure. It adds 2–5ms overhead per query and costs $0.015 per vCPU per hour. Self-managed PgBouncer on an EC2 instance costs approximately $15/month with sub-millisecond overhead. Use RDS Proxy for Lambda and serverless workloads where zero infrastructure management matters. Use PgBouncer for non-serverless applications where latency and cost efficiency are priorities.

    Why do I get 'prepared statement already exists' errors with PgBouncer?

    This happens when a client library that relies on server-side prepared statements — Prisma, asyncpg, or the Postgres JDBC driver — connects through PgBouncer in transaction mode, which by default doesn't guarantee the same backend connection across statements in a session. PgBouncer 1.21 and later, with max_prepared_statements set above 0, added support for prepared statements in transaction mode; Prisma needs pgbouncer=true on the connection string, and SQLAlchemy should use NullPool so it isn't running its own pool on top of PgBouncer's.

    Is Supavisor a drop-in replacement for PgBouncer?

    Not really — they're built for different problems. Supavisor is Supabase's Elixir/BEAM-based pooler, designed for multi-tenant, cloud-scale pooling where one cluster fronts a large number of separate Postgres databases, and Supabase has published benchmark claims of over 1 million simultaneous client connections against a single cluster. It's also heavier per node (roughly 200–400MB) than PgBouncer. Outside Supabase's own hosting, self-managed PgBouncer is still the simpler option to operate.

    How much faster is PostgreSQL with PgBouncer versus connecting directly?

    In published transaction-pooling benchmarks, PgBouncer sustains roughly 73,000 TPS versus roughly 8,000 TPS connecting directly to Postgres at the same client concurrency — the gap comes from avoiding a fresh backend process spin-up per connection, not from PgBouncer making individual queries faster. The benefit is largest under high connection churn (short-lived connections, serverless invocations), and smaller for a handful of long-lived, already-pooled application connections.

    What's a safe max_connections number if I haven't set up pooling yet?

    As a starting point before you add a pooler, 2–3 connections per CPU core is a commonly cited ceiling for OLTP workloads — well below PostgreSQL's default of 100 for small instances, but the real constraint is memory (5–10MB per idle backend process) and context-switching overhead, not an arbitrary count. Once your application's combined connection demand crosses roughly 200, connection pooling is the fix, not a higher max_connections value.

    If my ORM already has a connection pool, do I still need PgBouncer?

    Usually yes. An ORM's connection pool (SQLAlchemy's QueuePool, Prisma's internal pool) only manages connections within a single application process. Run 20 pods or Lambda instances each with a pool of 10, and PostgreSQL sees the sum — 200 connections — regardless of how well each individual pool is tuned. PgBouncer sits in front of every instance and centralizes pooling across your whole fleet, which an in-process ORM pool structurally can't do.

    Get a Straight Answer on Your Setup

    Tell us what you're running into. We read every message personally and reply within 24 hours with times for a free call.

    We'll reply within 24 hours. Your information is never shared.