--- id: db-connection-pooling-patterns title: Connection Pooling β€” PgBouncer / Pool / Statement category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [database, pool, vibe-coding] tech_stack: { language: "TS / Postgres", applicable_to: ["Backend"] } applied_in: [] aliases: [PgBouncer, connection pool, pool mode, statement pool, transaction pool, RDS Proxy] --- # Connection Pooling Patterns > Postgres connection κ°€ expensive. **App pool (small) + PgBouncer (transaction pool, 1000+ client)**. Lambda / serverless = HTTP driver / RDS Proxy. ## πŸ“– 핡심 κ°œλ… - App pool: λ§€ process κ°€ N connection. - External pool: PgBouncer κ°€ multiplex. - Pool mode: session / transaction / statement. - Limit: Postgres 의 max_connections. ## πŸ’» μ½”λ“œ νŒ¨ν„΄ ### App pool (간단) ```ts import { Pool } from 'pg'; const pool = new Pool({ connectionString, max: 20, idleTimeoutMillis: 30_000, connectionTimeoutMillis: 5_000, }); // λͺ¨λ“  query κ°€ pool μ‚¬μš© await pool.query('SELECT * FROM users'); ``` β†’ App instance λ‹Ή N. 큰 traffic = 큰 N β€” Postgres ν•œκ³„. ### Postgres max_connections ```sql SHOW max_connections; -- Default: 100. λ§€ connection ~= 10 MB RAM. -- Heavy production: ALTER SYSTEM SET max_connections = 200; ``` β†’ λ§€ connection 의 cost (memory + process). Limit 있음. ### Pool 크기 κ²°μ • ``` κ·œμΉ™ (basic): max = (CPU μ½”μ–΄ Γ— 2) + effective_spindle DB 4 core SSD: - μž‘μ€: 5-10 per app instance - 일반: 20-30 - 큰: 50 App instance Γ— pool max < Postgres max_connections. e.g. 10 instance Γ— 20 = 200 < 250. ``` ### PgBouncer (μ™ΈλΆ€ pool) ```ini # pgbouncer.ini [databases] app = host=primary-db port=5432 dbname=app [pgbouncer] listen_port = 6432 auth_type = md5 auth_file = /etc/pgbouncer/userlist.txt pool_mode = transaction # session / transaction / statement max_client_conn = 1000 # client β†’ pgbouncer default_pool_size = 25 # pgbouncer β†’ Postgres reserve_pool_size = 5 server_idle_timeout = 600 ``` β†’ App κ°€ PgBouncer (port 6432) 호좜. PgBouncer κ°€ Postgres connection multiplex. ### Pool modes ``` Session: - Client κ°€ connection 점유 (release κΉŒμ§€) - λͺ¨λ“  feature OK - Multiplex μ•ˆ 됨 Transaction: - Transaction 끝 λ§ˆλ‹€ release - ~10x more efficient - 일뢀 feature X (prepared statements, advisory locks) Statement: - λ§€ statement 끝 release - κ°€μž₯ efficient - 더 λ§Žμ€ feature X (transactions X) β†’ 보톡 transaction. ``` ### Transaction mode 의 함정 ``` Session-bound features μ•ˆ 됨: - SET (variable) - LISTEN / NOTIFY - Prepared statements (자체 prepare) - Advisory lock (xact 만 OK) - Cursor (WITH HOLD) - Temporary table (보톡) β†’ 일반 query λŠ” OK. ``` ```ts // Workaround: prepared statements off const pool = new Pool({ connectionString: 'postgres://user:pw@pgbouncer:6432/app', // 자체 prepare λΉ„ν™œμ„± }); // node-postgres client.query({ name: 'q', text: '...' }); // pgbouncer transaction mode 깨짐 κ°€λŠ₯ // Use raw query client.query('...'); ``` ### node-postgres + PgBouncer ```ts import postgres from 'postgres'; const sql = postgres(url, { max: 10, prepare: false, // pgbouncer transaction mode }); ``` ```ts // pg const pool = new Pool({ connectionString, // statement_timeout λ§€ connection statement_timeout: 30_000, }); pool.on('connect', (client) => { client.query('SET application_name = "my-app"'); // session 별 }); ``` ### RDS Proxy (AWS) ```ts // Lambda β†’ RDS Proxy β†’ RDS // μžλ™ connection pool + auth + IAM // Same code κ°€ κ·Έλƒ₯ endpoint λ³€κ²½ const pool = new Pool({ host: 'my-proxy.proxy-xxx.rds.amazonaws.com', // ... }); ``` β†’ Lambda + Postgres 의 λ‹΅. ### Hyperdrive (Cloudflare) ```ts // wrangler.toml [[hyperdrive]] binding = "HYPERDRIVE" id = "..." ``` ```ts import postgres from 'postgres'; export default { async fetch(req: Request, env: Env) { const sql = postgres(env.HYPERDRIVE.connectionString); const r = await sql`SELECT * FROM users WHERE id = ${id}`; return Response.json(r); }, }; ``` β†’ Hyperdrive = pool + cache. CF Workers μ—μ„œ 일반 Postgres. ### Neon HTTP driver ```ts import { neon } from '@neondatabase/serverless'; const sql = neon(url); const users = await sql`SELECT * FROM users`; ``` β†’ Connection μ—†μŒ. HTTP request 만. Edge / Lambda μΉœν™”. ### Supabase pooler ``` Supabase κ°€ PgBouncer 자체 host. - session: port 5432 - transaction: port 6543 β†’ App = transaction pool μ‚¬μš© (default). ``` ### Pool stats (λͺ¨λ‹ˆν„°λ§) ```ts setInterval(() => { log.info('pool', { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, }); }, 30_000); ``` β†’ waiting > 0 자주 = pool λΆ€μ‘± / leak. ### PgBouncer 확인 ```sql -- PgBouncer admin \c pgbouncer SHOW pools; -- cl_active / sv_active / cl_waiting SHOW stats; -- requests, queries, etc SHOW clients; SHOW servers; ``` ### Connection leak ```ts // ❌ Release μ•ˆ 함 const client = await pool.connect(); const r = await client.query('SELECT ...'); // λˆ„λ½: client.release() // βœ… try-finally const client = await pool.connect(); try { await client.query('...'); } finally { client.release(); } // λ˜λŠ” pool.query (μžλ™ release) await pool.query('...'); ``` ### Application restart ``` λͺ¨λ“  connection re-create. Pool warm-up: - pre-create min connections at startup - 첫 request κ°€ 빠름 ``` ```ts const pool = new Pool({ min: 5, // μ‹œμž‘ μ‹œ 미리 5개 }); ``` ### Multiple DBs (read / write split) ```ts const writer = new Pool({ connectionString: PRIMARY_URL, max: 20 }); const reader = new Pool({ connectionString: REPLICA_URL, max: 50 }); async function getOrders(userId: string) { return reader.query('SELECT * FROM orders WHERE user_id = $1', [userId]); } async function createOrder(data) { return writer.query('INSERT INTO orders ...', [...]); } ``` β†’ [[DB_Read_Replica_Patterns]]. ### Tenant pool (multi-tenant) ```ts // Approach: per-tenant DB const pools = new Map(); function getPool(tenantId: string): Pool { if (!pools.has(tenantId)) { pools.set(tenantId, new Pool({ ... })); } return pools.get(tenantId)!; } // Cleanup unused tenants ``` β†’ N tenant Γ— pool size = 큰 β€” 주의. ### DB connection 이 κ°€μž₯ λΉ„μ‹Ό μžμ› ``` 1 connection β‰ˆ 10 MB Postgres process. 1000 connection β‰ˆ 10 GB. β†’ Pool size μž‘κ²Œ + multiplex. ``` ### Lambda connection issue ``` Lambda = λ§€ invocation μƒˆ container κ°€λŠ₯. 1000 concurrent Lambda = 1000 connection. ν•΄κ²°: 1. RDS Proxy (AWS) 2. Hyperdrive (CF) 3. Neon HTTP / Serverless 4. Pool λ§€ container reuse (warm Lambda) ``` ### Idle in transaction ```sql -- App κ°€ BEGIN ν›„ μ™ΈλΆ€ 호좜 hang SELECT pid, state, query, query_start FROM pg_stat_activity WHERE state = 'idle in transaction'; -- Auto kill ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s'; ``` β†’ 60s μ•ˆ release μ•ˆ ν•˜λ©΄ cancel. β†’ [[DB_Lock_Analysis]]. ### Statement timeout ```ts // Connection 별 client.query('SET statement_timeout = 30000'); // 30s // λ˜λŠ” connection string postgres://user:pw@host:5432/db?statement_timeout=30000 ``` β†’ Hang query λ°©μ§€. ### Retry on connection error ```ts async function queryWithRetry(query: string, params: any[]): Promise { for (let i = 0; i < 3; i++) { try { return await pool.query(query, params); } catch (e: any) { if (e.code === 'ECONNRESET' || e.code === 'ETIMEDOUT') { await sleep(100 * (i + 1)); continue; } throw e; } } throw new Error('max retries'); } ``` ### Health check ```ts async function dbHealthy(): Promise { try { await Promise.race([ pool.query('SELECT 1'), new Promise((_, reject) => setTimeout(() => reject(), 5000)), ]); return true; } catch { return false; } } ``` ### PgBouncer alternative ``` - pgcat (Rust, modern) - pgpool-II (older, complex) - Supavisor (Supabase) - Odyssey (Yandex) β†’ PgBouncer κ°€ κ°€μž₯ 인기. ``` ### Production setup (typical) ``` App (10 instance) β†’ PgBouncer (3 instance) β†’ Postgres (primary + replicas) App pool: 5-10 / instance PgBouncer: max_client_conn = 1000, pool_size = 25 Postgres: max_connections = 100 ``` β†’ 1000 client β†’ 25 DB connection (40x multiplex). ### Architecture ``` [App] [App] [App] ↓ ↓ ↓ [PgBouncer] ↓ [Postgres primary] ↕ [Postgres replica] ``` ### Cloud manage ``` RDS Proxy: AWS, supports MySQL / Postgres Aurora Serverless v2: auto-scale Neon / Supabase: built-in pool Cloud SQL: external pool 직접 ``` ## πŸ€” μ˜μ‚¬κ²°μ • κΈ°μ€€ | ν™˜κ²½ | μΆ”μ²œ | |---|---| | 일반 server | App pool | | 큰 traffic / λ§Žμ€ instance | PgBouncer | | Lambda | RDS Proxy / Hyperdrive | | Cloudflare Workers | Hyperdrive / Neon HTTP | | Edge | Neon HTTP / Turso | | Production | App + PgBouncer | ## ❌ μ•ˆν‹°νŒ¨ν„΄ - **App instance Γ— pool > Postgres max**: connection 폭발. - **session pool mode + multi-tenant**: 격리 약함. - **Transaction pool + session feature μ‚¬μš©**: 깨짐. - **Pool μ•ˆ release**: leak. - **Long-running transaction**: pool λ‹€ 작음. - **Idle timeout 길음 NAT 보닀**: zombie. - **λͺ¨λ‹ˆν„°λ§ μ—†μŒ**: 점진 λ‹€μš΄. ## πŸ€– LLM ν™œμš© 힌트 - App pool (μž‘κ²Œ) + PgBouncer (multiplex). - Lambda = HTTP driver / proxy. - Transaction mode = default. - Pool stats 항상 monitor. ## πŸ”— κ΄€λ ¨ λ¬Έμ„œ - [[Backend_Connection_Handling]] - [[DB_Connection_Pool]] - [[DB_Lock_Analysis]]