Postgres Connection Pooling Pattern
Overview
Section titled “Overview”All services that talk to PostgreSQL must use a shared connection pool instead of opening a new raw pg client per request or per Cloud Run instance. This prevents exhausting the database’s max connections and aligns with the pattern used by apps/webapp.
Current Implementation in @repo/db (packages/db/src/client.ts)
Section titled “Current Implementation in @repo/db (packages/db/src/client.ts)”packages/db/src/client.ts provides a singleton Drizzle ORM instance (db) backed by a connection pool. By default it uses @neondatabase/serverless (NeonPool + drizzle-orm/neon-serverless), or standard pg (PgPool + drizzle-orm/node-postgres) when USE_PG_DRIVER=1 is set (for direct TCP connections to GCE/Docker PostgreSQL):
export function getDatabaseConfig(): DatabaseConfig { return { connectionString: dbEnv.DATABASE_URL, max: dbEnv.DB_POOL_MAX || (isProduction() ? 20 : 5), idleTimeoutMillis: 30000, connectionTimeoutMillis: 10000, };}import { Pool as NeonPool } from '@neondatabase/serverless';import { drizzle as drizzleNeon } from 'drizzle-orm/neon-serverless';import { drizzle as drizzlePg } from 'drizzle-orm/node-postgres';import pg from 'pg';import * as schema from './schema/index.js';
const usePgDriver = process.env.USE_PG_DRIVER === '1' || process.env.USE_PG_DRIVER === 'true';
const pool = usePgDriver ? new pg.Pool({ connectionString: config.connectionString, max: config.max, idleTimeoutMillis: config.idleTimeoutMillis, connectionTimeoutMillis: config.connectionTimeoutMillis, }) : new NeonPool({ connectionString: config.connectionString, max: config.max, idleTimeoutMillis: config.idleTimeoutMillis, connectionTimeoutMillis: config.connectionTimeoutMillis, });
export const db = usePgDriver ? drizzlePg(pool, { schema }) : drizzleNeon(pool, { schema });The Drizzle db instance and underlying pool are singletons imported from @repo/db wherever queries are needed.
Recommended Pattern for Other Services
Section titled “Recommended Pattern for Other Services”- Create a shared pool module similar to
packages/db/src/client.ts. - Do not instantiate a pool inside request handlers – import the singleton.
- Configure via
DATABASE_URL. When running on Neon, the URL includes the serverless endpoint; for Cloud SQL use the PgBouncer host. - Use
pool.connect()only for transactions and release the client promptly withfinally { client.release(); }. - Set sensible limits in production Cloud SQL deployments (e.g.,
max: 5per instance) to stay well below the instance’s total connection quota.
Example Usage in Another Service
Section titled “Example Usage in Another Service”import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL, // Adjust for CloudSQL if needed max: Number(process.env.PG_POOL_MAX) || 5, idleTimeoutMillis: 30000,});
export const query = (sql: string, params?: any[]) => pool.query(sql, params);import { query } from './db';
export const getUser = async (userId: string) => { const { rows } = await query('SELECT * FROM users WHERE id = $1', [userId]); return rows[0];};Enforcement
Section titled “Enforcement”- Static analysis: search for
new Pool(ornew Client(outsidepackages/db. Ensure only the shared module is used. - CodeRabbit rule:
SQL identifier interpolation– verify that dynamic identifiers are validated.
References
Section titled “References”apps/webapp/src/lib/api/base-handler.ts– shows how the shared pool is used for request‑level DB access.packages/db/src/client.ts– the canonical pool implementation.