Database Connection Pooling: Why Your App Slows Down Under Load
Most apps work fine in development with a handful of users. Under load, they slow to a crawl — or crash — because of connection pool misconfiguration.
What Is a Connection Pool?
Opening a database connection is expensive: TCP handshake, authentication, session setup — 20-100ms per connection.
A connection pool maintains a set of open connections that are reused across requests. Instead of opening a new connection per request, your app borrows one from the pool and returns it when done.
The Classic Misconfiguration
// Bad: creates a new connection per request
app.get('/users', async (req, res) => {
const client = new pg.Client(connectionString);
await client.connect(); // 50ms overhead every single time
const result = await client.query('SELECT * FROM users');
await client.end();
res.json(result.rows);
});
// Good: pool created once, reused for every request
const pool = new pg.Pool({ connectionString, max: 20 });
app.get('/users', async (req, res) => {
const result = await pool.query('SELECT * FROM users');
res.json(result.rows);
});
Pool Sizing
The right pool size depends on your workload, not a magic number:
pool_size = (core_count * 2) + effective_spindle_count
For most web apps:
- Too small: requests queue waiting for a connection — latency spikes
- Too large: database gets overwhelmed, context switching degrades performance
- Sweet spot: typically 10-25 connections per application instance
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // Maximum connections in pool
min: 2, // Maintain at least 2 open connections
idleTimeoutMillis: 30000, // Remove idle connections after 30s
connectionTimeoutMillis: 5000, // Fail fast if pool is exhausted
});
Prisma Pool Configuration
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
// Connection pool URL params
// ?connection_limit=20&pool_timeout=10
}
// prisma client initialization
const prisma = new PrismaClient({
datasources: {
db: {
url: `${process.env.DATABASE_URL}?connection_limit=20&pool_timeout=10`,
},
},
log: ['warn', 'error'],
});
Serverless: The Pool Problem
Serverless functions can't use traditional connection pools — each cold start creates a new pool, and hundreds of concurrent invocations can exhaust DB connections:
// Serverless: use a connection pooler (PgBouncer, Supabase pooler, Prisma Accelerate)
// DATABASE_URL points to the pooler, not directly to Postgres
const prisma = new PrismaClient({
datasources: { db: { url: process.env.DATABASE_POOL_URL } },
});
Monitoring Pool Health
// pg-pool exposes pool stats
setInterval(() => {
metrics.gauge('db.pool.total', pool.totalCount);
metrics.gauge('db.pool.idle', pool.idleCount);
metrics.gauge('db.pool.waiting', pool.waitingCount);
}, 5000);
// Alert if too many requests are waiting for a connection
if (pool.waitingCount > 5) {
alerts.warn('DB pool exhaustion risk', { waiting: pool.waitingCount });
}
Connection pooling, Prisma setup, and database observability patterns are all included in the AI SaaS Starter Kit — production-ready from day one.
Top comments (0)