Module P-5·12 min read

Setting up Prisma with the App Router, why new PrismaClient() per request will kill your database at scale, PgBouncer and Neon for serverless pooling, and the full mutation pattern via Server Actions.

Written by Jatin Jain Saraf · Architect of SupraScan

P-5 — Database Integration: Prisma, PostgreSQL, and Connection Pooling

Who this is for: Practitioners who need to connect a Next.js App Router application to a PostgreSQL database using Prisma. This module goes beyond "run prisma generate and it works" — it covers the connection pooling failure mode that takes down serverless applications at scale, the correct singleton pattern, and the full mutation cycle from Server Action to database to cache invalidation.


The Problem You Will Hit at Scale

Let me front-load the most important thing in this module, because it's the issue most engineers only discover in production.

In a serverless environment (Vercel Functions, AWS Lambda), each function invocation is an isolated process. Without connection pooling at the application level, each invocation creates a new database connection. At low traffic, this works fine. At scale:

  • 100 concurrent requests → 100 new database connections opened
  • PostgreSQL's default max connections: 100
  • Result: PrismaClientInitializationError: Unable to start a transaction in the given time

The application starts returning 500 errors. The database is overwhelmed not by query load but by connection overhead.

The solution is connection pooling — a pool of pre-established connections that function invocations borrow and return, rather than opening and closing on every request. There are two levels at which this needs to be solved in a Next.js + Prisma application:

  1. Application-level: The Prisma singleton pattern — ensures one PrismaClient instance is reused across hot-reloaded development sessions and within a single server process.
  2. Infrastructure-level: PgBouncer, Neon connection pooling, or Prisma Accelerate — sits between your serverless functions and PostgreSQL, pooling connections externally.

Both are required. The singleton alone is insufficient for serverless.


Schema Setup

Install Prisma and initialise:

npm install @prisma/client
npm install -D prisma
npx prisma init

This creates prisma/schema.prisma and a .env with DATABASE_URL. A basic schema for an application with posts and users:

// prisma/schema.prisma
 
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id           String   @id @default(cuid())
  email        String   @unique
  name         String?
  passwordHash String?
  role         String   @default("viewer")
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
  posts        Post[]
}
 
model Post {
  id          String   @id @default(cuid())
  title       String
  content     String
  published   Boolean  @default(false)
  publishedAt DateTime?
  authorId    String
  author      User     @relation(fields: [authorId], references: [id])
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
 
  @@index([authorId])
  @@index([published, publishedAt])
}

Generate the Prisma client and push the schema:

npx prisma generate
npx prisma db push  # for development (no migration files)
# or
npx prisma migrate dev --name init  # for production (generates migration files)

The Singleton Pattern — Mandatory in Next.js

In Next.js development, the module system is hot-reloaded on file changes. Without the singleton pattern, each hot reload creates a new PrismaClient instance with its own connection pool — eventually exhausting database connections in development, and creating confusion about which instance is active.

// lib/db.ts
import 'server-only';
import { PrismaClient } from '@prisma/client';
 
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};
 
export const db =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
  });
 
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;

What this does: In development, the first time the module is imported, globalForPrisma.prisma is undefined, so a new PrismaClient is created and stored on globalThis. On subsequent hot reloads, globalForPrisma.prisma already exists, so the same instance is reused. In production, hot reloading doesn't happen, so the globalThis trick isn't needed — the module is imported once.

import 'server-only' ensures this module is never accidentally imported in a Client Component, which would expose database credentials.


Connection Pooling for Serverless

The singleton solves the development hot-reload problem. It does not solve the serverless concurrency problem. In production on Vercel, each serverless function invocation is a separate Node.js process — globalThis doesn't persist between them.

There are three standard solutions:

Option 1: PgBouncer (Self-Hosted)

PgBouncer is a lightweight connection pooler that sits in front of PostgreSQL. Your application connects to PgBouncer; PgBouncer maintains a pool of connections to PostgreSQL.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.