Por qué su base de datos se queda sin conexiones en Vercel
Una instancia de función serverless no puede compartir un pool de conexiones con la siguiente. Entender eso es la diferencia entre una elección de driver y un incidente recurrente.
El error llega después de un despliegue que fue bien, normalmente en la primera
ráfaga de tráfico: too many connections, o Timed out fetching a new connection from the connection pool. La base de datos está ociosa. La
aplicación no hace nada raro. El mismo código lleva meses corriendo en local.
Lo que cambió es cuántas copias de él existen.
Lo que de verdad es distinto
En desarrollo, next dev es un proceso Node. Crea un cliente de base de datos
una vez, abre un pool, y todas las peticiones de su aplicación lo comparten. Ese
es el modelo que asume cualquier tutorial de pooling, y es correcto.
Desplegado en una plataforma serverless, su route handler corre dentro de una instancia de función. Cada instancia tiene su propio ámbito de módulo, su propia memoria y por tanto su propio pool. Diez instancias calientes con un pool de diez son cien conexiones, y en ningún sitio de su código aparece el número cien.
El número no lo dirigen las peticiones concurrentes. Lo dirige cuántas instancias mantiene calientes la plataforma, que es un número que usted no controla y no ve desde dentro de la aplicación.
El error que lo hace inmediato
// app/api/orders/route.ts
export async function GET() {
const client = new PrismaClient(); // ← un pool nuevo, por petición
...
}Un cliente construido dentro de un handler abre un pool en cada invocación y lo deja al recolector de basura. Esto agota una base de datos en minutos con cualquier tráfico real.
La corrección a nivel de instancia es construirlo una vez en el ámbito del módulo, con la salvedad de que la recarga en caliente vuelve a evaluar los módulos y acumularía clientes:
// lib/db.ts
import { PrismaClient } from '@prisma/client';
const globalForDb = globalThis as unknown as { db?: PrismaClient };
export const db = globalForDb.db ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForDb.db = db;Eso es necesario y no suficiente. Acota el pool por instancia. No puede acotar el número de instancias.
Las tres respuestas reales
1. Un pooler delante de la base de datos. PgBouncer, o como lo llame su proveedor. Sus funciones se conectan a él; él las multiplexa sobre un número pequeño de backends reales. Es la respuesta estándar y la primera a la que recurrir.
Lo que hay que acertar es el modo. El pooling por transacción es el que produce las cifras de conexiones, y le entrega un backend solo durante una transacción, así que todo lo que abarca varias sentencias deja de funcionar. En la práctica eso son las sentencias preparadas en servidor, que la mayoría de los ORM usan por defecto. Todos los proveedores documentan una bandera para esto; póngala a propósito en lugar de descubrirla.
Conserve una segunda cadena de conexión directa para las migraciones. Los cambios de esquema necesitan una sesión real, y pasarlos por un pooler por transacción falla de formas difíciles de leer.
2. Un driver que no sostiene ninguna conexión. Algunos proveedores exponen su base de datos sobre HTTP, así que una consulta es una petición y no hay nada que poolear:
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
const rows = await sql`select id, total from orders limit 20`;Es el encaje más limpio con el modelo de ejecución, y es lo único que funciona en un runtime de edge sin sockets TCP. El intercambio es que una consulta HTTP de un solo disparo no es una sesión: las transacciones interactivas necesitan la vía WebSocket, y un handler que emite seis consultas seguidas paga seis viajes donde una conexión pooleada pagaba uno.
Úselo para caminos de lectura con una o dos consultas. Use una conexión pooleada para cualquier cosa transaccional.
3. Menos conexiones, porque menos consultas. Infravalorado y normalmente disponible. Una página que hace una consulta en vez de cuatro necesita un cuarto del tiempo de conexión. Cargar en un Server Component en lugar de a través de un route handler que llama su cliente elimina un salto entero. Cachear una consulta que cambia cada hora la saca del camino caliente.
La conexión más rápida es la que nadie abre.
Dónde viven las consultas, que es la parte de Next.js
El App Router le da varios sitios donde puede correr una consulta, y tienen comportamientos distintos respecto a las conexiones.
Los Server Components corren durante el render, en el servidor, y pueden consultar directamente. Dos componentes hermanos cargando cada uno significan dos consultas en la misma invocación: bien para un pool, peor para un driver HTTP.
Los Route Handlers son el caso convencional y se comportan como cualquier endpoint.
Las Server Actions corren por envío. Una acción que escribe quiere una transacción real, que es el caso donde importa la limitación del driver HTTP.
El middleware corre en cada petición que coincide, antes que nada. Consultar desde ahí multiplica su presión de conexiones por su tráfico, incluidas peticiones que se habrían servido desde caché, y también es caro por otros motivos.
Si se aloja usted mismo
Casi todo esto desaparece. Un proceso Node de larga vida significa un ámbito de módulo, un pool, y un número de conexiones que usted fija y puede razonar. Vuelve al modelo que describe cualquier artículo sobre Node desde 2015.
Es un punto genuino a favor de ejecutarlo usted mismo, y es pequeño comparado con lo que asume. Menciónelo en el intercambio; no elija un modelo de despliegue por él.
El diagnóstico, en orden
- Pregunte a la base de datos cuántas conexiones existen y de dónde vienen, en lugar de inferirlo.
- Confirme que el cliente se construye una vez por módulo, no por petición.
- Confirme que se conecta al pooler, y que las migraciones no.
- Compruebe si las sentencias preparadas están desactivadas donde el pooling por transacción lo requiere.
- Solo entonces suba el límite, y escriba en qué se basó el número nuevo, porque la siguiente persona que añada una ruta necesitará saber que existe un presupuesto.
