--- id: backend-connection-handling title: Connection Handling — Pool / Reuse / Timeout category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [backend, connection, pool, vibe-coding] tech_stack: { language: "TS", applicable_to: ["Backend"] } applied_in: [] aliases: [HTTP keep-alive, agent, pgbouncer, connection pool, idle timeout, EADDRINUSE] --- # Connection Handling > 매 request 새 connection = 느림 + resource. **Pool + keep-alive + 적절 timeout**. Database / HTTP / Redis / TCP 모두 적용. Lambda / serverless 는 다른 패턴. ## 📖 핵심 개념 - Pool: 미리 N 개 connection 보관. - Keep-alive: 같은 conn 다중 request. - Idle timeout: 안 쓰면 close. - Acquire timeout: pool exhausted 시 wait. ## 💻 코드 패턴 ### HTTP keep-alive (Node fetch) ```ts import { Agent } from 'undici'; const agent = new Agent({ keepAliveTimeout: 60_000, keepAliveMaxTimeout: 600_000, connections: 100, // per host pipelining: 10, }); import { fetch as undiciFetch } from 'undici'; const r = await undiciFetch('https://api.example.com', { dispatcher: agent }); ``` → 매 request 가 같은 TCP 재사용. ### Native fetch (Node 18+) ```ts // 자동 keep-alive (default). // 그러나 default agent — 작은 limit. // 큰 throughput = undici Agent. ``` ### Postgres pool (pg) ```ts import { Pool } from 'pg'; const pool = new Pool({ connectionString: process.env.DB_URL, max: 20, // max connections min: 2, // 항상 ready idleTimeoutMillis: 30_000, // 30s 후 close connectionTimeoutMillis: 5_000, // 5s acquire timeout statement_timeout: 30_000, // 30s query timeout query_timeout: 30_000, application_name: 'my-app', }); // 사용 const client = await pool.connect(); try { const r = await client.query('SELECT * FROM users'); } finally { client.release(); } // 또는 단일 query const r = await pool.query('SELECT * FROM users'); ``` ### Postgres pool — Drizzle / Prisma ```ts // Drizzle import { drizzle } from 'drizzle-orm/node-postgres'; const db = drizzle(pool); // Prisma const prisma = new PrismaClient({ datasources: { db: { url: process.env.DB_URL } }, }); // Prisma 자체 pool 관리 — connection_limit URL param // postgresql://...?connection_limit=20 ``` ### PgBouncer (외부 connection pool) ```ini # pgbouncer.ini [databases] app = host=primary-db port=5432 dbname=app [pgbouncer] pool_mode = transaction # transaction / session / statement max_client_conn = 1000 # client → pgbouncer default_pool_size = 25 # pgbouncer → DB reserve_pool_size = 5 server_idle_timeout = 600 ``` → 1000 client → 25 DB connection. PG 의 connection limit 회피. ### Pool 크기 결정 ``` 규칙: max = (CPU cores × 2) + effective_spindles 일반 DB: 10-30 connections per app instance 큰 cluster: pgbouncer 로 multiplexing 너무 큼: DB OOM, lock contention 너무 적음: queue 늘어남, 502 ``` ### Acquire timeout 처리 ```ts try { const client = await pool.connect(); // ... } catch (e) { if (e.message.includes('connection terminated')) { // DB down — circuit breaker } if (e.code === 'ETIMEDOUT') { // Pool exhausted — overload return res.status(503).end(); } throw e; } ``` ### Connection leak detection ```ts // 모든 connection 사용 중 + acquire 무한 wait pool.on('error', (err, client) => { log.error('pool error', err); }); // 주기적 stats setInterval(() => { log.info('pool stats', { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, }); }, 30_000); ``` → waiting > 0 자주 = pool 부족 / leak. ### Lambda / serverless 패턴 ``` Lambda 매 invocation = 새 container 가능. → Pool 의 의미 적음. 해결: 1. Connection 가까이 — RDS Proxy / Hyperdrive (CF) 2. HTTP 기반 DB driver (Neon, Supabase) 3. 재사용 (warm container) ``` ```ts // Lambda — global scope (warm reuse) import { Pool } from 'pg'; let pool: Pool | null = null; export const handler = async (event) => { if (!pool) pool = new Pool({ max: 1, ...config }); const r = await pool.query('SELECT * FROM users WHERE id = $1', [event.id]); return r.rows[0]; }; ``` → Same container 재사용 시 pool 재사용. ### Neon / Supabase HTTP driver ```ts import { neon } from '@neondatabase/serverless'; const sql = neon(process.env.DATABASE_URL); const users = await sql`SELECT * FROM users WHERE id = ${id}`; ``` → HTTP — connection pool 불필요. Edge / serverless 친화. ### Redis pool ```ts import Redis from 'ioredis'; const redis = new Redis({ host: 'redis', port: 6379, maxRetriesPerRequest: 3, enableReadyCheck: true, lazyConnect: false, // 자동 connection pool — 한 instance OK }); // Cluster const cluster = new Redis.Cluster([{ host: 'r1' }, { host: 'r2' }], { scaleReads: 'slave', }); ``` ### MySQL pool (mysql2) ```ts import mysql from 'mysql2/promise'; const pool = mysql.createPool({ host: 'db', user: 'app', database: 'app', connectionLimit: 20, queueLimit: 0, enableKeepAlive: true, keepAliveInitialDelay: 10000, }); ``` ### HTTP outbound — global agent ```ts import http from 'node:http'; import https from 'node:https'; // Default agents http.globalAgent.keepAlive = true; http.globalAgent.maxSockets = 100; https.globalAgent.keepAlive = true; https.globalAgent.maxSockets = 100; ``` ### Connection retry / circuit breaker ```ts import { CircuitBreaker } from 'opossum'; const breaker = new CircuitBreaker(async (id: string) => { return await fetch(`http://upstream/users/${id}`); }, { timeout: 5000, errorThresholdPercentage: 50, resetTimeout: 30000, }); const r = await breaker.fire('u1'); ``` ### Idle timeout 의 함정 ``` NAT / load balancer 가 60s+ idle conn close. App 의 pool idle 60s+ 면 — stale conn. 해결: - pool idle < LB idle (e.g. 30s pool, 60s LB). - 또는 ping every N seconds. ``` ### Database connection death (zombie) ```ts // PG 가 connection 살아있다 가정 — 사실 dead pool.on('connect', (client) => { client.query('SET application_name = "my-app"'); }); // Health check 시 ping async function checkPool() { try { await pool.query('SELECT 1'); } catch { // Pool 재시작 } } ``` ### EADDRINUSE / port 재사용 ```ts // 빠른 restart 시 port still LISTEN server.listen(3000, () => { ... }); server.on('error', (err) => { if (err.code === 'EADDRINUSE') { log.error('port 3000 in use'); process.exit(1); } }); // SO_REUSEPORT (Linux) — 여러 process 같은 port // Node cluster 자동. ``` ### 측정 ``` PostgreSQL stats: SELECT count(*) FROM pg_stat_activity WHERE state = 'active'; SELECT count(*) FROM pg_stat_activity WHERE state = 'idle in transaction'; ``` ```ts // App level metrics.gauge('pool.total', pool.totalCount); metrics.gauge('pool.idle', pool.idleCount); metrics.gauge('pool.waiting', pool.waitingCount); ``` ## 🤔 의사결정 기준 | 환경 | 추천 | |---|---| | 일반 server | Pool (10-30) | | Serverless | HTTP driver / RDS Proxy | | Edge | Neon / Hyperdrive | | 1000+ client | PgBouncer | | HTTP outbound | undici Agent | | Redis | ioredis (자체 pool) | ## ❌ 안티패턴 - **매 request 새 connection**: 슬로우. - **Pool 너무 큰 (100+)**: DB OOM. - **Pool 너무 작은 (3)**: queue. - **Idle timeout 길음 LB 보다**: zombie conn. - **Lambda 새 client 매번**: 재사용 X — global scope. - **Connection release 안 함**: leak. - **Statement timeout 없음**: hang. ## 🤖 LLM 활용 힌트 - Pool max = (CPU × 2) + 적절. - Idle timeout < LB idle. - Lambda = HTTP driver / RDS Proxy. - Stats 모니터링 — waiting > 0 alarm. ## 🔗 관련 문서 - [[DB_Connection_Pool]] - [[Backend_Service_Discovery]] - [[DB_Lock_Analysis]]