---
title: "Pools de conexión a base de datos bajo carga: síntomas que los equipos leen como «lentitud de la app»"
description: "Detecta agotamiento de pool, hilos en espera y tormentas de timeout—luego ajusta pools y queries con evidencia."
publishDate: "2026-09-07"
draft: false
keyword: "pruebas carga pools conexion base datos"
intent: "MOFU"
cta: "Usa Performate para reproducir recorridos API con JDBC/ORM pesado mientras etiquetas rutas dependientes de BD."
tags: ["k6", "pruebas-de-carga", "rendimiento-api", "connection", "pool"]
translationOf: database-connection-pools-load-testing
---

La latencia de tu API sube mientras la CPU se mantiene plana. On-call asume «hacen falta más pods»—pero Postgres muestra **200 sesiones en espera**, HikariCP registra `Connection is not available, request timed out after 30000ms`, y el p99 solo se dispara en rutas que tocan la base de datos. Ese patrón es agotamiento de pool, no una lentitud misteriosa de la aplicación.

Los pools de conexión bajo carga son un problema de capacidad escondido dentro de métricas HTTP. En esta guía verás por qué pools saturados parecen servicios sanos en los dashboards, cómo diseñar escenarios k6 que estresen mezclas read/write de forma realista y qué señales deberían guiar el dimensionamiento del pool—no suposiciones a partir de valores por defecto del ORM.

## Por qué la saturación del pool se disfraza de lentitud de app

Los clientes HTTP ven respuestas lentas. Los servidores de aplicación suelen reportar CPU moderada porque los hilos **están bloqueados esperando conexiones**, no quemando ciclos en lógica de negocio. Bajo concurrencia, varios factores se combinan:

- **Techos fijos del pool** limitan sesiones concurrentes en BD; las peticiones extra hacen cola en el pool mientras crece el tiempo de iteración en k6.
- **Transacciones largas** retienen conexiones a través de varias llamadas ORM, saltos a APIs externas o ámbitos `@Transactional` mal ubicados.
- **Asimetría read/write** deja que el tráfico de lectura agote un pool compartido mientras las escrituras se mueren de hambre—o al revés cuando las réplicas van retrasadas.
- **Fugas de conexión** por recursos sin cerrar reducen el pool efectivo hasta que un reinicio «arregla» la latencia temporalmente.
- **Patrones ORM ruidosos** (N+1, eager fetch) multiplican idas y vueltas por petición HTTP y, con ellas, la presión sobre el pool.

Piensa en un pool como un parking con plazas fijas. El tráfico en la calle parece normal hasta que no queda sitio y los coches dan vueltas—tu API es el coche dando vueltas, no el operador del parking.

### Cuando el APM parece sano pero el tiempo de iteración k6 diverge

El APM puede mostrar latencia aceptable si promedia rutas cacheadas. [`http_req_duration`](https://k6.io/docs/using-k6/metrics/) de k6 en endpoints pesados en BD cuenta otra historia—sobre todo en colas ([p95 vs p99](/es/blog/p95-vs-p99-latencia)). Cruza tags de ruta con métricas de espera en BD (`pg_stat_activity`, JMX del pool o RDS Performance Insights) para correlacionar **sesiones en espera** con familias concretas de API.

Diseña escenarios que reutilicen tokens de auth pero abran queries read/write de forma realista ([guía de VU](/es/blog/cuantos-usuarios-virtuales-k6)) en lugar de martillear un único GET cacheado.

## Implementación práctica en k6: etiqueta rutas pesadas en BD y mezcla read/write

Modela los recorridos que realmente retienen conexiones—checkout, búsquedas con joins, informes admin—no solo health checks que nunca tocan el pool.

**Script de ejemplo (ilustrativo—no es una prueba lista para producción).** El fragmento usa URLs, tokens y SLO ficticios. Adapta base URL, auth, payloads y umbrales a tu entorno.

**Qué demuestra este ejemplo:**

- **Reparto read/write:** dos escenarios en paralelo a 40 y 10 req/s reflejan una mezcla tipo catálogo en producción, no 100 % escrituras.
- **Tags por ruta:** `db:read` y `db:write` permiten separar latencia y fallos por perfil de presión en el pool.
- **Umbrales separados:** las escrituras pueden ser más lentas con legitimidad; umbrales agregados ocultan inanición del pool en el lado write.
- **Rates por entorno:** `READ_RPS` / `WRITE_RPS` dejan que CI reproduzca el mismo script cuando analytics cambia antes de una venta.

```javascript
import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE = __ENV.API_BASE || 'https://staging.example.com';
const headers = {
  'Content-Type': 'application/json',
  Authorization: `Bearer ${__ENV.TOKEN}`,
};

export const options = {
  scenarios: {
    catalog_reads: {
      executor: 'constant-arrival-rate',
      rate: Number(__ENV.READ_RPS || 40),
      timeUnit: '1s',
      duration: '8m',
      preAllocatedVUs: 30,
      maxVUs: 120,
      tags: { db: 'read', route: 'catalog_search' },
      exec: 'searchCatalog',
    },
    order_writes: {
      executor: 'constant-arrival-rate',
      rate: Number(__ENV.WRITE_RPS || 10),
      timeUnit: '1s',
      duration: '8m',
      preAllocatedVUs: 15,
      maxVUs: 60,
      tags: { db: 'write', route: 'order_create' },
      exec: 'createOrder',
    },
  },
  thresholds: {
    'http_req_duration{db:read}': ['p(95)<400', 'p(99)<800'],
    'http_req_duration{db:write}': ['p(95)<900', 'p(99)<1500'],
    'http_req_failed{db:write}': ['rate<0.02'],
    http_req_failed: ['rate<0.01'],
  },
};

export function searchCatalog() {
  const res = http.get(`${BASE}/api/catalog/search?q=widget&limit=50`, {
    headers,
    tags: { db: 'read', route: 'catalog_search' },
  });
  check(res, { 'search 2xx': (r) => r.status >= 200 && r.status < 300 });
  sleep(0.2);
}

export function createOrder() {
  const body = JSON.stringify({ sku: 'SKU-200', qty: 2, currency: 'USD' });
  const res = http.post(`${BASE}/api/orders`, body, {
    headers,
    tags: { db: 'write', route: 'order_create' },
  });
  check(res, { 'order 2xx': (r) => r.status >= 200 && r.status < 300 });
  sleep(0.5);
}
```

**Patrones que funcionan**

- **Escenarios en paralelo** con `constant-arrival-rate` mantienen porcentajes read/write honestos ([referencia de executors](https://k6.io/docs/using-k6/scenarios/executors/)).
- **Tags alineados con observabilidad** (`db:read`, `route:order_create`) mapean cortes de k6 a APM y dashboards de BD ([tags de observabilidad k6](/es/blog/k6-observabilidad-metricas-tags)).
- **Segmentos soak:** alarga la duración en escenarios write para detectar fugas que solo aparecen tras 20+ minutos ([patrones soak](/es/blog/playbook-soak-testing-memory-leaks)).
- **Correlation IDs** en headers atan iteraciones k6 a logs de slow query ([correlation IDs](/es/blog/correlation-ids-trazabilidad-distribuida-k6)).

**Antipatrones a evitar**

- Probar solo `/health` o assets estáticos—cero señal de pool.
- Subir `maxSize` del pool sin medir tiempo de espera—enmascara queries ineficientes hasta saturar la BD.
- Usar `shared-iterations` con muchos VU en código ORM bloqueante—resultados engañosos frente a arrival rate ([tipos de escenario](/es/blog/tipos-escenarios-k6-explicados)).

**Pro tip (comando de ejemplo):** la línea siguiente muestra latencia de cola en rutas etiquetadas durante revisiones de tuning de pool.

```bash
k6 run pool-mix.js --summary-trend-stats="p(95),p(99),max"
```

**Qué demuestra este comando:** k6 imprime tendencias de percentiles más picos máximos de iteración para comparar colas read vs write cuando los timeouts de adquisición se agrupan justo bajo tu límite configurado.

## Marco de decisión: ajustar pool vs optimizar queries vs escalar en horizontal

| Situación | Acción recomendada |
|:---|:---|
| Timeouts de espera en pool con CPU de BD baja | Reducir ámbito transaccional, corregir fugas, ajustar `maxLifetime` / eviction idle |
| CPU de BD alta + sesiones activas en alza | Optimizar queries, añadir índices, separar tráfico a réplica de lectura |
| Pico read-heavy (rebajas, lanzamiento) | Escalar réplicas + pool de lectura separado; probar primero solo el escenario read |
| Cola de escrituras con arrival rate estable | Revisar contención de locks, batch writes o shard de tablas calientes |
| Staging pasa, prod falla al mismo RPS | Comparar tamaños de pool, latencia de red y límites DSN ([staging vs prod](/es/blog/pruebas-carga-staging-vs-produccion)) |

**Ajusta el pool primero si** domina el tiempo de espera de adquisición y las queries ya están por debajo de 50 ms a baja concurrencia.

**Optimiza queries primero si** `pg_stat_statements` o APM muestran SQL lento repetido independientemente del tamaño del pool.

**Escala en horizontal si** pool y queries están sanos pero el conteo de conexiones se acerca a `max_connections` de la plataforma.

## Observabilidad, documentación y próximos pasos

Las investigaciones de pool fallan cuando el equipo persigue la capa equivocada. Antes del próximo pedido de scale-up:

- [ ] Captura métricas de pool (activas, idle, pending, timeouts) en la misma ventana temporal que la corrida k6.
- [ ] Documenta rates read/write de escenarios y su fuente en analytics—no defaults arbitrarios de 100 VU.
- [ ] Alerta cuando `p(99){db:write}` diverge del baseline mientras pending del pool supera tu umbral acordado.
- [ ] Correlaciona tags de ruta k6 con slow-query logs y spans ORM de los tres mayores contribuyentes de latencia.
- [ ] Archiva JSON de escenario, snapshots de config de pool y git SHA por corrida para comparar regresiones con criterio.

## Cómo Performate simplifica pruebas de carga pesadas en BD

Reproducir recorridos ORM sin mantener forks frágiles de scripts es la parte difícil. Abajo, un **ejemplo de flujo concreto** para la misma API catálogo + pedidos de este artículo.

**Ejemplo: reproducir rutas JDBC con escenarios etiquetados**

1. **Importa una colección Postman** (u OpenAPI) con `catalog/search` y POST `orders` con bodies realistas. *Problema resuelto:* una fuente de verdad para payloads que disparan joins y escrituras.
2. **Crea dos escenarios en el editor visual**—`catalog_reads` a **40 req/s** y `order_writes` a **10 req/s**—siguiendo la mezcla de la semana pasada. *Problema resuelto:* presión read/write honesta sin editar executors a mano cada sprint.
3. **Aplica tags en el panel de escenario:** `db:read` en búsqueda, `db:write` en creación de pedido, más `service:commerce` compartido. *Problema resuelto:* los reportes filtran las mismas dimensiones que el ejemplo k6.
4. **Ejecuta ambos escenarios y abre la vista comparativa** del informe integrado. Comprueba si el `p99` de write sube mientras read se mantiene plano—firma clásica de inanición de pool. *Problema resuelto:* backend y DBA debaten un export, no capturas contradictorias.
5. **Itera ajustes de pool en staging**, vuelve a ejecutar con los mismos pesos y compara informes lado a lado. *Problema resuelto:* el tuning del pool es un experimento controlado, no una apuesta en producción.
6. **Exporta el script k6 generado** para smoke gates en CI ([pruebas de carga en CI/CD](/es/blog/pruebas-de-carga-en-ci-cd)) y mantener alineados tuning local y pipeline.

Ese flujo encaja con el `cta` de esta entrada: reproducir recorridos JDBC/ORM pesados etiquetando rutas dependientes de BD en un solo espacio de trabajo.

## Conclusión

El agotamiento de pool es una **señal de capacidad disfrazada de lentitud HTTP**. Prueba en carga las mezclas read/write que realmente retienen conexiones, etiqueta cada ruta pesada en BD y correlaciona colas k6 con métricas de espera del pool antes de añadir pods o subir límites a ciegas.

Ejecuta esta semana la mezcla de analytics contra staging—y anota si las escrituras o las lecturas arrastran la cola de latencia que tu configuración de pool debe soportar.

[Prueba Performate gratis](https://performate.app) | [Reserva una demo](/demo) | [Métricas k6](https://k6.io/docs/using-k6/metrics/)
