HU

Humano

← Blog

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.

postgresqlconnection-poolinginfraestructura

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:

FrameworkPool defaultNotas
HikariCP (Spring Boot)10maximumPoolSize
Rails ActiveRecord5pool en database.yml
Django0 (sin pool)Abre y cierra por request a menos que uses CONN_MAX_AGE
Go database/sqlIlimitadoSetMaxOpenConns 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 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:

  1. 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
  2. Tiempo de espera por lock: SELECT wait_event_type, wait_event, count(*) FROM pg_stat_activity WHERE state = 'active' GROUP BY 1, 2; — Si ves LWLock, Lock, o BufferPin como eventos frecuentes, las conexiones están compitiendo
  3. Cola del pool de la aplicación: En HikariCP, la métrica hikaricp_connections_pending muestra 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étricaAntesDespués
Conexiones activas en PostgreSQL180-24028-32
Context switches/s en el servidor PG~45,000~6,000
Latencia p50 del servicio320ms85ms
Latencia p99 del servicio4,200ms380ms
Instancias del servicio necesarias248
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

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:

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