niso.moe
  • Projects
  • Posts
  • Contact

© 2026 niso.moe

  • Imprint
  • Privacy
  1. posts
  2. /
  3. database-connections

Running out of database connections on Vercel

NINikki Sophie 🌸17 Oct 20263 min read0 views
Running out of database connections on Vercel's cover picture

The evening the home page broke

On a Friday evening in October, a few seconds were enough to break this site. The home page answered with a server error, and every request that looked up who was signed in failed. The logs all said the same thing:

Too many database connections opened: too many connections for role "portfolio"

Eighteen of these errors arrived within about six seconds, and then it was quiet again. Nothing had been deployed, and there was hardly any traffic. So what was using up the connections?

How the database is set up

The site runs on Vercel, but its database doesn't. PostgreSQL runs on my own server, next to the databases of a few other projects of mine. Every project gets its own role, and every role has a connection limit, so one misbehaving site can't take the connections the others need. The portfolio's role may hold 15 connections at once.

On the site's side, Prisma talks to the database through a small pg pool:

typescript
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: process.env.NEXT_PHASE === 'phase-production-build' ? 2 : 5, }); // Lets Vercel close idle connections before it suspends the function attachDatabasePool(pool);

Five connections per instance, two per worker during a build. With a limit of 15, that leaves room for three busy instances. It looked like plenty.

Looking at the database itself

The error came from the role's limit, not the server's, so the place to look was pg_stat_activity, which lists every open connection:

sql
select usename, state, count(*), max(now() - state_change) from pg_stat_activity group by 1, 2;

The portfolio role held nine connections, and all of them were idle. Most came from Vercel and had been sitting there unused for several minutes. Two came from my own laptop and had been idle for over two hours, left behind by a dev server I had long stopped.

Nothing was wrong with any single request. The problem was the connections nobody was using any more.

Where idle connections come from

A Vercel function doesn't run all the time. Between requests its instance is paused, and when it isn't needed for a while it is shut down. A connection pool in that instance can only close its idle connections while its code is running. attachDatabasePool from @vercel/functions helps with that by keeping an instance alive until its pool has let go, but as far as I can tell an instance that is frozen or shut down at the wrong moment simply leaves its sockets open.

From the database's point of view, those connections still exist. Nobody will ever use them again, but they count against the role's limit, and PostgreSQL by default never closes an idle session on its own.

So the leftovers piled up quietly. Then a short burst of real traffic started two or three fresh instances, each opening its five connections, and the role hit 15.

Fixing it on both ends

The most reliable fix sits on the side that never sleeps: the database. PostgreSQL can close sessions that have been idle for too long, and the setting can be made for just one role:

sql
alter role portfolio set idle_session_timeout = '2min';

This only applies to sessions opened after the change, so I closed the existing leftovers by hand once with pg_terminate_backend. Since then, a connection that an instance forgets is gone after two minutes at most. The other projects on the server aren't affected.

On the site's side, the pool now hands idle connections back after five seconds instead of the default ten. That doesn't help an instance that is already frozen, but it makes the window smaller. More importantly, the pool now expects the server to close connections:

typescript
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: process.env.NEXT_PHASE === 'phase-production-build' ? 2 : 5, idleTimeoutMillis: 5000, }); // A connection the server closed while idle is dropped from the pool rather than crashing the process pool.on('error', (error) => console.warn('Idle database connection closed:', error.message), );

Without that listener, a connection closed by the server while it sits in the pool emits an error nobody handles, and Node ends the process. With the new timeout on the server, that would have become the next problem.

What I took from it

  • Per-role limits did their job. The portfolio broke, but nothing else on the server did. I'd rather have one site fail loudly than all of them slowly.
  • Clean-up belongs on the side that is always awake. Serverless instances can't promise to close anything. The database can.
  • Look at the database, not just the app. The app's logs only said "too many". pg_stat_activity showed in one query whose connections they were and how long they had been idle.

The errors stopped as soon as the stale connections were closed, and they haven't come back since.

On this page

  1. The evening the home page broke
  2. How the database is set up
  3. Looking at the database itself
  4. Where idle connections come from
  5. Fixing it on both ends
  6. What I took from it
Back to top

Comments 0

Sign in with Discord to join the conversation.

Sign in

No comments yet. Be the first!