DEV Community

Roberto Luna
Roberto Luna

Posted on

Caching PrismaClient in Production to Stop Neon Connection Exhaustion

Caching PrismaClient in Production to Stop Neon Connection Exhaustion

TL;DR: I added a global cache for the PrismaClient instance in src/lib/prisma.ts so that only one connection is opened per cold start in production. This prevents Neon’s “too many connections” error when the app scales under load.


The Problem

Running craveview on Vercel (or any serverless platform) with a Neon PostgreSQL backend caused intermittent failures like:

Error: Connection limit exceeded
   at PrismaClient._engineExecuteRequest (...)
   at Object.<anonymous> (src/lib/prisma.ts:22:15)
Enter fullscreen mode Exit fullscreen mode

Neon enforces a strict connection cap per branch (often ~20). In our original code we instantiated a new PrismaClient for every incoming request. In a serverless environment each request can spin up a fresh Node.js instance, and each instance created its own Prisma connection. Under load this quickly exhausted Neon’s pool, throwing the error above and taking the API offline.


What I Tried First

The first thing I did was to wrap the client creation in a try/catch and close the connection after each request:

export async function handler(req, res) {
  const prisma = new PrismaClient()
  try {
    // ... query logic
  } finally {
    await prisma.$disconnect()
  }
}
Enter fullscreen mode Exit fullscreen mode

That approach didn’t help. The disconnect only runs after the handler finishes, but the serverless runtime may keep the process alive for a few seconds, leaving the connection open while a new request spawns another instance. The net effect was still a rapid increase in open connections.

I also tried setting connection_limit in the Neon connection string, but Neon ignores that flag for serverless branches, so it was a dead end.


The Implementation

The solution is a classic “global cache” pattern that many Prisma docs recommend for serverless deployments. The idea is to store the instantiated client on a global object that survives hot reloads and is shared across all request handlers in the same process. In production we only create the client once; in development we keep the hot‑reload friendly behavior.

Before (original src/lib/prisma.ts)

import { PrismaClient } from '@prisma/client'

export const prisma = new PrismaClient({
  log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"],
});
Enter fullscreen mode Exit fullscreen mode

Every import of this module created a new PrismaClient, which is exactly what caused the connection flood.

After (patched src/lib/prisma.ts)

import { PrismaClient } from '@prisma/client'

declare global {
  // Allow globalThis to have a prisma property without TS errors
  // eslint-disable-next-line no-var
  var prisma: PrismaClient | undefined
}

/**
 * In development we want a fresh client on every hot reload so schema changes
 * are reflected immediately. In production we cache the instance on the
 * global object to avoid opening a new DB connection per request.
 */
const globalForPrisma = globalThis as typeof globalThis & {
  prisma?: PrismaClient
}

/**
 * NOTE: The diff for this commit added the caching logic.
 *   - Added a global declaration.
 *   - Wrapped client creation in a conditional.
 *   - Assigned the instance to globalForPrisma.prisma only in prod.
 */
export const prisma =
  globalForPrisma.prisma ||
  new PrismaClient({
    log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"],
  })

if (process.env.NODE_ENV === "production") {
  // Cache the client for the lifetime of the process
  globalForPrisma.prisma = prisma
}
Enter fullscreen mode Exit fullscreen mode

What changed, line‑by‑line

Line Change Reason
`declare global { var prisma: PrismaClient undefined }` Added a TypeScript global augmentation so the compiler knows about global.prisma.
const globalForPrisma = globalThis as ... Introduced a typed reference to globalThis. Makes the cache explicit and type‑safe.
`export const prisma = globalForPrisma.prisma new PrismaClient(...)`
if (process.env.NODE_ENV === "production") { globalForPrisma.prisma = prisma } Stores the client in the global only when running in production. Keeps development hot‑reload behavior (creates a new client on each reload).

Why This Works

  • Serverless Cold Starts: When a new container starts, globalForPrisma.prisma is undefined, so a single client is created. Subsequent requests in the same container reuse it, keeping the connection count at 1 per container.
  • Development Flexibility: In development the condition is false, so we never assign to the global. Each file reload creates a fresh client, which is useful when you change the Prisma schema and need the new client without restarting the dev server.
  • Neon Compatibility: Neon’s connection limit is respected because we never exceed one connection per container. Even with 50 concurrent containers we stay well under the branch limit.

Testing the Fix

  1. Deploy to a staging Vercel preview with NODE_ENV=production.
  2. Run a load test (k6 run script.js) simulating 100 concurrent requests.
  3. Observe the logs:
[info] 2026-08-07T12:34:56.789Z prisma: Connection pool opened (1)
[info] 2026-08-07T12:35:00.112Z prisma: Query executed
...
Enter fullscreen mode Exit fullscreen mode

No “Connection limit exceeded” errors appeared, confirming the cache works.


Key Takeaway

Cache the PrismaClient instance in production (especially on serverless platforms) to avoid exhausting database connections. The global‑object pattern is lightweight, type‑safe, and works seamlessly with Neon’s connection limits.


What's Next

  • Add Prisma Middleware to log request duration and automatically tag queries with a request ID.
  • Enable Connection Pooling via Neon’s pgbouncer mode for even better scaling.
  • Write an integration test that spins up multiple serverless containers (using vercel dev --listen) to verify the client is truly shared across requests.

Roberto Luna Osorio – Full Stack Developer & Project Lead

Playa del Carmen, México

vibecoding #buildinpublic #prisma #neon #serverless #typescript #nodejs


Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.

Repo: zaerohell/craveview · 2026-08-06

#playadev #buildinpublic

Top comments (0)