Connection Pooling

Patterns for managing database connections efficiently in serverless and high-traffic environments.

Overview#

Connection pooling optimizes database access:

  • Reduced connection overhead
  • Better resource utilization
  • Serverless-compatible
  • Improved scalability

Prerequisites:

  • Prisma ORM setup
  • PgBouncer or Neon for pooling (optional)

Implementation#

Prisma Connection Pooling#

1// lib/db.ts 2import { PrismaClient } from '@prisma/client' 3 4const globalForPrisma = globalThis as unknown as { 5 prisma: PrismaClient | undefined 6} 7 8export const prisma = 9 globalForPrisma.prisma ?? 10 new PrismaClient({ 11 log: process.env.NODE_ENV === 'development' 12 ? ['query', 'error', 'warn'] 13 : ['error'], 14 datasources: { 15 db: { 16 url: process.env.DATABASE_URL 17 } 18 } 19 }) 20 21if (process.env.NODE_ENV !== 'production') { 22 globalForPrisma.prisma = prisma 23}

Connection URL with Pool Settings#

1# .env 2# PostgreSQL connection with pool settings 3DATABASE_URL="postgresql://user:password@localhost:5432/mydb?connection_limit=20&pool_timeout=30" 4 5# Using PgBouncer (recommended for serverless) 6DATABASE_URL="postgresql://user:password@pgbouncer:6432/mydb?pgbouncer=true" 7 8# Direct URL for migrations (bypasses PgBouncer) 9DIRECT_URL="postgresql://user:password@localhost:5432/mydb"
1// prisma/schema.prisma 2datasource db { 3 provider = "postgresql" 4 url = env("DATABASE_URL") 5 directUrl = env("DIRECT_URL") 6}

Serverless Connection Handling#

1// lib/db/serverless.ts 2import { PrismaClient } from '@prisma/client' 3import { Pool } from '@neondatabase/serverless' 4 5// For Neon serverless driver 6const pool = new Pool({ connectionString: process.env.DATABASE_URL }) 7 8// For edge functions 9export async function queryWithServerlessPool<T>( 10 query: string, 11 params?: any[] 12): Promise<T[]> { 13 const client = await pool.connect() 14 try { 15 const result = await client.query(query, params) 16 return result.rows as T[] 17 } finally { 18 client.release() 19 } 20} 21 22// Prisma with connection limit for serverless 23const prisma = new PrismaClient({ 24 datasources: { 25 db: { 26 url: `${process.env.DATABASE_URL}?connection_limit=1` 27 } 28 } 29}) 30 31export { prisma }

PgBouncer Configuration#

1# pgbouncer.ini 2[databases] 3mydb = host=localhost port=5432 dbname=mydb 4 5[pgbouncer] 6listen_addr = 0.0.0.0 7listen_port = 6432 8auth_type = md5 9auth_file = /etc/pgbouncer/userlist.txt 10 11# Pool settings 12pool_mode = transaction 13max_client_conn = 1000 14default_pool_size = 25 15min_pool_size = 5 16reserve_pool_size = 5 17 18# Timeouts 19server_connect_timeout = 15 20server_idle_timeout = 600 21server_lifetime = 3600

Connection Lifecycle Management#

1// lib/db/lifecycle.ts 2import { PrismaClient } from '@prisma/client' 3 4class DatabaseManager { 5 private static instance: PrismaClient | null = null 6 private static connectionCount = 0 7 8 static async getClient(): Promise<PrismaClient> { 9 if (!this.instance) { 10 this.instance = new PrismaClient({ 11 log: ['error', 'warn'] 12 }) 13 await this.instance.$connect() 14 console.log('Database connected') 15 } 16 this.connectionCount++ 17 return this.instance 18 } 19 20 static async releaseClient(): Promise<void> { 21 this.connectionCount-- 22 if (this.connectionCount <= 0 && this.instance) { 23 // Optionally disconnect when no clients are using it 24 // await this.instance.$disconnect() 25 // this.instance = null 26 } 27 } 28 29 static async disconnect(): Promise<void> { 30 if (this.instance) { 31 await this.instance.$disconnect() 32 this.instance = null 33 this.connectionCount = 0 34 console.log('Database disconnected') 35 } 36 } 37 38 static async healthCheck(): Promise<boolean> { 39 try { 40 const client = await this.getClient() 41 await client.$queryRaw`SELECT 1` 42 return true 43 } catch { 44 return false 45 } 46 } 47} 48 49// Graceful shutdown 50process.on('SIGINT', async () => { 51 await DatabaseManager.disconnect() 52 process.exit(0) 53}) 54 55process.on('SIGTERM', async () => { 56 await DatabaseManager.disconnect() 57 process.exit(0) 58}) 59 60export { DatabaseManager }

Connection Pool Monitoring#

1// lib/db/monitoring.ts 2import { PrismaClient } from '@prisma/client' 3 4interface PoolMetrics { 5 activeConnections: number 6 idleConnections: number 7 waitingRequests: number 8 totalQueries: number 9} 10 11class MonitoredPrismaClient extends PrismaClient { 12 private metrics: PoolMetrics = { 13 activeConnections: 0, 14 idleConnections: 0, 15 waitingRequests: 0, 16 totalQueries: 0 17 } 18 19 constructor() { 20 super({ 21 log: [ 22 { level: 'query', emit: 'event' }, 23 { level: 'error', emit: 'event' } 24 ] 25 }) 26 27 // Track queries 28 this.$on('query' as never, (e: any) => { 29 this.metrics.totalQueries++ 30 console.log(`Query: ${e.query} - Duration: ${e.duration}ms`) 31 }) 32 33 this.$on('error' as never, (e: any) => { 34 console.error('Database error:', e) 35 }) 36 } 37 38 getMetrics(): PoolMetrics { 39 return { ...this.metrics } 40 } 41 42 async getPoolStatus(): Promise<object> { 43 const result = await this.$queryRaw<any[]>` 44 SELECT 45 count(*) FILTER (WHERE state = 'active') as active, 46 count(*) FILTER (WHERE state = 'idle') as idle, 47 count(*) as total 48 FROM pg_stat_activity 49 WHERE datname = current_database() 50 ` 51 return result[0] 52 } 53} 54 55export const monitoredPrisma = new MonitoredPrismaClient()

Health Check Endpoint#

1// app/api/health/db/route.ts 2import { NextResponse } from 'next/server' 3import { monitoredPrisma } from '@/lib/db/monitoring' 4 5export async function GET() { 6 try { 7 const status = await monitoredPrisma.getPoolStatus() 8 const metrics = monitoredPrisma.getMetrics() 9 10 return NextResponse.json({ 11 healthy: true, 12 poolStatus: status, 13 metrics 14 }) 15 } catch (error) { 16 return NextResponse.json( 17 { healthy: false, error: 'Database connection failed' }, 18 { status: 503 } 19 ) 20 } 21}

Retry Logic with Backoff#

1// lib/db/retry.ts 2import { PrismaClient, Prisma } from '@prisma/client' 3 4interface RetryOptions { 5 maxRetries?: number 6 baseDelay?: number 7 maxDelay?: number 8} 9 10export async function withRetry<T>( 11 operation: () => Promise<T>, 12 options: RetryOptions = {} 13): Promise<T> { 14 const { maxRetries = 3, baseDelay = 100, maxDelay = 5000 } = options 15 16 let lastError: Error | null = null 17 18 for (let attempt = 0; attempt < maxRetries; attempt++) { 19 try { 20 return await operation() 21 } catch (error) { 22 lastError = error as Error 23 24 // Check if error is retryable 25 if (error instanceof Prisma.PrismaClientKnownRequestError) { 26 // Connection errors 27 if (['P1001', 'P1002', 'P1008', 'P1017'].includes(error.code)) { 28 const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay) 29 console.log(`Retrying after ${delay}ms (attempt ${attempt + 1}/${maxRetries})`) 30 await new Promise(resolve => setTimeout(resolve, delay)) 31 continue 32 } 33 } 34 35 // Non-retryable error 36 throw error 37 } 38 } 39 40 throw lastError 41} 42 43// Usage 44const users = await withRetry(() => prisma.user.findMany(), { 45 maxRetries: 3, 46 baseDelay: 100 47})

Edge-Compatible Setup#

1// lib/db/edge.ts 2import { neon } from '@neondatabase/serverless' 3 4// For Edge Runtime (no persistent connections) 5const sql = neon(process.env.DATABASE_URL!) 6 7export async function queryEdge<T>( 8 query: string, 9 params?: any[] 10): Promise<T[]> { 11 const result = await sql(query, params) 12 return result as T[] 13} 14 15// Usage in Edge API route 16export const runtime = 'edge' 17 18export async function GET() { 19 const users = await queryEdge<User>('SELECT * FROM users LIMIT 10') 20 return Response.json(users) 21}

Best Practices#

  1. Use singleton pattern - Prevent connection exhaustion in development
  2. Configure connection limits - Match pool size to database limits
  3. Use PgBouncer for serverless - Efficient connection reuse
  4. Implement health checks - Monitor connection pool status
  5. Handle connection errors - Implement retry logic with backoff