¿Cómo funciona el pooling de conexiones y cómo se dimensiona un pool correctamente?
Abrir una conexión a la base de datos es tan costoso que hacerlo por cada request te va a limitar mucho antes que la capacidad real de consultas de la base de datos.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo funciona el pooling de conexiones y cómo se dimensiona un pool correctamente?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Por qué existe el pooling
Abrir una conexión TCP a una base de datos, negociar TLS, autenticarse y hacer que la base de datos cree un proceso o hilo dedicado para esa sesión no es gratis — típicamente cuesta entre unos pocos y varias decenas de milisegundos, y en Postgres en particular cada conexión es un proceso completo del sistema operativo con su propio overhead de memoria, así que las conexiones inactivas no son "gratis de mantener" como podría sugerir un simple descriptor de socket. Si abres y cierras una conexión nueva en cada request, pagas ese costo de inicialización en el camino crítico de cada request, y puedes agotar el límite de conexiones máximas de la base de datos bajo carga mucho antes de agotar su capacidad real de CPU o I/O para ejecutar consultas.
Un pool de conexiones amortiza esto: abre un conjunto fijo de conexiones una sola vez, al iniciar la aplicación, y las entrega a los hilos/requests que las necesitan, devolviéndolas al pool en vez de cerrarlas cuando el trabajo termina. La aplicación toma prestada una conexión, la usa y la devuelve — la conexión en sí permanece abierta hacia la base de datos todo el tiempo.
#Una configuración concreta: HikariCP
HikariCP (el pool por defecto en Spring Boot) es una buena referencia porque sus valores por defecto son deliberadamente conservadores y sus parámetros se mapean claramente a los conceptos que comparte cualquier pool:
spring:datasource:hikari:maximum-pool-size: 20 # tope duro de conexiones concurrentes a la BDminimum-idle: 5 # mantener al menos esta cantidad tibias, incluso inactivasconnection-timeout: 30000 # ms a esperar por una conexión libre antes de lanzar erroridle-timeout: 600000 # ms que puede vivir una conexión inactiva por encima de minimum-idlemax-lifetime: 1800000 # ms antes de retirar y reemplazar una conexiónleak-detection-threshold: 60000 # advierte si una conexión está tomada por más de esto
Cada campo tiene su razón de ser: maximum-pool-size limita cuánta carga puede poner esta instancia de la aplicación sobre la base de datos; connection-timeout decide qué pasa cuando todas las conexiones están ocupadas (fallar rápido con un error claro, en vez de dejar que los requests se encolen en silencio para siempre); max-lifetime fuerza el reciclaje periódico de conexiones para que un balanceador de carga o un failover de base de datos delante de la BD pueda de verdad enrutarte hacia un nodo nuevo en vez de que mantengas una sesión TCP obsoleta para siempre; y leak-detection-threshold detecta el bug de pool más común de todos — código que toma prestada una conexión y olvida devolverla (normalmente un close() faltante en un try/finally, o un camino de excepción que se salta la limpieza).
#Dimensionar el pool: más grande no es mejor
El instinto es poner maximum-pool-size alto — "más conexiones, más throughput." Eso es al revés a partir de un número bastante pequeño, porque una conexión de base de datos se mapea a un hilo/proceso real del sistema operativo en el servidor, y más allá del número de núcleos de CPU realmente disponibles para ejecutar consultas, las conexiones adicionales solo crean contención por los mismos recursos de ejecución: más cambios de contexto, más contención de locks, sin más ejecución paralela real de consultas. La propia guía de dimensionamiento de HikariCP, que refleja la sabiduría de la comunidad sobre "pooling de conexiones en PostgreSQL," da una fórmula inicial muy citada:
tamaño_pool = ((núcleos * 2) + spindles_efectivos)
Para un servidor de base de datos de 8 núcleos con almacenamiento SSD (spindles efectivos ≈ 1), eso da aproximadamente (8 * 2) + 1 = 17, que es por lo que el propio valor por defecto de HikariCP de 10 es deliberadamente modesto en vez de "lo más alto posible." La fórmula es un punto de partida para medir desde ahí, no un dogma — el número correcto depende de cuánto tiempo pasa cada consulta esperando I/O frente a consumiendo CPU, algo que depende de la carga de trabajo específica.
La segunda parte del dimensionamiento, a menudo pasada por alto: N instancias de la aplicación × tamaño de pool por instancia debe mantenerse por debajo de max_connections de la base de datos, con margen para conexiones de administración/replicación y otros servicios. Diez pods escalados horizontalmente, cada uno abriendo un pool de 20, son 200 conexiones apuntando a una base de datos que quizás tope en 100 — la solución ahí normalmente no es reducir a mano el pool de cada pod, sino poner un pooler externo (PgBouncer en modo transacción, por ejemplo) entre la flota y la base de datos, de forma que la base de datos vea un número pequeño y estable de conexiones reales, mientras miles de "conexiones" del lado de la aplicación se multiplexan a través de un conjunto mucho más pequeño de conexiones backend reales de PgBouncer.
#Qué falla en la práctica
| Síntoma | Causa habitual |
|---|---|
Connection is not available, request timed out bajo carga moderada | Pool demasiado pequeño para la concurrencia real, o conexiones con fuga (nunca devueltas) |
| Picos de latencia que correlacionan con los deploys | max-lifetime demasiado largo — conexiones obsoletas sobreviven a un failover de base de datos y fallan en el primer uso |
| CPU de la base de datos al tope, pool de la app "saludable" | Pool bien dimensionado, pero las consultas individuales son lentas/sin índice — el pooling no arregla consultas malas |
FATAL: too many connections en la base de datos | La suma de los pools de todas las instancias de la app supera max_connections; hace falta una capa de pooler, no pools más grandes por instancia |