DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Database Connection Pooling: Why Your App Slows Down Under Load

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);
});
Enter fullscreen mode Exit fullscreen mode

Pool Sizing

The right pool size depends on your workload, not a magic number:

pool_size = (core_count * 2) + effective_spindle_count
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

Prisma Pool Configuration

// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
  // Connection pool URL params
  // ?connection_limit=20&pool_timeout=10
}
Enter fullscreen mode Exit fullscreen mode
// prisma client initialization
const prisma = new PrismaClient({
  datasources: {
    db: {
      url: `${process.env.DATABASE_URL}?connection_limit=20&pool_timeout=10`,
    },
  },
  log: ['warn', 'error'],
});
Enter fullscreen mode Exit fullscreen mode

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 } },
});
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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)