Skip to content
ExponaBLOG
NESTJS · DAY 013 / 100

Prisma connection management: what developers should know

Connection pool exhaustion is the classic Prisma production bug.

Expona Admin6 min read

Prisma connection management

I've watched production incidents unfold because developers treated database connections like they were free. Prisma connection management is one of those foundational skills that separates stable platforms from ones that mysteriously grind to a halt under load. In this article, I'll walk you through the mistakes I've made and the patterns that actually work.

Understanding the connection lifecycle

Before we talk about management, let's be clear about what's happening under the hood. Every time your NestJS application connects to PostgreSQL through Prisma, you're creating a connection object that holds a TCP socket, memory buffers, and authentication state. That connection doesn't disappear when your query finishes—it stays open in a pool, waiting for reuse.

The problem emerges when you create too many connections and your database runs out of capacity to accept new ones. Your application then hangs, times out, or crashes. I learned this the hard way on a ride-hailing platform when we scaled from 50 concurrent requests to 500 overnight. The code didn't change, but the connection pool was exhausted within minutes.

The root cause was almost always one thing: multiple PrismaClient instances running in the same process. Each one maintains its own connection pool. Multiply that by dozens of modules, and you've created a connection leak that compounds under load.

One PrismaClient per process — never per request

This is the single most important rule. Your NestJS application should instantiate PrismaClient exactly once and share that instance across all requests, modules, and services.

Here's the right pattern using a service:

typescript
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

Then register it as a provider in your root module:

typescript
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

@Module({
  imports: [PrismaModule],
})
export class AppModule {}

Now any service that needs database access injects PrismaService and reuses the same client:

typescript
@Injectable()
export class UserService {
  constructor(private prisma: PrismaService) {}

  async findUser(id: string) {
    return this.prisma.user.findUnique({ where: { id } });
  }
}

The mistake I see constantly is creating a new PrismaClient in middleware, interceptors, or even within request handlers. Every fresh instantiation adds a new connection pool to memory. Even if you're careful, you'll accumulate instances over the lifetime of your application.

Tune connection_limit in the connection string

The connection_limit parameter in your database URL controls how many connections Prisma will keep open in its pool. The default is 10, which is fine for development but will cause bottlenecks in production.

Your connection string looks like this:

typescript
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public&connection_limit=20"

The connection_limit=20 tells Prisma to maintain a maximum of 20 concurrent connections to your database. Here's how to think about sizing it:

  • Single instance with low concurrency (under 50 concurrent requests): 10–15 connections is enough.
  • High-traffic single instance (hundreds of concurrent requests): 30–50 connections.
  • Multiple instances (Kubernetes, Fargate): Lower per-instance (10–20) and rely on the pooler to manage across pods.

On a CRM platform I worked on, we set connection_limit=10 for 12 containerized instances, giving us 120 connections total to the database. PostgreSQL's max_connections was set to 200, leaving room for admin queries and safety margin. That configuration survived sustained load without connection timeouts.

Too few connections and your application queues up requests waiting for a free slot. Too many and your database becomes resource-starved managing all those connections. There's no magic number—you need to monitor your actual usage and adjust.

Use a pooler like PgBouncer for serverless

Serverless functions (AWS Lambda, Google Cloud Functions) create a new process for each invocation. If each Lambda maintains its own Prisma connection pool, you'll exhaust the database's connection limit within seconds under any meaningful load.

That's where a connection pooler comes in. PgBouncer sits between your application and PostgreSQL, reusing a smaller set of real database connections across many client connections.

Instead of connecting directly to your database:

typescript
DATABASE_URL="postgresql://user:password@mydb.rds.amazonaws.com:5432/mydb"

You connect to PgBouncer:

typescript
DATABASE_URL="postgresql://user:password@pgbouncer.internal:6432/mydb"

PgBouncer then multiplexes those connections to your actual PostgreSQL instance. A basic PgBouncer config looks like this:

typescript
[databases]
mydb = host=mydb.rds.amazonaws.com port=5432 dbname=mydb

[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
reserve_pool_size = 5
reserve_pool_timeout = 3

The pool_mode = transaction setting means each connection is returned to the pool after every transaction completes. This is essential for serverless because Lambda functions are short-lived.

I've seen teams deploy PgBouncer on EC2 or as a container in their Kubernetes cluster. The operational overhead is minimal compared to the alternative—a database that's constantly rejecting new connections.

Close connections on shutdown with enableShutdownHooks

When your NestJS application shuts down, Prisma needs to gracefully close all open connections. Without this, connections remain in a half-open state and accumulate on your database server.

NestJS provides a lifecycle hook for this. In your main application bootstrap file, enable shutdown hooks:

typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableShutdownHooks();
  await app.listen(3000);
}

bootstrap();

Then implement the OnModuleDestroy interface in your PrismaService:

typescript
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

When NestJS receives a termination signal (SIGTERM or SIGINT), it calls onModuleDestroy, which gracefully closes all Prisma connections before the process exits.

This matters especially in containerized environments. Kubernetes sends SIGTERM to pods before killing them. If your connections don't close cleanly, you accumulate zombie connections on the database that consume resources for minutes or hours until timeout.

Common mistakes

Creating PrismaClient in a factory or middleware: I've seen this pattern in codebases that swear they're following best practices. Middleware runs on every request, so creating a new client there is a guarantee of exhaustion.

Forgetting to call $connect() and $disconnect(): Some developers assume Prisma auto-connects. It doesn't. Without explicit $connect() in your module init hook, the first query incurs connection setup latency. Without $disconnect() on shutdown, you leak connections.

Setting connection_limit too high: I've seen teams set connection_limit=200 thinking "more is better." Your database then spends all its CPU juggling those connections instead of executing queries. Measure, don't guess.

Not monitoring connection usage: Most teams don't know their actual connection count until it breaks. Add a metric. On production systems I maintain, I log pg_stat_activity count every 60 seconds and alert if it climbs above 80% of max_connections.

Key takeaways

  • Maintain exactly one PrismaClient instance per process and share it via dependency injection.
  • Tune connection_limit based on your expected concurrency and instance count, not arbitrarily.
  • Deploy a connection pooler like PgBouncer when using serverless functions or multiple application instances.
  • Implement OnModuleDestroy and call $disconnect() to gracefully close connections on shutdown.
  • Monitor connection usage in production—connection exhaustion is silent until it crashes your application.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles