Launch offer: the first 1,000 users get Settl free for a year*Claim your spot
settlbuilding in public

The serverless-Postgres survival kit (the exact 2-line fix + why)

My database hung up every five minutes and my server died of surprise. Neon (like most serverless Postgres) closes idle TCP connections server-side after 3-5 minutes. My pg.Pool kept handing out those dead connections; the connection's async error event had no listener; Node treats an unhandled 'error' as uncaughtException; the whole serverless instance died, and the next user paid the cold start. Two lines fixed it.

The exact fix

import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  idleTimeoutMillis: 10_000,   // line 1: WE close idle conns before Neon does
});

pool.on('error', (err) => {    // line 2: async conn failures ≠ process death
  console.warn('pg pool error (connection retired)', err.message);
});

Why each line matters

  1. idleTimeoutMillis: 10_000: the race is "who closes the idle connection first." If Neon wins (3-5 min), your pool holds a corpse and the next query finds out. If you win (10s), the pool retires connections cleanly and opens fresh ones on demand.
  2. pool.on('error', ...): a pooled connection can fail while idle, between queries. That error is emitted on the pool, not thrown to any caller. No listener → uncaughtException → process death → cold start for whoever's next. With the listener, the pool logs a warning and retires the bad client. It already knows how. It just needs you to not crash first.

The rest of the survival kit

  1. Do NOT "fix" this with a keepalive cron. Pinging the DB to keep connections warm defeats auto-suspend and burns your compute quota. I have a separate outage story about exactly that (settl.fyi/social/cron-outage).
  2. Expect the first request after idle to reconnect. A fresh handshake in-region is ~30ms. That's the correct price; pay it.
  3. Keep pool sizes tiny on serverless functions. Many concurrent instances × big pools = connection-limit blowout. 1-5 connections per instance, or use the provider's pooled connection string (pgbouncer).
  4. Watch for the signature in your error tracker: Connection terminated unexpectedly with an onuncaughtexception mechanism. That exact pair = this exact bug.
  5. Test it deliberately: deploy, wait 6 idle minutes, fire one request. If p99 spikes or the instance restarts, you haven't fixed it.

Steal this for your app

Run this on your codebase

Paste this into Claude Code in your repo:

Audit this repo for the idle-connection crash.
Find every pg.Pool (or equivalent client pool) and check whether idleTimeoutMillis is set below the provider's server-side idle close window (Neon and Supabase close at 3-5 minutes).
Check each pool for a pool.on('error') handler; flag any pool without one as a scheduled process crash.
List every other long-lived resource (Redis client, WebSocket, queue consumer) missing an error listener.
Check pool max size against a serverless deployment; flag anything over 5 connections per instance without a pgbouncer-style pooled connection string.
Search for keepalive crons or warmers that ping the DB and flag them.
Report findings as a checklist before changing anything.

CONNECT: budget connections before production does it for you

Pool size is not a per-process preference. It is a database-wide budget. Write the constraint down:

(maximum concurrent app instances × pool size)
+ background workers + migrations + admin reserve
<= database connection cap - safety headroom

Use maximum concurrent instances, not the number running during a quiet test. Then load-test that many instances together and watch active connections, wait time, rejected checkouts, and database CPU. Alert before the hard cap, with enough headroom for a migration or incident investigation.

If the multiplication does not fit, put a pooler in front of the database. Transaction pooling is usually right for short stateless queries. Session pooling is required when code depends on session-level state, prepared statements, temporary tables, or advisory locks. Test those assumptions explicitly instead of treating every pooled URL as interchangeable.

Download the runnable pack

The pack turns the connection-budget inputs into a repeatable dry-run and includes an n8n endpoint, Docker Compose environment, and GitHub Actions validation. No database is changed by the sample run.

Get the next one in your inbox