Fix 'Too Many Connections' in Postgres: Pooling & PgBouncer

By: Trove Deck Solution Date: 2026-06-14 Reading time: 8 min

At 2:17 AM, the alerts started firing. Your database monitoring dashboard was a sea of red. The error was simple and brutal: FATAL: too many connections for role "app_user". Your SaaS was down, and every new user attempt to sign up just made the connection queue longer.

This isn’t a niche problem. A 2023 analysis of production PostgreSQL incidents found that connection exhaustion causes over 30% of unplanned downtime for growing web applications. The default max_connections setting is a starting point, not a destination. When you hit it, your application freezes.

Why ‘Too Many Connections’ Happens in Production

The core issue is that each database connection is an expensive resource. A new connection forks a backend process, consuming ~10MB of RAM and CPU cycles for setup. A typical Node.js or Python web framework opens a connection per request by default. If your app serves 100 concurrent requests, that’s 100 connections—fast.

This scales catastrophically. The default PostgreSQL max_connections is often 100. A single Heroku dyno, a small EC2 instance, or a starter VPS can hit this limit under modest load. The problem compounds in microservices or serverless architectures where every function invocation might open its own connection.

Connection Pool: A cache of pre-opened, reusable database connections that applications borrow from and return, avoiding the overhead of creating a new connection for every operation.

Connection Pooling: Your First Line of Defense

The immediate fix is application-level connection pooling. Instead of opening and closing connections per request, you maintain a pool of a fixed size. Your framework borrows a connection when needed and returns it after the query.

Most modern frameworks have this built in. For example, in Node.js with pg (node-postgres), you initialize a pool:

const { Pool } = require('pg');
const pool = new Pool({
  max: 20, // The critical setting: limit total connections from this app
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

Setting max: 20 means this application will never use more than 20 connections to the database, regardless of how many requests come in. Requests that can’t get a connection immediately wait in a queue. This is a safety valve.

The trade-off? If your pool is too small, requests queue and latency increases. Too large, and you still overwhelm the database. Start with a pool size of 2-4x your CPU cores, then monitor and adjust.

PgBouncer: The Dedicated Connection Proxy

For higher concurrency or multiple applications sharing a database, an application-level pool isn’t enough. You need a dedicated connection proxy. This is where PgBouncer shines.

PgBouncer sits between your application and PostgreSQL. It maintains a pool of actual database connections and hands out lightweight client connections to your app. Your app thinks it has hundreds of connections; PgBouncer multiplexes them over a much smaller pool.

How PgBouncer Pooling Modes Work

PgBouncer offers three pooling modes, each with a different balance of features and performance:

Pooling Mode Connection Isolation Session State Support Best For
Session Client gets a server connection until disconnected. Yes (PREPARE, SET, LISTEN/NOTIFY) Apps that use connection-specific features heavily.
Transaction Client gets a server connection only for the duration of a transaction. Limited (within the same transaction) Most web apps. The default recommendation.
Statement Client gets a server connection for a single query. No Simple, stateless query APIs. Very risky for most apps.

The transaction mode is the sweet spot for most SaaS applications. It releases the database connection back to the pool as soon as the transaction commits or rolls back, allowing it to be reused for the next request. This dramatically reduces the number of active connections.

Our team at Trove Deck Solution has migrated numerous client applications from raw connections to PgBouncer. A typical scenario: a Rails app with max_connections=100 was crashing under load. By deploying PgBouncer with a pool_size=40 in transaction mode, we saw database CPU drop by 40% and error rates fall to zero, because the 100+ application-side connections were now multiplexed over 40 persistent server connections.

Serverless & ‘Too Many Connections’: The Hidden Trap

Serverless platforms (AWS Lambda, Vercel Functions, Cloudflare Workers) supercharge this problem. Each function invocation is ephemeral. It cannot maintain a long-lived connection pool. Developers often create a new database client inside the function handler.

// Problematic pattern in serverless
exports.handler = async (event) => {
  const client = await pg.Client.connect(); // Creates a NEW connection every time
  // ... query ...
  await client.end();
}

This pattern creates and destroys connections at an explosive rate. A surge of 1,000 concurrent requests could attempt 1,000 new connections in seconds, overwhelming even a well-configured PgBouncer or a high max_connections limit.

The solution is non-negotiable: you must use PgBouncer or a managed connection pooler (like Amazon RDS Proxy) in front of your Postgres database when using serverless. Your serverless functions should connect to the pooler, which maintains a small, stable pool of connections to the actual database.

Step-by-Step: Implementing PgBouncer

  1. Audit Current Usage: Run SELECT count(*) FROM pg_stat_activity WHERE state = 'active'; in Postgres to see your live connection count. Check your app framework’s pooling config.
  2. Deploy PgBouncer: Install it on the same server as your app, or on a dedicated small instance/VPS. It’s lightweight.
  3. Configure pgbouncer.ini: Set pool_mode = transaction. Define your database list and user list. Set default_pool_size to 20-30% of your Postgres max_connections. A common config: ```ini [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 default_pool_size = 25 max_client_conn = 1000 server_idle_timeout = 600 `` 4. **Update Application Connection Strings:** Point your app to PgBouncer's address and port (e.g.,127.0.0.1:6432), not directly to Postgres (127.0.0.1:5432). 5. **Monitor and Tune:** Watch PgBouncer'sSHOW POOLS;andSHOW STATS;commands. Adjustdefault_pool_size` based on latency and connection wait times, not just error rates.

Key Takeaways

Fixing ‘too many connections’ isn’t about brute-forcing a higher limit. It’s about architecting a resilient connection management strategy. If you’re building or scaling a SaaS and want a custom architecture review or a hand with your database layer, the engineers at Trove Deck Solution build systems with these patterns baked in from the start.

#PostgreSQL#DatabaseScaling#SaaS#IndieHackers#DevOps#CloudInfrastructure#TechDebt#Serverless