Cada servicio de Node que se comunica con una base de datos en realidad ejecuta dos sistemas a la vez: la base de datos en sí, y una pequeña pieza de infraestructura dentro del proceso de Node cuyo único trabajo es gestionar cómo se utiliza esa conexión.
Ese segundo sistema, el pool de conexiones, es fácil de pasar por alto porque un driver como pg o un ORM como Prisma lo oculta detrás de una simple llamada .query(), pero casi todos los problemas de producción reales en esta sección (pools agotados, tormentas de conexiones frías sin servidor, consultas N+1, fugas de transacciones) se remontan a cómo se comporta realmente ese pool.
Conceptos básicos de bases de datos muestra el código para usar un pool correctamente; esta página trata sobre por qué existe el pooling, y dónde se sitúan los drivers, los ORMs y el bucle de eventos en la arquitectura que lo rodea.
Un proceso de Node no abre una nueva conexión a la base de datos por cada consulta, sino que mantiene un pequeño conjunto reutilizado de conexiones abiertas (un pool) y toma prestada una durante la duración de cada consulta, porque abrir una conexión es órdenes de magnitud más costoso que usar una ya abierta.
Por qué es importante: El tamaño del pool, no la velocidad bruta del driver, suele ser el límite de concurrencia real en un servicio de Node respaldado por una base de datos; no comprender esto lleva a solicitudes hambrientas (pool demasiado pequeño) o a una base de datos abrumada (pool demasiado grande, o uno por proceso en una implementación de múltiples instancias).
Conceptos clave:pool de conexiones, driver, ORM, protocolo de red, límite de transacción, consulta N+1.
Cuándo usarlo: Cualquier servicio de Node que emita más de un puñado de consultas por segundo, cualquier implementación sin servidor donde la rotación de conexiones sea el riesgo real, y cualquier base de código que elija entre un driver puro y un ORM para un nuevo servicio.
Limitaciones / Compensaciones: El pooling añade una capa de estado (conexiones prestadas vs. inactivas, tiempos de espera, fugas) que una conexión ingenua por solicitud no tendría; los ORMs añaden comodidad y seguridad de tipos a costa de cierto control sobre el SQL exacto que se ejecuta.
Temas relacionados: gestión de transacciones, patrones de consulta N+1, límites de conexión sin servidor, el patrón de repositorio, dimensionamiento del pool de conexiones.
Abrir una conexión a la base de datos no es como abrir un archivo: implica un handshake TCP con el servidor de la base de datos, a menudo una negociación TLS además de eso, y luego un intercambio de autenticación antes de que se pueda ejecutar una sola consulta.
Toda esa secuencia puede tardar decenas de milisegundos, lo cual es enorme en comparación con la consulta en sí, por lo que abrir una conexión nueva para cada solicitud significaría pagar ese costo, y mantener abiertas las ranuras de conexión limitadas de la base de datos, constantemente.
Un pool de conexiones resuelve esto de la misma manera que una parada de taxis compartida resuelve el problema de "comprar un coche para cada viaje": un número fijo de conexiones se abren una vez, se mantienen vivas y se entregan a quien las necesite a continuación, y se devuelven al pool (no se cierran) cuando la consulta finaliza.
// Un pool por proceso, creado una vez, no una conexión por solicitudconst pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10 });const { rows } = await pool.query("SELECT id FROM users WHERE id = $1", [id]);// pool.query() toma prestada una conexión, ejecuta la consulta y la devuelve automáticamente
Sobre el pool se encuentra un driver —pg para Postgres, el driver de MongoDB, ioredis para Redis— una biblioteca que habla el protocolo de red real de la base de datos: el formato binario o de texto específico que el servidor espera a través del socket.
Un ORM (Prisma, Drizzle) es una capa adicional sobre un driver, no un reemplazo de uno; aún abre el mismo tipo de conexiones agrupadas a través del driver subyacente, pero agrega una API con tipos y consciente del esquema y genera el SQL (o equivalente) por ti en lugar de que lo escribas a mano.
El papel del bucle de eventos aquí es fácil de juzgar mal: el modelo de E/S no bloqueante de Node significa que una consulta no congela el proceso mientras espera la respuesta de la base de datos, pero la conexión en sí sigue siendo un canal único y ordenado; la mayoría de los protocolos de red de bases de datos procesan una solicitud a la vez por socket, por lo que un pool de diez conexiones realmente significa como máximo diez consultas en curso a la vez, no una concurrencia ilimitada.
Es por eso que el tamaño del pool es un límite de concurrencia que tú eliges, no un dial de rendimiento para maximizar; un pool de max: 10 significa que la undécima solicitud de consulta concurrente espera en una cola a que una conexión se libere, sin importar cuán rápido esté funcionando el propio bucle de eventos.
solicitud 1 ─┐
solicitud 2 ─┼─ pool.query() ─▶ [pool: 10 conexiones] ─▶ base de datos
solicitud 3 ─┘ ▲ 3 prestadas, 7 inactivas
solicitud 11 ── espera en cola ────┘ (las 10 actualmente prestadas)
Dimensionar ese límite demasiado pequeño agota la aplicación bajo carga: las solicitudes se ponen en cola para una conexión a pesar de que la base de datos tiene capacidad de sobra; dimensionarlo demasiado grande simplemente traslada el cuello de botella al servidor de la base de datos, que tiene su propio límite estricto de conexiones totales y se degrada (o rechaza nuevas) una vez que se alcanza ese límite.
Las matemáticas se vuelven más precisas en una implementación de múltiples instancias: si la base de datos permite 100 conexiones en total y cinco instancias de Node abren cada una un pool de max: 30, eso son 150 conexiones posibles contra un techo de 100 conexiones, una configuración que funciona bien con poco tráfico y falla exactamente cuando la carga aumenta lo suficiente como para que cada instancia alcance su pool completo a la vez.
Las transacciones añaden una segunda capa de estado sobre el pooling: una transacción debe ejecutarse en una conexión prestada específica desde BEGIN hasta COMMIT/ROLLBACK, porque la base de datos rastrea el estado de la transacción por conexión, no por consulta; por eso el código de transacción extrae explícitamente un cliente y lo pasa, en lugar de llamar a pool.query() para cada instrucción dentro de ella.
Los entornos sin servidor llevan el pool de conexiones al límite, porque la suposición central del pool (un proceso de larga duración que abre conexiones una vez y las reutiliza) no se cumple cuando cada invocación puede iniciar un nuevo proceso con un pool vacío.
A escala real, una ráfaga de invocaciones concurrentes sin servidor puede intentar abrir su propio pequeño pool simultáneamente, produciendo un pico de nuevas conexiones que puede exceder el límite total de la base de datos en segundos. La solución estándar es un pooler de conexiones externo (PgBouncer, o un equivalente gestionado como el pooler de Supabase o Neon) que se sitúa entre la base de datos y cada instancia sin servidor, multiplexando muchas conexiones lógicas de la aplicación en un número menor y estable de conexiones reales a la base de datos. Ajuste del pool de conexiones cubre esta matemática de dimensionamiento y los patrones específicos sin servidor en profundidad.
La elección entre driver y ORM es realmente una cuestión de dónde quieres que resida el control: un driver puro ofrece visibilidad total sobre el SQL exacto que se ejecuta, a costa de escribirlo y mantenerlo a mano; un ORM genera ese SQL a partir de un esquema y un constructor de consultas tipado, intercambiando parte de esa visibilidad por velocidad de desarrollo y seguridad en tiempo de compilación, pero también puede ocultar patrones de consulta costosos detrás de un código que parece conveniente, siendo el más notorio el problema de consulta N+1, donde la obtención de una lista y luego la obtención perezosa de los datos relacionados de cada elemento convierte una consulta prevista en N+1 viajes de ida y vuelta.
Ni un driver ni un ORM deben ser llamados directamente desde los manejadores de rutas en nada más allá de un servicio pequeño; el patrón común es una capa de repositorio o acceso a datos que se sitúa entre la lógica de la aplicación y el cliente de la base de datos, de modo que la tecnología de la base de datos (o incluso la elección entre driver y ORM) pueda cambiar sin reescribir cada manejador que toca los datos. Patrón de Repositorio cubre ese límite en detalle.
Enfoque
Fortaleza
Debilidad
Mejor ajuste
Driver puro (pg, driver de MongoDB)
Control total sobre las consultas exactas; mínima sobrecarga de abstracción
Escribes y mantienes SQL/consultas a mano; menos seguridad en tiempo de compilación
Rutas críticas para el rendimiento, SQL de informes complejos
ORM basado en esquema (Prisma)
Tipado fuerte generado a partir de un esquema, migraciones incorporadas
Las consultas generadas pueden ocultar patrones N+1; menos control sobre el SQL exacto
APIs nuevas que priorizan la velocidad del desarrollador y la seguridad de tipos
ORM basado en SQL (Drizzle)
Constructor de consultas tipado que se mantiene cerca del SQL real
Ecosistema/madurez más pequeños que Prisma actualmente
Equipos que desean consultas tipadas sin perder visibilidad a nivel de SQL
Pooler externo (PgBouncer, poolers gestionados)
Absorbe tormentas de conexiones; desacopla el recuento de instancias de la aplicación de los límites de conexión de la DB
Pieza de infraestructura adicional; el pooling en modo de transacción tiene sus propias advertencias
Implementaciones sin servidor, flotas con un gran número de instancias
"El bucle de eventos hace que las consultas a la base de datos sean totalmente paralelas." El bucle de eventos hace que el proceso no sea bloqueante mientras espera, pero cada conexión agrupada sigue procesando una consulta a la vez; la verdadera concurrencia está limitada por el tamaño del pool, no por cuántas consultas puede manejar el bucle de eventos.
"Un pool de conexiones más grande siempre es más seguro." A partir de cierto punto, simplemente traslada el cuello de botella al propio límite de conexiones del servidor de la base de datos, que cada instancia de la aplicación comparte; los pools sobredimensionados en varias instancias son una causa común de incidentes de "la base de datos rechaza conexiones".
"Un ORM reemplaza el driver y el pool." Se sitúa sobre ambos; Prisma y Drizzle aún abren conexiones agrupadas a través de un driver subyacente; el ORM solo cambia cómo se escriben y tipan las consultas, no la arquitectura de conexión subyacente.
"Serverless solo necesita un pool más grande por instancia para manejar la carga." Más conexiones por instancia empeora el problema de la tormenta de conexiones, no lo mejora, porque cada invocación concurrente abre su propio pool; la solución es un pooler externo compartido, no un max más grande por instancia.
"Usar un ORM significa que no necesito pensar en las consultas N+1." Los ORMs facilitan la escritura accidental de patrones N+1, no los hacen imposibles de escribir; la carga perezosa de relaciones dentro de un bucle genera los mismos viajes de ida y vuelta excesivos que un ORM se suponía que debía ayudar a evitar.
¿Por qué Node utiliza un pool de conexiones en lugar de una conexión por solicitud?
Abrir una conexión a la base de datos implica un handshake TCP, a menudo TLS y autenticación, una secuencia que es lenta en comparación con la ejecución de una consulta. Un pool abre un pequeño conjunto de conexiones una vez y las reutiliza, evitando ese costo en cada solicitud.
¿Cuál es la diferencia entre un driver de base de datos y un ORM?
Un driver (como pg) habla directamente el protocolo de red de la base de datos y expone una API de consulta de bajo nivel. Un ORM (como Prisma o Drizzle) se sitúa sobre un driver, generando consultas a partir de un esquema o un constructor de consultas tipado en lugar de que escribas SQL a mano; no reemplaza el driver ni su pool.
¿Significa la E/S no bloqueante de Node que un pool de 10 conexiones puede manejar consultas concurrentes ilimitadas?
No, la mayoría de los protocolos de red de bases de datos procesan una consulta a la vez por conexión, por lo que un pool de 10 realmente limita las consultas concurrentes en curso a 10. El bucle de eventos mantiene el proceso libre mientras espera, pero el pool limita cuántas consultas pueden estar "en curso" contra la base de datos simultáneamente.
¿Cómo debo decidir el tamaño máximo (`max`) de un pool?
Basándote en la carga de consultas concurrentes esperada por instancia, dividida por cuántas instancias se ejecutarán simultáneamente y el límite total de conexiones de la base de datos, no en el número de núcleos de CPU o un número predeterminado. Ajuste del pool de conexiones cubre las matemáticas de dimensionamiento.
¿Por qué las transacciones necesitan un manejo especial más allá de `pool.query()`?
El estado de una transacción (desde BEGIN hasta COMMIT/ROLLBACK) es rastreado por conexión física por la base de datos, no por consulta; por lo tanto, cada instrucción en una transacción debe ejecutarse en la misma conexión extraída, razón por la cual el código de transacción toma prestado explícitamente un cliente en lugar de llamar a pool.query() repetidamente.
¿Por qué el entorno sin servidor es especialmente difícil para el pool de conexiones?
Un pool asume un proceso de larga duración que abre conexiones una vez y las reutiliza en muchas solicitudes. Las invocaciones sin servidor pueden iniciar procesos nuevos con frecuencia, por lo que una ráfaga de invocaciones concurrentes puede abrir su propio pool a la vez, produciendo un pico de conexiones que puede exceder el límite de la base de datos.
¿Qué es realmente un problema de consulta N+1?
Obtener una lista de N elementos y luego obtener por separado los datos relacionados de cada elemento uno por uno, convirtiendo lo que deberían ser una o dos consultas en N+1 viajes de ida y vuelta; es más común con la carga perezosa de relaciones de ORM utilizada descuidadamente dentro de un bucle.
¿Debería el código de la aplicación llamar directamente al driver o al ORM de la base de datos?
En cualquier cosa más allá de un servicio pequeño, no; una capa de repositorio o acceso a datos generalmente se sitúa entre los manejadores de rutas y el driver/ORM, de modo que la elección de persistencia puede cambiar sin reescribir la lógica de negocio. Consulta Patrón de Repositorio.
¿Una base de datos de documentos (MongoDB) utiliza el pool de conexiones de la misma manera que una relacional?
Sí, conceptualmente; el driver de MongoDB también mantiene un pool de conexiones reutilizadas en lugar de abrir una por operación, por la misma razón: la configuración de la conexión es costosa en relación con la ejecución de una operación.
¿Cuál es el mayor error de principiante con las conexiones a bases de datos en Node?
Crear una nueva conexión (o un nuevo pool) por solicitud en lugar de un pool compartido por proceso; esto anula todo el propósito del pooling y puede agotar el límite de conexiones de la base de datos incluso con una carga moderada.