StackPractices
intermediate Por Mathias Paulenko

Pooling de Conexiones a Base de Datos: Guía Práctica

Guía práctica sobre pooling de conexiones de base de datos: dimensionar pools, manejar timeouts de inactividad, detectar fugas, y configurar HikariCP, PgBouncer y pools nativos en la nube para máximo throughput.

Descripción General

Las conexiones de base de datos son costosas de crear. Cada conexión requiere un handshake TCP, negociación TLS cuando el cifrado está activo, autenticación, asignación de memoria y un fork de proceso backend en el servidor. En un PostgreSQL típico eso cuesta desde unos pocos hasta decenas de milisegundos — multiplicado por cada consulta, destruye el rendimiento bajo carga. El pooling de conexiones reutiliza conexiones establecidas, reduciendo ese overhead a casi cero mientras previene el agotamiento de conexiones.

Esta guía cubre dimensionamiento, configuración, monitoreo y troubleshooting de pools tanto a nivel de aplicación (HikariCP, SQLAlchemy, node-postgres) como proxies de middleware (PgBouncer, ProxySQL).

Cuándo Usar

  • Tu aplicación abre más de 10 conexiones concurrentes a base de datos
  • Ves errores de too many connections bajo carga
  • El tiempo de establecimiento de conexión excede el 5% del tiempo total de consulta
  • Tu servidor de base de datos tiene cientos o miles de conexiones inactivas
  • Ejecutas una arquitectura de microservicios donde cada servicio se conecta a bases compartidas
  • Quieres limitar el uso de recursos de base de datos por aplicación o usuario

Conceptos Clave

ConceptoDescripción
PoolUna colección de conexiones de base de datos reutilizables
Tamaño Mínimo del PoolConexiones mantenidas listas incluso cuando están inactivas
Tamaño Máximo del PoolLímite superior de conexiones que el pool creará
Timeout de ConexiónTiempo de espera para obtener una conexión disponible del pool
Timeout de InactividadCuánto tiempo una conexión inactiva permanece abierta antes de cerrarse
Detección de FugasIdentificar código que adquiere conexiones sin liberarlas

El Problema de Conexión

flowchart diagram: subgraph SinPool[

Sin pooling, cada petición paga el costo completo de conexión; con pooling, el handshake ocurre una vez y la conexión queda caliente para reutilizarse.

Implementación de Pooling de Conexiones Paso a Paso

1. Dimensiona tu Pool Correctamente

La configuración más importante es el tamaño del pool. Demasiado pequeño = peticiones bloqueadas. Demasiado grande = memoria desperdiciada y contención en base de datos.

Fórmula para Tamaño Óptimo de Pool

El punto de partida clásico viene de la wiki de PostgreSQL:

conexiones = ((núcleos * 2) + discos_efectivos)

Para PostgreSQL en un servidor de 16 núcleos con SSD:

conexiones = (16 * 2) + 1 = 33 conexiones para throughput máximo

Tómalo como heurística, no como regla — discos_efectivos vale 1 con SSD/NVMe y aproximadamente la cantidad de discos en almacenamiento mecánico. Valida contra métricas de latencia reales antes de dejarlo fijo.

Ejemplo Trabajado

Digamos que tu PostgreSQL maneja cómodamente 300 conexiones (por debajo de su max_connections de 500, dejando espacio para admin y replicación). Corres 8 instancias del servicio API más 2 workers de batch:

presupuesto = 300 conexiones
instancias API: 8 × maxPoolSize 30 = 240
workers batch: 2 × maxPoolSize 15 = 30
total: 270 de 300 — cabe, con un margen pequeño

Si la misma base tuviera que servir 20 instancias, la cuenta deja de funcionar: 20 × 30 = 600 > 300. Ahí es cuando pones PgBouncer delante y achicas cada pool de app a 3-5 — el proxy embute 600 conexiones cliente sobre ~25 conexiones reales al servidor en modo transaction.

Dimensionamiento de Pool por Servicio

EscenarioTamaño Máximo del PoolRazonamiento
Servicio pequeño (2 instancias)10-15Compartir un límite pequeño de conexiones de base de datos
Servicio mediano (5 instancias)5-10Tamaño de pool × instancias ≤ límite de base de datos
Servicio grande (20+ instancias)3-5Muchas instancias, pools pequeños, usar PgBouncer
Worker de batch2-5Pocas operaciones concurrentes, conexiones largas
API en tiempo real10-20Muchas peticiones cortas, respuesta rápida
# Ejemplo: Configuración de HikariCP (Java)
spring:
  datasource:
    hikari:
      minimum-idle: 5
      maximum-pool-size: 20
      idle-timeout: 300000        # 5 minutos
      max-lifetime: 1200000       # 20 minutos
      connection-timeout: 30000  # 30 segundos
      leak-detection-threshold: 60000  # 60 segundos
      pool-name: OrderServicePool
# Ejemplo: Configuración de pool de SQLAlchemy (Python)
from sqlalchemy import create_engine

engine = create_engine(
    "postgresql://user:pass@localhost/db",
    pool_size=10,              # Conexiones mínimas mantenidas
    max_overflow=5,            # Conexiones extra más allá de pool_size
    pool_timeout=30,           # Segundos esperando conexión disponible
    pool_recycle=1800,         # Reciclar conexiones después de 30 minutos
    pool_pre_ping=True,        # Verificar salud de conexión antes de usar
    echo=False
)
// Ejemplo: Pool de node-postgres (Node.js)
const { Pool } = require('pg');

const pool = new Pool({
    host: 'localhost',
    database: 'app',
    user: 'app_user',
    password: process.env.DB_PASSWORD,
    max: 20,                    // Máximo de conexiones
    idleTimeoutMillis: 300000,  // Cerrar conexiones inactivas después de 5 min
    connectionTimeoutMillis: 10000,  // Timeout adquiriendo conexión
    allowExitOnIdle: true       // Permitir salida del proceso cuando pool inactivo
});

2. Configura el Comportamiento del Pool

Ajusta cómo el pool gestiona conexiones:

ConfiguraciónQué ControlaValor Recomendado
minIdleConexiones mantenidas calientes20-50% de maxPoolSize
maxLifetimeEdad máxima de conexión15-30 minutos (menor que timeout de DB)
idleTimeoutCuánto tiempo permanecen abiertas conexiones inactivas5-10 minutos
connectionTimeoutTiempo esperando conexión disponible10-30 segundos
validationTimeoutTimeout de health check2-5 segundos
leakDetectionThresholdAdvertir si conexión se mantiene demasiado30-60 segundos
// Ejemplo: Configuración avanzada de HikariCP
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://db:5432/app");
config.setUsername("app");
config.setPassword(System.getenv("DB_PASSWORD"));

// Dimensionamiento de pool
config.setMinimumIdle(5);
config.setMaximumPoolSize(20);

// Timeouts
config.setConnectionTimeout(30000);      // 30s espera máxima
config.setIdleTimeout(600000);           // 10m cierre inactivo
config.setMaxLifetime(1800000);          // 30m edad máxima
config.setValidationTimeout(5000);       // 5s health check

// Detección de fugas
config.setLeakDetectionThreshold(60000); // 60s umbral de advertencia

// Rendimiento
config.setAutoCommit(false);             // Usar transacciones explícitas
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");

HikariDataSource ds = new HikariDataSource(config);

Por Qué Importan Estos Settings

  • minIdle: Previene latencia de creación de conexión durante picos de tráfico
  • maxLifetime: Previene conexiones obsoletas y trabaja alrededor de firewalls que eliminan TCP inactivo
  • idleTimeout: Cierra conexiones no usadas para liberar recursos de base de datos
  • connectionTimeout: Falla rápido en lugar de quedar colgado indefinidamente
  • leakDetectionThreshold: Atrapa código que olvida cerrar conexiones

3. Usa Pooling de Conexiones en Middleware

Cuando tienes muchas instancias de aplicación, usa un proxy de pool de conexiones:

# Ejemplo: Configuración de PgBouncer
[databases]
app_db = host=primary.db port=5432 dbname=app

[pgbouncer]
listen_port = 6432
listen_addr = 0.0.0.0
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt

# Modos de pool:
# session = conexión fijada hasta que el cliente se desconecta (default, más seguro)
# transaction = conexión devuelta al pool después de cada transacción (mejor compartir)
# statement = conexión devuelta después de cada statement (más agresivo)
pool_mode = transaction

# Límites de conexiones
max_client_conn = 10000
default_pool_size = 25
min_pool_size = 10
reserve_pool_size = 5
reserve_pool_timeout = 3

# Timeouts
server_idle_timeout = 600
server_lifetime = 3600
server_connect_timeout = 15
# Ejemplo: Configuración de ProxySQL
mysql_servers =
(
    { hostgroup_id=1, hostname="primary.db", port=3306, weight=1 },
    { hostgroup_id=2, hostname="replica1.db", port=3306, weight=1 },
    { hostgroup_id=2, hostname="replica2.db", port=3306, weight=1 }
)

mysql_query_rules =
(
    { rule_id=1, active=1, match_pattern="^SELECT", destination_hostgroup=2, apply=1 },
    { rule_id=2, active=1, match_pattern="^SELECT.*FOR UPDATE", destination_hostgroup=1, apply=1 }
)

Modos de Pool Explicados

ModoComportamientoMejor Para
SessionConexión retenida por toda la sesión del clientePrepared statements, variables de sesión
TransactionConexión devuelta después de COMMIT/ROLLBACKLa mayoría de aplicaciones web (recomendado)
StatementConexión devuelta después de cada statementStateless, consultas simples (raramente usado)

Dos detalles operativos que conviene saber antes de producción: PgBouncer autentica contra auth_file (un userlist.txt con usuarios y hashes SCRAM — genéralo desde pg_authid en vez de escribirlo a mano), y termina TLS él mismo, así que ponlo en el mismo host o segmento de red que la app y restringe listen_addr acorde. Exponer PgBouncer en 0.0.0.0 sin regla de firewall es una forma común de publicar tu base de datos entera en internet por accidente.

4. Detecta y Corrige Fugas de Conexión

Las fugas de conexión son el problema de producción más común relacionado con pools:

// MAL: La conexión nunca se cierra si hay excepción
public User getUser(String id) {
    Connection conn = dataSource.getConnection();
    ResultSet rs = conn.prepareStatement("SELECT * FROM users WHERE id = ?")
                        .executeQuery();
    // Si hay excepción aquí, la conexión nunca se retorna!
    return mapUser(rs);
    // Falta: conn.close()
}

// BIEN: Try-with-resources (Java)
public User getUser(String id) {
    try (Connection conn = dataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
        ps.setString(1, id);
        try (ResultSet rs = ps.executeQuery()) {
            return mapUser(rs);
        }
    } // Auto-cerrado
}
# MAL: La conexión no se libera en excepción
def get_user(user_id):
    conn = engine.connect()
    result = conn.execute("SELECT * FROM users WHERE id = %s", user_id)
    user = result.fetchone()
    return user  # Conexión nunca cerrada!

# BIEN: Context manager (Python/SQLAlchemy)
def get_user(user_id):
    with engine.connect() as conn:
        result = conn.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id})
        return result.fetchone()
    # Conexión auto-devuelta al pool

Estrategias de Detección de Fugas

EnfoqueCómoCuándo
Logging de poolleakDetectionThreshold de HikariCPDesarrollo y staging
Wrapping de conexiónRastrear trazas de pila de adquisición/liberaciónDepurar fugas específicas
Tracing APMMétricas de conexión de Datadog, New RelicMonitoreo en producción
Basado en timeoutCerrar conexiones fugadas después de N minutosRed de seguridad en producción

Atrapando la Fuga en un Stack Trace

Con leakDetectionThreshold habilitado, HikariCP registra el stack trace del código que tomó la conexión y nunca la devolvió. La traza apunta al sitio de adquisición, no a donde debió cerrarse — busca el primer frame de tu propio código y sigue el camino de llamada hasta encontrar el close() o bloque with faltante. Un infractor clásico es un helper que devuelve un ResultSet o Stream manteniendo la conexión abierta después de que el método retorna: la corrección es consumir los resultados dentro del scope gestionado, no fuera.

5. Monitorea la Salud del Pool

Rastrea métricas de pool para detectar problemas antes de que causen caídas:

# Ejemplo: Métricas Prometheus para HikariCP
# Estas se exponen automáticamente vía Micrometer en Spring Boot

# Métricas clave:
# hikaricp_connections_active     - Conexiones actualmente en uso
# hikaricp_connections_idle       - Conexiones disponibles en pool
# hikaricp_connections_pending    - Hilos esperando conexión
# hikaricp_connections_timeout_total  - Eventos de timeout
# hikaricp_connections_usage_seconds  - Histograma de uso de conexión
# Ejemplo: Monitoreo de pool personalizado (Python)
from prometheus_client import Gauge, Counter

pool_active = Gauge('db_pool_connections_active', 'Conexiones activas')
pool_idle = Gauge('db_pool_connections_idle', 'Conexiones inactivas')
pool_waiters = Gauge('db_pool_waiters', 'Hilos esperando conexión')
pool_timeouts = Counter('db_pool_timeouts_total', 'Timeouts de conexión')

def monitor_pool(pool):
    pool_active.set(pool.size())
    pool_idle.set(pool.maxsize - pool.size())
    # Alertar si waiters > 0 o active == max por > 30s

Grafica tres series por servicio: conexiones activas, waiters pendientes y latencia de adquisición. Waiters por encima de cero con activas en el máximo significa que el pool es el cuello de botella; activas en el máximo con cero waiters significa que está justo en capacidad — déjalo quieto. Combínalo con el conteo propio de pg_stat_activity de la base para ver ambos lados del cuello de botella.

Revisa También el Lado de la Base

Las métricas del pool muestran la vista del cliente. La base de datos ve la otra mitad — ejecuta esto cuando los números no cuadren:

-- Conexiones activas vs inactivas ahora mismo
SELECT state, COUNT(*)
FROM pg_stat_activity
WHERE datname = 'app'
GROUP BY state;

-- Sentencias que más tiempo acumulado consumen
SELECT query, calls, total_exec_time, mean_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

Para una prueba rápida de saturación, pgbench -c 50 -T 30 mydb simula 50 clientes concurrentes durante 30 segundos. Si satura sin problema mientras tu app se traba, el cuello de botella es el pool, no la base de datos.

Alertas Críticas

AlertaUmbralSignificado
Agotamiento de poolActive == Max por > 60sTodas las conexiones en uso, nuevas peticiones bloqueadas
Tiempo de espera altoEspera promedio > 1sPool muy pequeño o consultas muy lentas
Tasa de timeout> 1% de peticionesAgotamiento severo de pool
Fuga detectadaCualquier advertencia de fugaCódigo no cerrando conexiones
Edad de conexiónEdad promedio > maxLifetimeConexiones no rotando correctamente

Troubleshooting de Problemas Comunes del Pool

SíntomaCausa probableCorrección
connection reset en horas vallemaxLifetime mayor que el timeout de inactividad del firewall/DBBaja maxLifetime por debajo del timeout; habilita pre-ping
Picos de latencia bajo cargaPool subdimensionado para la concurrenciaSube maxPoolSize gradualmente observando la CPU de la DB
too many connections en la DBLa suma de pools de instancias excede max_connectionsAchica los pools de app o pon PgBouncer delante
El pool se agota solo durante jobs batchBatch y OLTP comparten un poolSepara en dos pools con presupuestos propios
Hilos esperando para siempreconnectionTimeout ausente o enormePonlo en 10-30s para que las peticiones fallen rápido

6. Pooling en Serverless y Cold Starts

Lambda, Cloud Run y otros runtimes de funciones rompen el modelo clásico de pooling: las instancias aparecen y desaparecen a demanda, así que los pools por instancia se multiplican impredeciblemente. Cincuenta invocaciones concurrentes con un pool de 10 conexiones cada una pueden tumbar una base de datos en segundos.

La mitigación estándar es poner un proxy delante — RDS Proxy en AWS, PgBouncer en una VM pequeña, o el pooler gestionado de servicios como Supabase — y apuntar las funciones a él en vez de a la base. Si tu runtime mantiene entornos calientes entre invocaciones, un pool global de inicialización perezosa también ayuda: la primera invocación paga el costo de conexión, las siguientes la reutilizan.

// Reutiliza el pool entre invocaciones calientes de Lambda
// (la inicialización a nivel de módulo corre una vez por entorno)
const { Pool } = require('pg');

let pool;
function getPool() {
    if (!pool) {
        pool = new Pool({
            connectionString: process.env.DATABASE_URL, // apunta al proxy
            max: 2,                    // pequeño: el pooling real lo hace el proxy
            idleTimeoutMillis: 10000,
            allowExitOnIdle: true,
        });
    }
    return pool;
}

Mantén el pool por función deliberadamente pequeño. El proxy es el componente que condensa cientos de conexiones efímeras en un conjunto estable de conexiones al servidor.

7. Ajusta PgBouncer Bajo Carga

Los valores por defecto de PgBouncer son conservadores. Tres parámetros importan más en producción:

  • pool_mode = transaction da la mejor compartición, pero resetea el estado de sesión entre transacciones. Los prepared statements, comandos SET, advisory locks a nivel de sesión y LISTEN/NOTIFY no sobreviven al límite de transacción — deja esas cargas en modo session o en una conexión directa.
  • default_pool_size es por par usuario+base. Con pool_size = 25 y 4 usuarios pegándole a 2 bases, PgBouncer puede abrir hasta 200 conexiones al servidor. Multiplica antes de desplegar.
  • reserve_pool_size y reserve_pool_timeout absorben ráfagas: pasado el timeout, los clientes en espera toman conexiones del pool de reserva en vez de recibir error.
  • max_prepared_statements (PgBouncer 1.21+) hace que los prepared statements con nombre funcionen en modo transaction — el pooler los rastrea entre backends. En versiones anteriores, las cargas con prepared statements siguen necesitando modo session o una conexión directa.

PgBouncer expone su propia consola de administración (psql -h 127.0.0.1 -p 6432 pgbouncer). SHOW POOLS muestra esperas de cliente/servidor por pool; SHOW STATS reporta el tiempo de espera promedio — los dos números que quieres en tu dashboard junto a las métricas del paso 5.

8. Haz Pooling También en los Clientes HTTP

Las bases de datos no son el único recurso en pool. Cada llamada HTTP saliente de tu app paga un handshake TCP+TLS a menos que el cliente reutilice conexiones — y los pools keep-alive sin límite causan el mismo problema de agotamiento contra APIs downstream y load balancers.

// node: agente keep-alive con un pool de sockets acotado
const http = require('http');
const agent = new http.Agent({
    keepAlive: true,
    maxSockets: 50,        // límite de sockets concurrentes por host
    maxFreeSockets: 10,    // cuántos sockets inactivos mantener calientes
    timeout: 60000,
});
# requests/httpx: una Session reutiliza su pool de conexiones
import httpx

client = httpx.Client(
    limits=httpx.Limits(max_connections=50, max_keepalive_connections=10),
    timeout=30.0,
)

Las reglas se trasladan directamente: acota el pool (maxSockets/max_connections), mantén un pequeño conjunto caliente, y reutiliza el objeto cliente entre peticiones en vez de crear uno por llamada. Un requests.get() suelto en un bucle es el equivalente HTTP de abrir una conexión de base de datos por consulta.

9. Sobrevive a los Deploys y Restarts

Los rolling deploys causan una tormenta de conexiones predecible: cada instancia nueva abre sus minIdle conexiones de golpe mientras las instancias viejas aún retienen las suyas. Con 10 instancias × minimum-idle: 10, un restart rodante duplica brevemente la demanda de conexiones. Dos defensas: mantén minimum-idle bajo (5 o menos) para que el warm-up sea barato, y dimensiona maxPoolSize asumiendo que existe el doble de tu flota durante la ventana de deploy. Si tus deploys se solapan mucho, cuenta las flotas vieja y nueva por separado en el presupuesto del paso 1.

Lo que funciona

  • Dimensiona pools basado en capacidad de base de datos, no deseo de aplicación. Tu base de datos tiene un límite duro de conexiones. Suma todos los maxPoolSizes y asegúrate de que quepan.
  • Usa pooling a nivel de transacción (PgBouncer) para apps web. El pooling a nivel de sesión desperdicia conexiones durante el tiempo inactivo de petición HTTP.
  • Siempre usa try-with-resources o context managers. Nunca confíes en llamadas manuales a close().
  • Configura maxLifetime menor que el timeout de inactividad de base de datos. Previene errores de “connection reset” de firewalls o configuraciones de base de datos.
  • Habilita verificación de conexión (pre-ping). Verifica que las conexiones estén vivas antes de entregarlas al código de aplicación.
  • Usa pools separados para diferentes cargas de trabajo. Trabajos batch y APIs en tiempo real no deberían compartir un pool.

Errores Comunes

  • Pools sobredimensionados. Un pool de 100 conexiones por instancia × 20 instancias = 2000 conexiones. La mayoría de servidores PostgreSQL luchan más allá de 500.
  • Sin timeout de conexión. Timeouts por defecto de 30s+ causan fallos en cascada durante cortes.
  • Mantener conexiones durante peticiones HTTP. Si tu llamada a API toma 5s y mantienes una conexión DB todo ese tiempo, necesitas 5× más conexiones.
  • No manejar agotamiento de pool. Cuando el pool está lleno, tu aplicación debería degradarse elegantemente, no quedarse colgada indefinidamente.
  • Un pool para todo. Trabajos batch que mantienen conexiones por minutos privan a peticiones API en tiempo real.

Variantes

  • Pool de aplicación (HikariCP, SQLAlchemy, node-postgres): vive dentro de cada instancia. Es lo más simple de configurar, pero cada instancia tiene su propio presupuesto de conexiones — sirve hasta unas 5-10 instancias.
  • Pool de middleware (PgBouncer, ProxySQL, pgpool): proxy entre las apps y la base que condensa conexiones cliente en un conjunto fijo de conexiones al servidor. La opción correcta cuando el número de instancias supera lo que instancias × pool_size puede sostener.
  • Pool gestionado en la nube (RDS Proxy, pooling integrado de Cloud SQL): la misma idea operada por el proveedor, normalmente con integración IAM y manejo automático de failover.
  • Pooling serverless: los runtimes efímeros (Lambda, Cloud Run) no pueden mantener pools estables; necesitan un proxy o un pool global reutilizado en caliente — cubierto en el paso 6. Ver también el patrón de pooling de conexiones DB serverless para una versión más reducida de este escenario.

Conclusión

El pooling de conexiones es de esos arreglos que parecen aburridos hasta el día en que salvan un lanzamiento. Dimensiona bien con la fórmula, acota los timeouts para que los fallos salgan rápido, y vigila los dos números que de verdad dicen algo: waiters y tiempo de espera. Con eso hecho, las tormentas de conexiones dejan de ser un problema de las 3 de la mañana.

Ver También

Lecturas Adicionales

Preguntas frecuentes

¿Con qué tamaño de pool empiezo?

Empieza con 10. Monitorea las conexiones activas bajo carga pico; si se acercan al máximo de forma sostenida, sube gradualmente. Nunca dejes que pool_size × instancias supere lo que la base puede manejar — ver el ejemplo trabajado del paso 1.

¿PgBouncer o un pool en la aplicación?

Ambos. El pool en la app resuelve la eficiencia por instancia; PgBouncer resuelve la compartición entre instancias. Pasadas ~5 instancias, un proxy delante deja de ser opcional.

¿Por qué aparecen errores de "connection reset" de noche?

Casi siempre es un desfase de timeouts: la base o un firewall cierran conexiones inactivas mientras tu pool las sigue considerando vivas. Baja maxLifetime por debajo del timeout de inactividad y habilita pool_pre_ping/testOnBorrow para que las conexiones muertas se descarten antes de usarse.

¿Cómo hago pooling desde funciones serverless?

Apunta las funciones a un proxy (RDS Proxy, PgBouncer) y conserva una variable global de pool para reutilizar entre invocaciones calientes. Los cold starts siguen pagando el costo de conexión — es inevitable — pero el proxy lo absorbe.

¿Puede pool_size × instancias superar max_connections?

Puede, y te va a morder durante un pico de tráfico cuando todas las instancias llenen sus pools a la vez. Presupuesta siempre suma(maxPoolSize entre instancias) < max_connections − margen, o pon un proxy delante y trata el pool de la app como limitador de concurrencia, no como el pool real.