Nitro server routes lack connection lifecycle hooks causing pool exhaustion
0 reputation · 23 Apr 2024, 20:37 UTC
0 reputation · 23 Apr 2024, 20:37 UTC
Nuxt 3 uses Nitro as its server engine for API routes and SSR. When an ORM such as Prisma or Drizzle opens a database connection per request, each serverless invocation can create a new connection, rapidly exhausting the pool under burst traffic.
Nitro does not expose a built‑in request/response hook that guarantees a connection is returned to the pool before the response is sent. In long‑running Node.js processes a singleton client can reuse pooled connections, but on serverless platforms (Vercel, Netlify, Cloudflare Workers) globals may be reset between invocations, breaking the singleton pattern.
The unresolved behavior leaves developers to rely on ad‑hoc middleware or event.waitUntil workarounds, which may be cut off by function timeouts.
29775 reputation · 24 Apr 2024, 08:07 UTC
1. Does Nitro provide a cleanup guarantee? No. Nitro has no request/response hook that forces the ORM to close or return a connection before a serverless function freezes. event.waitUntil is best‑effort and may be cut off by the platform’s timeout.
2. How to size the pool? The rule of thumb is:
max_connections_on_DB >= max_concurrent_instances × per_instance_pool_size.
When an external pooler (PgBouncer, Prisma Accelerate, RDS Proxy, etc.) sits between the function and the DB, keep per_instance_pool_size to 1‑2 connections and size the pooler to the DB limit.
3. Pattern for preserving a pooled client across invocations. Store the ORM client on globalThis so that warm instances reuse the same TCP connection pool. In serverless, the global survives across warm invocations; cold starts may open a new pool, but the pooler limits the total number of concurrent connections.
In a typical Nuxt 3 + Nitro setup, each incoming request executes the route handler in a fresh execution context. If the handler creates a new ORM client (e.g., new PrismaClient() or createConnection()), that client opens a fresh TCP connection or adds to its internal pool. Nitro does not wait for that client to finish before the function terminates, so the connection can remain open until the platform’s freeze or until the DB times out. Under burst traffic, many cold instances spawn concurrently, each opening a connection, quickly hitting the database’s max_connections limit.
export const getPrisma = () => {
if (!(globalThis as any).prisma) {
(globalThis as any).prisma = new PrismaClient({
log: ['query'],
});
}
return (globalThis as any).prisma;
};
export default defineEventHandler(async (event) => {
const prisma = getPrisma();
const users = await prisma.user.findMany();
return users;
});
max_connections (e.g., 100). Keep the per‑function pool to 1 connection.
hey -n 1000 -c 50 http://yourapp/api/users) and monitoring pg_stat_activity or the pooler metrics to confirm connections do not grow beyond the pooler limit.
if (!(globalThis as any).prisma) {
console.log('Creating new Prisma client');
}
| Platform | Max concurrent instances | Per‑instance pool size | DB connections used |
|---|---|---|---|
| Node server | 1 | 1‑2 | 1‑2 |
| Vercel serverless | ~10 | 1 | ~10 |
| Netlify serverless | ~15 | 1 | ~15 |
| Cloudflare Workers (V8) | ~50 | 1 | ~50 |
To fine‑tune the pool size and choose the right pooler, could you let us know which database provider and driver you are using (PostgreSQL, MySQL, SQLite, etc.) and which Nitro preset (node-server, vercel, netlify, cloudflare) you target?
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.