Prisma ignored my connection limit: connection pooling with driver adapters
I added connection_limit to my database URL and thought the pool was under control. With Prisma's PostgreSQL driver adapter, that parameter is silently ignored. Here is how connection pooling really works with driver adapters, why it matters on a shared database, and how to size the pool.
The problem
The pool size set in the database URL (connection_limit) had no effect, so the service could open more connections than intended on a shared PostgreSQL server.
The result
The pool is now capped explicitly in code and configurable per environment; bursts queue inside the service instead of exhausting the shared database.
Stack
The problem
After fixing an export that fired thousands of parallel queries (see the previous post, "Too many clients"), I wanted a second line of defense: a hard cap on how many database connections this service can ever open.
The database server is shared with other services. If one service grabs too many connections, everyone else gets sorry, too many clients already. So the rule was simple: this service gets a fixed share, no matter what its code does.
With classic Prisma, the standard way to do that is a URL parameter:
postgresql://user:password@host:5432/app?connection_limit=10
I checked our setup and found something important in how the client was created: we were using Prisma's PostgreSQL driver adapter (@prisma/adapter-pg). And with a driver adapter, connection_limit in the URL is silently ignored. No warning, no error. The service simply didn't have the limit everyone assumed it had.
The details
Two ways Prisma talks to PostgreSQL
To understand why the parameter is ignored, you need to know who actually owns the connections.
1. The classic way (Prisma's own engine). Prisma's query engine opens and manages the connections itself. It reads pool settings from the URL: connection_limit, pool_timeout, and so on.
2. The driver adapter way. You create a normal JavaScript database driver (here node-postgres, the pg package) and hand it to Prisma. Prisma builds the SQL, but the driver owns the connections:
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });
In this setup, the pool is a node-postgres pool. node-postgres has its own options (max, idleTimeoutMillis, connectionTimeoutMillis…), and it does not know about Prisma's connection_limit. The URL parameter is Prisma-engine vocabulary, and the engine is no longer the one managing connections.
So the effective pool was node-postgres's default: 10 connections per instance, regardless of what the URL said.
Why "10 per instance" is not one number
The dangerous word is per instance. Connections add up across everything that runs your code:
total connections = pool size × instances × (processes per instance)
| Setup | Pool size | Instances | Total possible |
|---|---|---|---|
| 1 container | 10 | 1 | 10 |
| 3 replicas | 10 | 3 | 30 |
| 3 replicas + 1 worker + 1 cron job | 10 | 5 | 50 |
And this is only one service. On a shared server, other services add their own pools. PostgreSQL's default max_connections is often 100, and a few slots are reserved for superusers. It goes faster than you think.
Why default settings feel fine until they don't
At normal traffic, a service rarely uses more than a few connections at the same time. The pool default never matters. It matters during bursts: a big export, a batch import, a retry storm after a network blip. That is exactly when the database is already under pressure, and exactly when you need a predictable limit.
The challenges
The misconfiguration was invisible
Nothing failed. The URL parameter was accepted, the app connected, queries ran. The only symptom was that the real limit was different from the intended one, which you can't see until a burst happens.
Copy-pasted knowledge
Most tutorials and Stack Overflow answers about Prisma pooling describe the classic engine and connection_limit. That advice is correct for one setup and silently wrong for the other. When you switch to driver adapters, a part of your team's knowledge becomes outdated without anyone noticing.
Choosing the right number
A pool that is too small makes requests wait. A pool that is too large can hurt the database: PostgreSQL uses a process per connection, and hundreds of active connections compete for CPU, memory and locks. More connections is not more throughput past a certain point.
Logs drowning the signal
While investigating, I also noticed the client logged every query in every environment. During a burst of thousands of queries, the production logs became useless at the exact moment I needed them.
The solution
1. Set the pool where the pool actually lives
The limit goes on the driver adapter, using node-postgres's own option, max:
@Injectable()
export class DatabaseService extends PrismaClient {
constructor(config: ConfigService) {
const adapter = new PrismaPg({
connectionString: config.get('DATABASE_URL'),
// The database server is shared with other services, so our share
// is capped explicitly: a burst queues here instead of taking server
// slots. With a driver adapter, `connection_limit` in the URL is ignored.
max: Number(config.get('DB_POOL_MAX') ?? 10),
});
super({ adapter });
}
}
Three details matter here:
- The comment explains why. The next developer who sees
connection_limitin a tutorial will understand why it isn't in our URL. - It is configurable (
DB_POOL_MAX), so each environment can be tuned without a code change. - It has a safe default. If the variable is missing, the service still has a known limit.
2. Understand what happens when the pool is full
With max set, a burst no longer opens more connections. Extra queries wait in a queue inside the service until a connection is free. The service may become slower during a burst, but:
- the shared database keeps its free slots,
- other services keep working,
- the slow part is isolated to the service that caused the burst.
That is the trade I wanted: degrade locally, never globally.
3. Size the pool with arithmetic, not guesses
My rule of thumb for a shared server:
(max_connections − reserved slots) ≥ Σ over all services (pool size × instances)
Start from what the database can actually serve, divide it fairly between services and instances, and leave headroom for migrations, admin sessions and monitoring. If you scale a service from 2 to 6 replicas, its total connections triple, so either lower the pool per instance or add a pooler like PgBouncer in front of the database.
4. Make logs useful again
Per-query logging stays in development, where it helps debugging. In production, only warnings and errors:
super({
adapter,
log: process.env.NODE_ENV === 'production'
? ['warn', 'error']
: ['query', 'info', 'warn', 'error'],
});
5. Fix the cause too
A pool cap is a safety net, not a fix. The real cause of the bursts was an N+1 loop that fired thousands of queries for one export. That loop was rewritten to use a fixed number of bulk queries. With both changes, the service does far less work and can never exceed its share.
Lessons learned
- Know who owns the connections. With a driver adapter, the driver owns the pool, and pool settings must use the driver's options.
- "Accepted" is not "applied". A config value that is silently ignored is worse than one that fails loudly. Verify important limits in the running system.
- Pool size is per instance. Always multiply by replicas, workers and cron jobs before deciding a number.
- On a shared database, cap explicitly. A burst should slow down the service that caused it, not every service on the server.
- Write the "why" next to the config. One comment saves the next developer from "fixing" it back to the tutorial version.
- Keep production logs readable. Per-query logs at scale hide the errors you are looking for.
- A safety net is not a cure. Cap the pool and remove the burst at its source.
FAQ
Does Prisma's connection_limit work with driver adapters?
No. With a driver adapter such as @prisma/adapter-pg, the underlying driver (node-postgres) manages the connection pool, so you configure the pool with the driver's options, for example max, instead of connection_limit in the URL.
What is the default pool size of node-postgres?
node-postgres's Pool defaults to a maximum of 10 clients per pool. Remember that this is per pool, so every instance of your service has its own 10.
How many connections should my PostgreSQL pool have?
Start from your database's max_connections, subtract reserved and admin slots, and share the rest across all services and all their instances. More connections stops adding throughput after a point and can slow PostgreSQL down, so smaller pools with queuing are often better.
When should I use PgBouncer?
When you have many instances or services and the sum of their pools approaches the database's connection limit. PgBouncer keeps a small number of real database connections and shares them between many application connections.
Facing a similar problem?
I help teams fix performance, backend and reliability issues in production.
Let's talk