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 = 3600Connection 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#
- Use singleton pattern - Prevent connection exhaustion in development
- Configure connection limits - Match pool size to database limits
- Use PgBouncer for serverless - Efficient connection reuse
- Implement health checks - Monitor connection pool status
- Handle connection errors - Implement retry logic with backoff
Related Patterns#
- Prisma - Prisma setup and basics
- Transactions - Transaction handling
- Queries - Query optimization