ADR-002: Pool Management
Estado
Section titled “Estado”Aceptada - 2024 (implementada en Mother container)
Contexto
Section titled “Contexto”El sistema multi-tenant Nostromo (ADR-001) requiere conexiones PostgreSQL eficientes:
- Múltiples tenants: Sistema soporta decenas de clientes simultáneos
- Conexiones costosas: Crear conexión PostgreSQL toma ~50-100ms
- Concurrencia alta: Múltiples requests simultáneos por tenant
- Recursos limitados: PostgreSQL tiene límite de conexiones (~100-200 typical)
Problema
Section titled “Problema”Con schema-per-tenant, cada request necesita:
- Conexión a PostgreSQL
- Switch a schema correcto (
SET search_path) - Ejecutar query
- Liberar conexión
Sin pooling: Crear/destruir conexión por request = latencia inaceptable + agotamiento de conexiones.
Decisión
Section titled “Decisión”Implementar connection pool central con schema switching:
Arquitectura
Section titled “Arquitectura”[Orchestrator Nodes] ↓[Mother Container - PgBouncer/Pool] ↓[PostgreSQL Database] ├── schema: tenant_empresa_123 ├── schema: tenant_empresa_456 └── schema: centralComponentes
Section titled “Componentes”Mother Container (c:\dev\Accounting\orchestrator\src\lib\db\pool.ts):
- Pool central de conexiones (pg-pool)
- Configuración:
max: 50,idleTimeoutMillis: 30000
getTenantPool() function:
async function getTenantPool(tenantId: string) { const pool = await getPool(); // Pool global const client = await pool.connect();
// Switch schema await client.query(`SET search_path TO tenant_${tenantId}`);
return client;}Flujo por request:
- Request llega a Orchestrator
- Resolve tenant desde JWT →
tenantId getTenantPool(tenantId)obtiene conexión del pool- Ejecuta
SET search_pathautomáticamente - Ejecuta queries (ya en schema correcto)
- Libera conexión al pool (schema se resetea)
Consecuencias
Section titled “Consecuencias”Positivas
Section titled “Positivas”✅ Performance excelente
- Conexiones reutilizadas (no overhead de crear/destruir)
- Latencia típica: ~5-10ms para obtener conexión del pool
SET search_pathes rápido (~0.1ms en PostgreSQL)
✅ Escalabilidad
- Pool maneja concurrencia automáticamente
- 50 conexiones en pool soportan ~500-1000 req/s (con queries rápidas)
- Si pool agota, requests esperan (queue) en vez de fallar
✅ Gestión centralizada
- Single point of configuration (max connections, timeouts)
- Monitoring de pool health en un solo lugar
- Facilita tuning de performance
✅ Resource efficiency
- 50 conexiones compartidas entre todos los tenants
- Sin pool: necesitarías N tenants × M conexiones = explosión de recursos
- Con pool: recursos constantes sin importar número de tenants
✅ Compatibilidad
- Funciona con transacciones (
BEGIN/COMMIT) - Compatible con prepared statements
- No requiere cambios en queries
Negativas (Trade-offs)
Section titled “Negativas (Trade-offs)”❌ Schema switching overhead
- Cada request ejecuta
SET search_path(~0.1ms) - Overhead acumulado: 0.1ms × 1000 req/s = 100ms/s overhead
- Mitigación: Overhead es mínimo comparado con beneficio de pooling
❌ Pool exhaustion afecta a todos
- Si 1 tenant hace queries lentas, agota pool → otros tenants esperan
- Mitigación: Timeouts configurados (
statement_timeout,idle_in_transaction_session_timeout) - Mitigación: Monitoring de query time, disconnect clientes lentos
❌ Complejidad de debugging
- Pool oculta conexiones subyacentes
- Difícil rastrear qué tenant usa qué conexión física
- Mitigación: Logging de
tenantId+ query en cada request
❌ Schema leakage risk
- Si no se resetea schema entre usos, tenant A podría acceder a schema de tenant B
- Mitigación:
pg-poolresetea session state automáticamente al liberar conexión - Mitigación: Test de aislamiento en suite de tests
❌ Connection limits
- PostgreSQL tiene límite duro (ej:
max_connections = 100) - Pool no puede exceder ese límite
- Mitigación: PgBouncer como pool externo si se requiere más conexiones
Alternativas Consideradas
Section titled “Alternativas Consideradas”Opción A: Pool-per-tenant
Section titled “Opción A: Pool-per-tenant”Descripción: Cada tenant tiene su propio pool dedicado.
Ejemplo:
const pools = { tenant_123: new Pool({ max: 10 }), tenant_456: new Pool({ max: 10 }), // ...};Rechazada porque:
- Resource explosion: 50 tenants × 10 conexiones = 500 conexiones totales
- Pool overhead: Gestionar 50 pools es complejo
- Ineficiente: Tenants pequeños tienen pool ocioso, grandes saturan su pool
- No escala: Agregar tenant = crear pool nuevo
Cuándo sería válida: Si cada tenant tiene DB dedicada (no schema-per-tenant), o requisitos de resource isolation extremo.
Opción B: Serverless connections (RDS Proxy, Neon)
Section titled “Opción B: Serverless connections (RDS Proxy, Neon)”Descripción: Usar proxy que gestiona conexiones serverless.
Rechazada porque:
- Vendor lock-in: RDS Proxy es AWS-only, Neon es servicio externo
- Cold start latency: Conexiones serverless tienen latency inicial (~50-100ms)
- Costo: Servicios serverless suelen ser más caros que self-managed pool
- Overkill: Para aplicación con tráfico constante, pool tradicional es suficiente
Cuándo sería válida: Si deployment es serverless (AWS Lambda, Vercel Edge), o tráfico es muy esporádico (minutos/horas entre requests).
Opción C: No pooling (conexión por request)
Section titled “Opción C: No pooling (conexión por request)”Descripción: Crear conexión nueva por cada request, destruir al terminar.
Rechazada porque:
- Latencia inaceptable: 50-100ms por request solo en conexión
- Agotamiento de conexiones: PostgreSQL limita a ~100-200 conexiones concurrentes
- CPU overhead: Crear/destruir conexiones consume CPU en cliente y servidor
- No viable para producción
Cuándo sería válida: Scripts batch con 1-2 queries totales, o ambiente dev local.
Referencias
Section titled “Referencias”Implementación:
- Pool Management - Pool central y
getTenantPool() - Tenant Resolver - Resolución de tenant
- Mother Container - Setup PostgreSQL
Decisiones relacionadas:
- ADR-001: Multi-Tenant Strategy - Por qué schema-per-tenant
- Orchestrator Monitoring - Métricas de pool
Referencias externas:
- node-postgres Pool - Documentación oficial
- PgBouncer - Connection pooler externo
- PostgreSQL Connection Pooling Best Practices
Notas Adicionales
Section titled “Notas Adicionales”Configuración actual
Section titled “Configuración actual”Pool Size: max: 50
- Basado en: ~500-1000 req/s esperados, queries promedio ~10-50ms
- Formula aproximada:
pool_size = expected_rps × avg_query_time - Tuning futuro según métricas reales
Timeouts:
{ connectionTimeoutMillis: 5000, // Esperar max 5s por conexión idleTimeoutMillis: 30000, // Cerrar conexión idle después 30s max: 50, // Máximo 50 conexiones min: 10 // Mínimo 10 conexiones (warm)}Monitoring
Section titled “Monitoring”Métricas actuales:
- Pool size (active/idle/waiting)
- Query duration por tenant
- Connection errors (timeouts, pool exhaustion)
Dashboards:
- Grafana panel con pool health
- Alertas si
waiting_clients > 10(pool bajo presión)
Performance Tuning
Section titled “Performance Tuning”Lecciones aprendidas:
min: 10mantiene warm pool: Evita cold start en requests inicialesstatement_timeout: 30s: Previene queries runaway que agoten pool- Connection recycling:
pg-poolrecicla conexiones automáticamente cada hora (previene leaks)
Escalado futuro
Section titled “Escalado futuro”Si pool actual no es suficiente:
- PgBouncer: Proxy externo, soporta miles de conexiones virtuales
- Read replicas: Queries SELECT van a réplica, solo writes a primary
- Sharding: Distribuir tenants a múltiples PostgreSQL servers
Trigger para upgrade: Si waiting_clients promedio > 20 durante horas pico.