How to Set Up Database Connection Pooling

How-To Guide

How to Set Up Database Connection Pooling

TL;DR

TL;DR (20 minutes): Use your database provider's built-in pooler (Supabase, Neon, and PlanetScale all ship one). For Prisma, add ?pgbouncer=true to your connection string. Target 2-5 connections per serverless function instance, 10-20 for a traditional server. Handle connection errors and retry on timeout.

Prerequisites:

  • PostgreSQL, MySQL, or similar database
  • Understanding of your deployment environment (serverless vs. traditional)
  • Database connection string

Why This Matters

Database connections are expensive to create. Each connection uses memory, requires authentication, and takes time to establish. Without pooling, serverless functions can exhaust your database's connection limit within seconds during traffic spikes.

Connection pooling also tightens security. Centralizing connection management makes anomalies easier to spot and reduces how much of your database is directly reachable from your app code.

Step-by-Step Guide

1

Understand the problem

// Without pooling - each request creates new connection
export async function handler(req) {
  const client = new Client(connectionString);
  await client.connect();  // ~50-100ms overhead
  const result = await client.query('SELECT...');
  await client.end();
  return result;
}

// 100 concurrent requests = 100 connections
// Most databases allow only 100-500 connections total
// Result: "too many connections" errors

Connection pooling reuses existing connections, reducing overhead and preventing exhaustion.

2

Supabase connection pooling

Supabase provides built-in connection pooling via Supavisor:

// Direct connection (for migrations, admin tasks)
DATABASE_URL="postgresql://postgres:password@db.xxx.supabase.co:5432/postgres"

// Pooled connection (for application use) - use port 6543
DATABASE_URL="postgresql://postgres:password@db.xxx.supabase.co:6543/postgres?pgbouncer=true"

// For Prisma with Supabase
// schema.prisma
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")  // For migrations
}

// .env
DATABASE_URL="postgresql://...@db.xxx.supabase.co:6543/postgres?pgbouncer=true"
DIRECT_URL="postgresql://...@db.xxx.supabase.co:5432/postgres"
3

Neon connection pooling

// Neon provides built-in pooling
// Use the pooled connection string from dashboard

// With Prisma
DATABASE_URL="postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/mydb?sslmode=require&pgbouncer=true"

// Neon also supports serverless driver for edge
import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_URL);
const result = await sql`SELECT * FROM users WHERE id = ${userId}`;
4

Prisma connection pooling

// For serverless (Vercel, Lambda, etc.)
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// Connection management for serverless
import { PrismaClient } from '@prisma/client';

// Prevent multiple instances in development
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
};

export const prisma = globalForPrisma.prisma ?? new PrismaClient({
  log: process.env.NODE_ENV === 'development' ? ['query'] : [],
});

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma;
}

// Connection string with pool settings
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=5&pool_timeout=10"
5

node-postgres (pg) pooling

import { Pool } from 'pg';

// Create pool (do this once, not per request)
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,                    // Maximum connections in pool
  idleTimeoutMillis: 30000,   // Close idle connections after 30s
  connectionTimeoutMillis: 5000, // Timeout for new connections
  maxUses: 7500,              // Close connection after N queries
});

// Use pool for queries
export async function getUser(id) {
  const client = await pool.connect();
  try {
    const result = await client.query(
      'SELECT * FROM users WHERE id = $1',
      [id]
    );
    return result.rows[0];
  } finally {
    client.release();  // Always release back to pool!
  }
}

// Or simpler syntax (auto-releases)
export async function getUsers() {
  const result = await pool.query('SELECT * FROM users');
  return result.rows;
}
6

PgBouncer for self-hosted databases

# Install PgBouncer
sudo apt install pgbouncer

# /etc/pgbouncer/pgbouncer.ini
[databases]
mydb = host=localhost port=5432 dbname=mydb

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction    # Best for web apps
max_client_conn = 1000
default_pool_size = 20
min_pool_size = 5
reserve_pool_size = 5

# /etc/pgbouncer/userlist.txt
"myuser" "md5passwordhash"

# Connect through PgBouncer
psql -h localhost -p 6432 -U myuser mydb
7

Serverless-specific configuration

// For Vercel Edge Functions / Cloudflare Workers
// Use HTTP-based database connections

// Neon Serverless Driver
import { neon } from '@neondatabase/serverless';

export const config = { runtime: 'edge' };

export default async function handler(req) {
  const sql = neon(process.env.DATABASE_URL);
  const users = await sql`SELECT * FROM users LIMIT 10`;
  return Response.json(users);
}

// PlanetScale Serverless Driver
import { connect } from '@planetscale/database';

const conn = connect({
  host: process.env.DATABASE_HOST,
  username: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD,
});

const results = await conn.execute('SELECT * FROM users');

Pool Sizing Guidelines:

  • Serverless functions: 1-5 connections per function instance
  • Traditional Node.js server: 10-20 connections
  • Formula: connections = (cores * 2) + spindle_count (for traditional DBs)
  • Never exceed: Database max_connections / number_of_app_instances
  • Check pg_stat_activity regularly; the numbers in guides are starting points, not limits

How to Verify It Worked

  1. Check connection count: Run the SQL below before and after deploying. With pooling, concurrent requests won't each add a new row.
  2. Watch for reuse: The connection count should stay flat under load, not grow linearly with requests.
  3. Spot "too many connections" in logs: If that error disappears after adding the pooler, you're done.
-- PostgreSQL: Check current connections
SELECT count(*) FROM pg_stat_activity;

-- See connections by application
SELECT application_name, count(*)
FROM pg_stat_activity
GROUP BY application_name;

-- Check connection states
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;

Common Errors & Troubleshooting

Error: "too many connections"

You've hit your database's connection ceiling. Reduce pool size first (serverless functions commonly run with a pool of 10+ when they only need 2-3). If the problem persists, add PgBouncer or switch to a managed pooler.

Error: "connection terminated unexpectedly"

Pool timeout or the database closed an idle connection. Add retry logic, or raise idleTimeoutMillis so the pool keeps connections alive a bit longer.

Slow queries after an idle period

The pool let connections expire and they're being re-created from scratch. Set min_pool_size (PgBouncer) or use the keepAlive option in pg to stop this.

Prisma + PgBouncer prepared statements error

Add ?pgbouncer=true to your connection string. This disables prepared statements, which don't work in transaction pooling mode.

Transaction vs. session pooling?

Transaction pooling (recommended for web apps) assigns a connection per transaction. Session pooling assigns per client session. Transaction mode is more efficient but doesn't support certain features like prepared statements or SET commands.

Do I need external pooling if my ORM has built-in pooling?

For serverless, yes - each function instance creates its own pool. External poolers like PgBouncer or managed database poolers can aggregate connections across all instances.

Why are my Vercel functions exhausting connections?

Each function invocation can create new connections. Use your database provider's pooler, reduce pool size to 1-2, and consider using HTTP-based serverless database drivers.

Related guides:Prisma Security · PostgreSQL Roles · Database Encryption

How-To Guides

How to Set Up Database Connection Pooling