17 de marzo de 2026
El pool de conexiones que estaba costando $14K/mes en infraestructura
Un servicio con p99 de 4 segundos y 24 instancias. El problema no era falta de capacidad — era un pool de conexiones mal dimensionado que saturaba PostgreSQL.
Un equipo tiene un servicio de checkout que procesa 1,200 requests por segundo en hora pico. La latencia p99 lleva meses subiendo — hoy está en 4.2 segundos. La respuesta del equipo fue escalar horizontalmente: empezaron con 8 instancias y hoy tienen 24. El billing de AWS pasó de $8K/mes a $22K/mes. La latencia mejoró un poco con cada instancia nueva, pero el p99 sigue por encima de 3 segundos.
El problema nunca fue falta de capacidad de cómputo. Era cómo cada instancia se conectaba a PostgreSQL.
Cómo se forma el cuello de botella
PostgreSQL maneja cada conexión con un proceso dedicado del sistema operativo. Cada proceso consume entre 5MB y 10MB de memoria residente, y el scheduler del kernel paga un costo de context switching proporcional al número de procesos activos. El parámetro max_connections tiene un default de 100, pero en la práctica, un servidor PostgreSQL con 16 cores empieza a degradarse cuando mantiene más de 200 conexiones simultáneas — no por falta de memoria, sino por contención en locks internos y presión en el process scheduler.
La mayoría de los frameworks de aplicación (Spring Boot, Rails, Django, Go con database/sql) abren un pool de conexiones por instancia. El default típico varía:
| Framework | Pool default | Notas |
|---|---|---|
| HikariCP (Spring Boot) | 10 | maximumPoolSize |
| Rails ActiveRecord | 5 | pool en database.yml |
| Django | 0 (sin pool) | Abre y cierra por request a menos que uses CONN_MAX_AGE |
Go database/sql | Ilimitado | SetMaxOpenConns no tiene default; se abre bajo demanda |
Con 24 instancias de una app Spring Boot con HikariCP default, el servicio abre hasta 240 conexiones concurrentes contra PostgreSQL. Si el servidor tiene max_connections = 100 (el default), 140 de esos intentos de conexión quedan en cola o fallan. Si alguien subió max_connections a 300 para "resolver" el error, las conexiones se aceptan pero PostgreSQL paga el costo en contención interna.
Qué pasa con 240 conexiones abiertas
Con 240 conexiones activas en un PostgreSQL con 16 cores:
- El scheduler de Linux rota procesos más frecuentemente. El context switching pasa de ~5,000/s a ~45,000/s
- Las queries compiten por los mismos buffer pool locks. Una query que normalmente toma 2ms empieza a esperar 50-200ms por un lock en una página compartida
- El overhead de
COMMITyWAL writese multiplica porque cada conexión mantiene su propia transacción abierta - El p99 del servicio sube porque las queries se encolan detrás de conexiones que tienen transacciones idle o que están esperando locks
El efecto es contraintuitivo: agregar más instancias de aplicación empeora la latencia en la base de datos, porque cada instancia agrega conexiones que compiten por los mismos recursos del servidor PostgreSQL.
El diagnóstico en 20 minutos
El patrón se confirma con tres métricas:
- Conexiones activas en PostgreSQL:
SELECT count(*) FROM pg_stat_activity WHERE state = 'active';— Si el número es consistentemente mayor que 4x el número de cores del servidor, hay contención - Tiempo de espera por lock:
SELECT wait_event_type, wait_event, count(*) FROM pg_stat_activity WHERE state = 'active' GROUP BY 1, 2;— Si vesLWLock,Lock, oBufferPincomo eventos frecuentes, las conexiones están compitiendo - Cola del pool de la aplicación: En HikariCP, la métrica
hikaricp_connections_pendingmuestra cuántos threads están esperando una conexión libre. Si este número es > 0 en steady state, el pool es demasiado chico para la demanda — pero la solución no es agrandarlo
La intervención
La solución no es subir max_connections ni agrandar el pool. Es poner un connection pooler entre la aplicación y PostgreSQL que multiplexe conexiones.
PgBouncer en modo transacción mantiene un pool fijo de conexiones reales contra PostgreSQL (digamos, 32) y rutea las transacciones de las aplicaciones a través de ese pool. La aplicación cree que tiene 10 conexiones propias, pero PgBouncer las mapea a las 32 conexiones compartidas.
La configuración:
[pgbouncer]
pool_mode = transaction
default_pool_size = 32
max_client_conn = 500
server_idle_timeout = 30
Con default_pool_size = 32 en un PostgreSQL de 16 cores, el servidor nunca ve más de 32 conexiones activas. El ratio de ~2 conexiones reales por core es el rango donde PostgreSQL opera con mínima contención.
Del lado de la aplicación, cada instancia reduce su pool a 4 conexiones — suficiente para cubrir la concurrencia de threads sin crear presión. Con 24 instancias, la aplicación abre 96 conexiones cliente contra PgBouncer, pero PgBouncer las multiplexa en 32 conexiones reales contra PostgreSQL.
El resultado
| Métrica | Antes | Después |
|---|---|---|
| Conexiones activas en PostgreSQL | 180-240 | 28-32 |
| Context switches/s en el servidor PG | ~45,000 | ~6,000 |
| Latencia p50 del servicio | 320ms | 85ms |
| Latencia p99 del servicio | 4,200ms | 380ms |
| Instancias del servicio necesarias | 24 | 8 |
| Costo mensual de infra (servicio + PG) | $22,000 | $8,200 |
Con la contención eliminada, cada instancia procesa más requests por segundo. Se eliminaron 16 instancias manteniendo el mismo throughput de 1,200 RPS.
Cuándo este patrón no aplica
- Si tu base de datos es un servicio gestionado con connection pooling integrado (Aurora Serverless v2 con Data API, Neon con su proxy), el pooler ya existe
- Si tu aplicación tiene menos de 4 instancias, probablemente no estás cerca del punto de contención
- Si la latencia del servicio no correlaciona con el número de conexiones activas en PostgreSQL, el bottleneck está en otro lugar
Cuándo sí
El patrón aparece cuando un servicio escala horizontalmente pero la base de datos relacional no fue diseñada para recibir cientos de conexiones directas. Es especialmente visible en:
- Servicios con Spring Boot + HikariCP (pool default de 10) que escalan a más de 10 instancias
- Aplicaciones Go que no configuran
SetMaxOpenConnsy abren conexiones sin límite - Cualquier setup con más de 3x instancias que conexiones de PostgreSQL por core
Si tu servicio crece en instancias y la latencia de base de datos empeora en lugar de mejorar, el pool de conexiones es el primer lugar donde mirar.
Si ya identificaste que la latencia de tu sistema correlaciona con la carga en la base de datos y quieres un diagnóstico que confirme si el bottleneck está ahí o en otro lugar, podemos revisarlo en una conversación de 15 minutos sin costo.
Referencias
- PostgreSQL Documentation: Connection Establishment — modelo de proceso por conexión
- PostgreSQL Documentation: max_connections — default de 100 y configuración
- PostgreSQL Wiki: Number of Database Connections — por qué menos conexiones suelen ser mejor y reglas para dimensionar
- HikariCP Configuration —
maximumPoolSizedefault de 10 y guía de pool sizing ("About Pool Sizing") - HikariCP: About Pool Sizing — por qué un pool más chico puede tener mejor performance, basado en el análisis de la fórmula
connections = (core_count * 2) + effective_spindle_count - PgBouncer Features — modos de pooling (session, transaction, statement)
- PgBouncer Configuration — parámetros
pool_mode,default_pool_size,max_client_conn - Rails Database Configuration — default de pool size en ActiveRecord
- Django Persistent Connections — comportamiento de
CONN_MAX_AGE - Go database/sql: SetMaxOpenConns — default ilimitado y documentación del API
- PostgreSQL Documentation: pg_stat_activity — queries de diagnóstico usadas en el artículo