PostgreSQL Connection Pooling en 2026: PgBouncer, pgpool-II y mecanismos integrados bajo carga
Introducción: por qué cada conexión a PostgreSQL tiene un coste elevado
PostgreSQL gestiona cada conexión mediante un proceso independiente del sistema operativo (modelo forked process). Al establecer una conexión, el núcleo del SGBD hace un fork del proceso, asigna una pila (~8 MB por defecto), inicializa las estructuras de memoria compartida y carga los catálogos del sistema. Incluso una conexión "vacía" consume entre 5 y 10 MB de RAM y ocupa un slot en el array ProcArray.
El parámetro max_connections limita el número total de conexiones simultáneas. Aumentarlo no es una operación gratuita: con max_connections = 1000, PostgreSQL reserva aproximadamente 80 MB de memoria compartida solo para el array de bloqueos (max_locks_per_transaction * max_connections * 2). Además, el planificador debe recorrer todos los procesos activos al tomar un snapshot MVCC, lo que supone una operación O(N) por cada consulta.
En la práctica, una aplicación con 50 workers, cada uno manteniendo un pool de 10 conexiones, acumula 500 conexiones reales hacia PostgreSQL. En una arquitectura de microservicios con 20 servicios, esa cifra asciende a 10 000, lo que hace inestable al SGBD incluso en hardware estándar. Aquí es donde entra en juego el connection pooling en PostgreSQL.
Modos de pooling: Session, Transaction, Statement
Los poolers de conexiones operan en tres modos, y la elección del modo determina la compatibilidad con las funcionalidades de PostgreSQL.
Session pooling
La conexión del pool se asigna al cliente durante toda su sesión y solo se devuelve tras la desconexión completa del cliente. Es el modo más seguro: admite SET, LISTEN/NOTIFY, prepared statements y advisory locks. Sin embargo, no existe multiplexación real: el número de clientes no puede superar el tamaño del pool.
Transaction pooling
La conexión se devuelve al pool tras finalizar la transacción (COMMIT / ROLLBACK). Esto permite que 1000 clientes utilicen eficientemente 50 conexiones reales, siempre que no mantengan transacciones abiertas. Restricciones: no se puede usar SET SESSION, advisory locks a nivel de sesión, LISTEN ni prepared statements del lado del servidor (por defecto).
Statement pooling
La conexión se devuelve tras cada instrucción individual. Es el modo más agresivo: no soporta transacciones multisentencia. Se utiliza de forma muy puntual, principalmente en escenarios analíticos de solo lectura.
Comparativa de modos por parámetros clave:
- Session: multiplexación — no; prepared statements — sí; transacciones — sí; LISTEN/NOTIFY — sí.
- Transaction: multiplexación — sí; prepared statements — limitado; transacciones — sí; LISTEN/NOTIFY — no.
- Statement: multiplexación — máxima; prepared statements — no; transacciones — no; LISTEN/NOTIFY — no.
PgBouncer: instalación, configuración y uso práctico
PgBouncer es un pooler ligero de un solo hilo, escrito en C con libevent. En 2026, la rama activa es la 1.23+, que incluye soporte para SCRAM-SHA-256 y una pila TLS mejorada.
Instalación
# Debian/Ubuntu
apt-get install pgbouncer
# O mediante Docker
docker run -d \
--name pgbouncer \
-e DATABASE_URL="postgres://user:pass@pg-host:5432/mydb" \
-p 5432:5432 \
edoburu/pgbouncer:latest
Configuración principal pgbouncer.ini
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
; Modo de pool — transaction para alta carga
pool_mode = transaction
; Máximo de conexiones hacia PostgreSQL
max_client_conn = 10000
default_pool_size = 50
; Pool de reserva para picos
reserve_pool_size = 10
reserve_pool_timeout = 5
; Tiempos de espera
server_idle_timeout = 600
client_idle_timeout = 0
query_timeout = 0
; Logging
log_connections = 0
log_disconnections = 0
log_pooler_errors = 1
stats_period = 60
; Interfaz de administración
admin_users = pgbouncer_admin
Archivo de autenticación userlist.txt
"myuser" "SCRAM-SHA-256$4096:base64salt==:base64storedkey==:base64serverkey=="
"pgbouncer_admin" "adminpassword"
El problema de los prepared statements en transaction mode
Con pool_mode = transaction, los prepared statements del lado del servidor de PostgreSQL (PREPARE / EXECUTE) no funcionan, ya que están vinculados a la sesión. Solución: desactivar los prepared statements a nivel del driver o usar PgBouncer 1.21+ con el parámetro max_prepared_statements, que habilita el seguimiento y la redirección de los prepared statements.
; pgbouncer.ini — habilitación del seguimiento de prepared statements del servidor
max_prepared_statements = 200
En Go (pgx), para compatibilidad con transaction pooling, usa QueryExecModeSimpleProtocol o almacena en caché las consultas del lado del cliente mediante la caché de extended query de pgx/v5.
pgpool-II: balanceo, replicación y complejidad justificada
pgpool-II es un middleware multifuncional: pooler, balanceador de carga, gestor de replicación y caché de consultas. A diferencia de PgBouncer, es multiproceso y soporta el protocolo completo de PostgreSQL.
Funcionalidades clave de pgpool-II
- Load balancing: distribución automática de consultas SELECT entre réplicas (servidores standby).
- Connection pooling: similar a PgBouncer, pero con soporte completo del protocolo.
- Watchdog: clúster de HA con varias instancias de pgpool-II y una IP virtual.
- Online recovery: reconexión automática de nodos tras un fallo.
- Query cache: caché en memoria de resultados SELECT (raramente usado en producción debido a la invalidación).
Configuración mínima pgpool.conf para balanceo
listen_addresses = '*'
port = 5433
# Primario
backend_hostname0 = 'pg-primary'
backend_port0 = 5432
backend_weight0 = 1
backend_data_directory0 = '/var/lib/postgresql/data'
backend_flag0 = 'ALLOW_TO_FAILOVER'
# Réplica
backend_hostname1 = 'pg-replica'
backend_port1 = 5432
backend_weight1 = 2
backend_data_directory1 = '/var/lib/postgresql/data'
backend_flag1 = 'ALLOW_TO_FAILOVER'
load_balance_mode = on
master_slave_mode = on
master_slave_sub_mode = 'stream'
num_init_children = 100
max_pool = 4
Cuándo está justificado pgpool-II
pgpool-II se justifica cuando se necesita balanceo automático del tráfico de lectura entre réplicas sin modificar el código de la aplicación, failover a nivel de middleware y watchdog para HA. En el resto de casos, su complejidad es excesiva: PgBouncer rinde mejor con menor consumo de recursos.
Pooling integrado en drivers: pgx (Go) y Laravel
Pool de conexiones pgx (Go)
La librería pgx/v5 para Go incluye pgxpool, un pool de conexiones thread-safe a nivel de aplicación. Es un pooling del lado del cliente: las conexiones no se multiplexan entre goroutines, pero se reutilizan dentro del mismo proceso.
package main
import (
"context"
"os"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
config, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
if err != nil {
panic(err)
}
// Configuración del pool
config.MaxConns = 25
config.MinConns = 5
config.MaxConnLifetime = 3600 * time.Second
config.MaxConnIdleTime = 600 * time.Second
config.HealthCheckPeriod = 60 * time.Second
// Para trabajar con PgBouncer en transaction mode
config.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeSimpleProtocol
pool, err := pgxpool.NewWithConfig(context.Background(), config)
if err != nil {
panic(err)
}
defer pool.Close()
}
Limitación: cada Pod en Kubernetes tiene su propio pool; con 10 réplicas y MaxConns = 25, PostgreSQL recibe 250 conexiones reales. Sin un pooler externo, esto puede ser crítico.
Pool de PostgreSQL en Laravel
Laravel utiliza PDO, que no soporta connection pooling persistente en el sentido clásico. Para Laravel se recomienda enrutar el tráfico a través de PgBouncer. La configuración de config/database.php permanece estándar; la aplicación no tiene conocimiento del pooler:
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', 'pgbouncer-svc'), // dirección de PgBouncer
'port' => env('DB_PORT', '6432'),
'database' => env('DB_DATABASE', 'mydb'),
'username' => env('DB_USERNAME', 'myuser'),
'password' => env('DB_PASSWORD', ''),
'options' => [
// Desactivamos prepared statements para transaction mode
PDO::ATTR_EMULATE_PREPARES => true,
],
],
Punto crítico: con pool_mode = transaction en PgBouncer, es imprescindible establecer PDO::ATTR_EMULATE_PREPARES => true; de lo contrario, PDO usará prepared statements del lado del servidor de PostgreSQL, lo que provocará el error prepared statement does not exist.
Despliegue de PgBouncer como sidecar en Kubernetes
En Kubernetes, el patrón más fiable es desplegar PgBouncer como contenedor sidecar dentro del Pod de la aplicación. Esto elimina el salto de red y simplifica la gestión del ciclo de vida.
Manifiesto de Kubernetes: Deployment con sidecar de PgBouncer
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
volumes:
- name: pgbouncer-config
configMap:
name: pgbouncer-config
- name: pgbouncer-secrets
secret:
secretName: pgbouncer-secrets
containers:
- name: app
image: myapp:latest
env:
- name: DATABASE_URL
value: "postgres://myuser@localhost:6432/mydb"
- name: pgbouncer
image: pgbouncer/pgbouncer:1.23.0
ports:
- containerPort: 6432
volumeMounts:
- name: pgbouncer-config
mountPath: /etc/pgbouncer
- name: pgbouncer-secrets
mountPath: /etc/pgbouncer/secrets
readOnly: true
resources:
requests:
cpu: "50m"
memory: "32Mi"
limits:
cpu: "200m"
memory: "128Mi"
livenessProbe:
tcpSocket:
port: 6432
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
exec:
command:
- sh
- -c
- |
psql "postgres://pgbouncer_admin:${ADMIN_PASS}@localhost:6432/pgbouncer" \
-c "SHOW VERSION" > /dev/null 2>&1
initialDelaySeconds: 5
periodSeconds: 10
env:
- name: ADMIN_PASS
valueFrom:
secretKeyRef:
name: pgbouncer-secrets
key: admin_password
ConfigMap para pgbouncer.ini
apiVersion: v1
kind: ConfigMap
metadata:
name: pgbouncer-config
namespace: production
data:
pgbouncer.ini: |
[databases]
mydb = host=pg-primary.postgres.svc.cluster.local port=5432 dbname=mydb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/secrets/userlist.txt
pool_mode = transaction
max_client_conn = 500
default_pool_size = 20
reserve_pool_size = 5
server_idle_timeout = 300
log_connections = 0
log_disconnections = 0
admin_users = pgbouncer_admin
Secret con userlist.txt
apiVersion: v1
kind: Secret
metadata:
name: pgbouncer-secrets
namespace: production
type: Opaque
stringData:
userlist.txt: |
"myuser" "md5hash_or_scram_verifier"
"pgbouncer_admin" "adminpassword"
admin_password: "adminpassword"
Importante: con el patrón sidecar, listen_addr = 127.0.0.1 — PgBouncer solo es accesible desde dentro del Pod, lo que impide accesos no autorizados desde el exterior.
Benchmarks: pgbench bajo carga
Entorno de prueba: PostgreSQL 16.3, 8 vCPU, 32 GB RAM, SSD NVMe. Escenario tipo TPC-B con pgbench, 100 clientes, 60 segundos, scale factor 100.
Escenario 1: sin pooling, conexiones directas
pgbench -h pg-host -p 5432 -U myuser -d mydb \
-c 100 -j 4 -T 60 -P 10
# Resultado:
# TPS (sin establecimiento de conexión): 3 241
# Latencia media: 30,8 ms
# Tiempo de conexión: 12,3 ms (media)
Escenario 2: a través de PgBouncer, transaction mode, pool_size=50
pgbench -h pgbouncer-host -p 6432 -U myuser -d mydb \
-c 100 -j 4 -T 60 -P 10
# Resultado:
# TPS (sin establecimiento de conexión): 8 947
# Latencia media: 11,2 ms
# Tiempo de conexión: 0,4 ms (media)
Escenario 3: a través de pgpool-II, balanceo desactivado
pgbench -h pgpool-host -p 5433 -U myuser -d mydb \
-c 100 -j 4 -T 60 -P 10
# Resultado:
# TPS (sin establecimiento de conexión): 6 183
# Latencia media: 16,2 ms
# Tiempo de conexión: 1,8 ms (media)
Conclusiones de los benchmarks:
- PgBouncer en transaction mode ofrece un incremento de TPS de 2,76x respecto a las conexiones directas, gracias a la eliminación del overhead de establecimiento de conexión.
- pgpool-II sin balanceo muestra un incremento de 1,91x, inferior a PgBouncer por su arquitectura multiproceso.
- Con balanceo hacia una réplica de lectura activado, pgpool-II con el 70% de tráfico SELECT alcanza un TPS total de ~14 000, inalcanzable en un escenario de un solo nodo.
Errores de configuración habituales y sus síntomas
1. Connection leaks
Síntoma: el contador cl_waiting en SHOW POOLS crece constantemente. Causa: la aplicación no devuelve las conexiones al pool (transacciones no cerradas, excepciones sin rollback). Diagnóstico:
-- En psql hacia la base de datos administrativa de PgBouncer
SHOW CLIENTS;
-- Buscar clientes con estado 'active' sin actividad reciente
-- O mediante pg_stat_activity en el lado de PostgreSQL
SELECT pid, usename, state, query_start, state_change, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND state_change < NOW() - INTERVAL '5 minutes';
2. Error prepared statement does not exist
Síntoma: ERROR: prepared statement "s1" does not exist. Causa: el driver usa el protocolo extended query con prepared statements del servidor con pool_mode = transaction. Solución: PDO::ATTR_EMULATE_PREPARES = true para Laravel, QueryExecModeSimpleProtocol para pgx, o habilitar max_prepared_statements en PgBouncer ≥ 1.21.
3. Pool exhaustion
Síntoma: ERROR: no more connections allowed (max_client_conn) o clientes en cola indefinida. Causa: max_client_conn insuficiente o default_pool_size demasiado pequeño. Solución: aumentar reserve_pool_size, configurar client_login_timeout y verificar la existencia de transacciones largas que bloqueen la devolución de conexiones.
4. Healthcheck incorrecto en Kubernetes
Síntoma: el Pod se reinicia de forma caótica. Causa: la readinessProbe verifica el puerto TCP, pero PgBouncer ya está escuchando en el puerto aunque aún no esté listo para servir peticiones. Utiliza SHOW VERSION mediante psql como comando de healthcheck (ver ejemplo en el manifiesto anterior).
5. Comandos SET con transaction pooling
Síntoma: los ajustes de sesión (SET search_path, SET work_mem) no se conservan entre consultas. Causa: la sesión se reasigna tras cada transacción. Solución: usar server_reset_query = DISCARD ALL y definir los parámetros en la cadena de conexión (options=-c search_path=myschema) o mediante ALTER ROLE ... SET.
Monitoreo del pool: SHOW POOLS, SHOW STATS y Prometheus
Diagnóstico integrado de PgBouncer
-- Conectarse a la base de datos administrativa de PgBouncer
psql -h localhost -p 6432 -U pgbouncer_admin pgbouncer
-- Estado de los pools
SHOW POOLS;
-- Columnas: database, user, cl_active, cl_waiting, sv_active,
-- sv_idle, sv_used, sv_tested, sv_login, maxwait
-- Estadísticas de tráfico
SHOW STATS;
-- total_xact_count, total_query_count, total_received,
-- total_sent, total_xact_time, avg_xact_time, avg_query_time
-- Lista de clientes
SHOW CLIENTS;
-- Lista de conexiones al servidor
SHOW SERVERS;
-- Configuración
SHOW CONFIG;
-- Recarga de configuración sin reinicio
RELOAD;
Integración con Prometheus
Utiliza pgbouncer_exporter de Prometheus Community. Agrégalo como contenedor sidecar adicional:
- name: pgbouncer-exporter
image: prometheuscommunity/pgbouncer-exporter:v0.9.0
args:
- --pgBouncer.connectionString=postgresql://pgbouncer_admin:$(ADMIN_PASS)@localhost:6432/pgbouncer
ports:
- containerPort: 9127
name: metrics
env:
- name: ADMIN_PASS
valueFrom:
secretKeyRef:
name: pgbouncer-secrets
key: admin_password
Métricas clave para alertas:
pgbouncer_pools_cl_waiting— clientes en cola. Alerta si el valor es > 10 durante más de 1 minuto.pgbouncer_pools_sv_idle— conexiones de servidor en estado idle. Un valor bajo bajo alta carga indica escasez de pool.pgbouncer_stats_avg_query_time— latencia media de las consultas.pgbouncer_pools_maxwait— tiempo máximo de espera en segundos. Alerta si > 1.
Ejemplo de regla de alerta en Prometheus
groups:
- name: pgbouncer
rules:
- alert: PgBouncerPoolExhausted
expr: pgbouncer_pools_cl_waiting > 10
for: 1m
labels:
severity: critical
annotations:
summary: "PgBouncer pool exhausted"
description: "{{ $value }} clients waiting in pool {{ $labels.database }}"
- alert: PgBouncerHighWaitTime
expr: pgbouncer_pools_maxwait > 1
for: 30s
labels:
severity: warning
annotations:
summary: "High wait time in PgBouncer"
Recomendaciones para elegir estrategia según la arquitectura
Monolito o servicio pequeño (hasta 5 réplicas)
Usa el pool integrado del driver (pgxpool para Go, pool de conexiones estándar de Laravel mediante PDO). Un pooler externo añade latencia y complejidad sin una ganancia significativa con pocas conexiones. MaxConns óptimo: número de workers × 2–4.
Microservicios en Kubernetes (10+ servicios, 3+ réplicas cada uno)
Despliega PgBouncer como sidecar en cada Pod con pool_mode = transaction. Esto reduce el número real de conexiones a PostgreSQL de miles a decenas. Configura default_pool_size = 10–20 por Pod y establece max_connections de PostgreSQL entre 200 y 500.
Servicios de alto tráfico con predominio de lecturas (replicación en streaming, varias réplicas)
pgpool-II está justificado para el balanceo automático de consultas SELECT entre réplicas sin modificar el código. Alternativa: HAProxy o Patroni con configuración de Service en Kubernetes para separar el tráfico de lectura y escritura a nivel de red.
OLAP / consultas analíticas
Session pooling o directamente sin pooling: las consultas analíticas largas no se benefician de la multiplexación de transacciones y pueden sufrir el overhead del pooler al trabajar con CURSOR y COPY.
Aplicaciones Laravel en Kubernetes
Laravel con PHP-FPM no mantiene conexiones persistentes entre peticiones: cada worker de PHP abre una conexión al iniciarse y la cierra (o la mantiene según la configuración). PgBouncer como sidecar o como Deployment independiente (uno por namespace) es un componente imprescindible de la arquitectura con más de 20 workers por Pod.
Principios generales para calcular el tamaño del pool
- Regla empírica de Brandt:
pool_size = (número de núcleos de CPU × 2) + número de discosen el lado de PostgreSQL. max_connectionsreal de PostgreSQL =default_pool_size × número de Pods de la aplicación + superuser_reserved_connections.- Comienza con valores conservadores y auméntalos basándote en las métricas
maxwaitycl_waiting.
El connection pooling no es una solución mágica. Resuelve el problema del overhead en el establecimiento de conexiones, pero no sustituye la optimización de consultas, una indexación adecuada ni una gestión correcta de las transacciones. El monitoreo del pool debe ser parte del baseline de observabilidad de cualquier sistema PostgreSQL en producción.
Tecnologías
Etiquetas
Ruslan Ismailov
Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →