Every web application that uses a database faces the connection management problem. In traditional server architectures, the server maintains a pool of connections that are reused across requests. In serverless and Next.js dev mode, the problem is more acute. Here is what it is and how to fix it.
The Problem: One Connection Per Invocation
A serverless function is stateless. Each invocation (each API request) is potentially a new execution context. Without careful management, each invocation opens a new database connection.
A PostgreSQL instance has a default connection limit of 100. MongoDB Atlas free tier allows 500 connections. If your application handles 200 simultaneous requests, and each request opens a new connection, you exhaust the connection limit and subsequent requests fail with "too many connections" errors.
This is not hypothetical. It is the first scaling problem most Next.js applications hit when they start getting real traffic.
The second problem: Next.js development mode. Next.js reloads modules in development when you change code. Without a global singleton, each reload creates a new database connection. After 20 changes, you have 20 idle connections. Over a day of development, you hit the connection limit.
The MongoDB Singleton Pattern for Next.js
The solution for MongoDB in Next.js is a global singleton. The MongoClient instance is stored on the global object so it persists across module reloads in development and is reused across invocations in the same container in production.
// lib/mongodb/client.ts
import { MongoClient, Db } from "mongodb";
const uri = process.env.MONGODB_URI!;
if (!uri) throw new Error("MONGODB_URI environment variable is not set");
let client: MongoClient;
let clientPromise: Promise<MongoClient>;
if (process.env.NODE_ENV === "development") {
// In development, use a global variable so the connection is preserved
// across module reloads caused by HMR (Hot Module Replacement).
const globalWithMongo = global as typeof globalThis & {
_mongoClientPromise?: Promise<MongoClient>;
};
if (!globalWithMongo._mongoClientPromise) {
client = new MongoClient(uri);
globalWithMongo._mongoClientPromise = client.connect();
}
clientPromise = globalWithMongo._mongoClientPromise;
} else {
// In production, it is fine to create a new client in each module
// because serverless containers are reused across invocations.
client = new MongoClient(uri);
clientPromise = client.connect();
}
export async function getDatabase(dbName?: string): Promise<Db> {
const mongoClient = await clientPromise;
return mongoClient.db(dbName ?? process.env.MONGODB_DB_NAME);
}
Usage in any route handler or Server Component:
import { getDatabase } from "@/lib/mongodb/client";
const db = await getDatabase();
const users = await db.collection("users").find({ active: true }).toArray();
MongoDB's Node.js driver has a built-in connection pool. By default it maintains up to 5 connections. You can configure this with the maxPoolSize option:
client = new MongoClient(uri, { maxPoolSize: 10 });
Increase this if your application is connection-bound (many concurrent requests waiting for a connection). The total connections your MongoDB instance sees equals maxPoolSize times the number of running server instances.
PostgreSQL: The Different Problem
PostgreSQL's connection model is heavier than MongoDB's. Each PostgreSQL connection spawns an OS process and consumes significant memory (5-15 MB per connection). The default max_connections is 100, and hitting that limit causes hard failures.
With Prisma in Next.js, each Prisma Client instance creates its own connection pool. Without the global singleton pattern, you create many Prisma instances and exhaust connections quickly.
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["query"] : [],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
But even with the singleton, a busy Next.js deployment with 10 instances will open up to 10 * connection_limit connections. With Prisma's default pool of 5-10, that is 50-100 connections from Next.js alone.
For production PostgreSQL deployments, a connection pooler is necessary. PgBouncer sits between your application and PostgreSQL, maintaining a small pool of actual PostgreSQL connections and allowing many more application connections.
Managed services like Supabase and Neon include built-in connection poolers accessible via a separate connection string (the "pooled" connection string). Use the pooled connection string in your application, not the direct connection string.
Neon and PlanetScale: Managed Connection Pooling
Neon is a serverless PostgreSQL provider with HTTP-based queries. Instead of a persistent TCP connection, each query is an HTTP request. This is naturally serverless-compatible. No connection limits in the traditional sense. The Neon JavaScript client handles the HTTP transport transparently.
PlanetScale (before its pricing change) was MySQL-based with a proxy layer that handled connection management. The Vitess underlying technology that PlanetScale uses is now available as a self-hosted option.
Both solve the connection pooling problem by design. If you are starting a new project with PostgreSQL, Neon is worth considering specifically because the connection model is serverless-native.
When Connection Pooling Is Not Enough
If you are running hundreds of Next.js serverless instances and each needs database access, even a connection pooler may not be sufficient. At that scale, consider:
Read replicas. Distribute read traffic across multiple database instances. Most application traffic is reads. Sending writes to the primary and reads to replicas reduces load on each instance.
Caching. Redis or in-memory caching for frequently read data that does not change often. A leaderboard that is computed from database queries but cached for 60 seconds handles 60x the read traffic with the same database load.
Architecture change. If a single database connection pooler is a bottleneck, the application may have outgrown a single database instance. Database sharding, read replicas, or a different data storage strategy becomes necessary.
For most applications (under 10k concurrent users), the global singleton pattern with a managed database's built-in connection pooler is sufficient.
Practical Checklist
When setting up database access in a new Next.js project:
- Use a global singleton for the database client
- Set a reasonable connection pool size (
maxPoolSizefor MongoDB, Prisma'sconnection_limitfor PostgreSQL) - Use the pooled connection string if your database provider offers one
- Monitor active connections in your database dashboard, especially during load tests
- Set connection timeouts to fail fast rather than queue indefinitely (
serverSelectionTimeoutMSfor MongoDB,connect_timeoutfor PostgreSQL)
Keep Reading
- Next.js API Routes Best Practices: Patterns for Production APIs -- database queries in route handlers
- Next.js Caching in 2026: The Complete Guide to All Four Layers -- caching to reduce database load
- Next.js Performance Optimization: The Practical Guide for Production Apps -- N+1 query patterns and database optimization
Pristren builds AI-powered software for teams. Zlyqor is our all-in-one workspace -- chat, projects, time tracking, AI meeting summaries, and invoicing -- in one tool. Try it free.