DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Neon Serverless Postgres: Connection Pooling Lessons From 6 Months in Production

Serverless Postgres sounds perfect until your Vercel function hits too many connections under moderate traffic. Here's what I learned running Neon in production for 6 months across three SaaS apps.

The Problem With Serverless + Postgres

Traditional Postgres holds one OS-level process per connection. Each Next.js serverless function invocation wants its own connection. With 100 concurrent requests, that's 100 Postgres processes — most databases cap out around 100-200 connections before performance degrades.

Neon solves this two ways:

  1. Neon's built-in connection pooler (PgBouncer under the hood) — pool URL ends in -pooler.neon.tech
  2. @neondatabase/serverless driver — WebSocket-based, opens connections faster than TCP, works in edge runtimes

Most tutorials tell you to use one. You often need both.

Setup: The Right Way

Install

npm install @neondatabase/serverless drizzle-orm drizzle-orm/neon-serverless
Enter fullscreen mode Exit fullscreen mode

Two connection strings — know when to use each

# .env.local
# Standard connection — use for migrations, seeds, long-running queries
DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/mydb

# Pooled connection — use for serverless functions, API routes
DATABASE_POOLED_URL=postgresql://user:pass@ep-xxx-pooler.us-east-2.aws.neon.tech/mydb?pgbouncer=true
Enter fullscreen mode Exit fullscreen mode

The ?pgbouncer=true flag disables prepared statements — PgBouncer doesn't support them in transaction mode. Miss this and you'll get cryptic errors.

Drizzle + Neon setup

// lib/db.ts
import { neon, neonConfig } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'
import * as schema from './schema'

// Enable WebSocket pooling in Node.js environments
neonConfig.fetchConnectionCache = true

// Use pooled URL for API routes
const sql = neon(process.env.DATABASE_POOLED_URL!)
export const db = drizzle(sql, { schema })
Enter fullscreen mode Exit fullscreen mode
// lib/db-migrate.ts — separate file for migrations
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import * as schema from './schema'

// Use standard URL (not pooled) for migrations
const pool = new Pool({ connectionString: process.env.DATABASE_URL! })
export const dbMigrate = drizzle(pool, { schema })
Enter fullscreen mode Exit fullscreen mode

Connection Limits in Practice

Neon's free tier: max 100 connections total across all branches.
With PgBouncer pooler: each pool counts as one server-side connection regardless of how many clients.

I've run 500 concurrent serverless function invocations against a free-tier Neon project without hitting limits — as long as every function uses the pooled URL.

The Mistakes That Burned Me

1. Running migrations against the pooled URL

PgBouncer in transaction mode doesn't support SET commands, advisory locks, or multi-statement transactions — all things Drizzle's migration runner uses. Always run drizzle-kit migrate against the standard (non-pooled) URL.

Add this to your package.json:

{
  "scripts": {
    "db:migrate": "DATABASE_URL=$DATABASE_URL drizzle-kit migrate",
    "db:studio": "DATABASE_URL=$DATABASE_URL drizzle-kit studio"
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Not using branches for staging

Neon's branching is the actual killer feature. Create a branch per PR:

# .github/workflows/preview.yml
- name: Create Neon branch
  uses: neondatabase/create-branch-action@v5
  with:
    project_id: ${{ secrets.NEON_PROJECT_ID }}
    branch_name: preview/${{ github.event.number }}
    api_key: ${{ secrets.NEON_API_KEY }}
Enter fullscreen mode Exit fullscreen mode

Each preview environment gets its own Postgres with data copied from main — no shared state between PRs, no test data leaking.

3. Forgetting ?sslmode=require in non-Neon environments

Neon requires SSL. If you ever point the same codebase at a local Postgres, make sure your connection string handling is environment-aware:

const connectionString = process.env.NODE_ENV === 'development' && process.env.DATABASE_URL?.includes('localhost')
  ? process.env.DATABASE_URL
  : `${process.env.DATABASE_POOLED_URL}${process.env.DATABASE_POOLED_URL?.includes('?') ? '&' : '?'}sslmode=require`
Enter fullscreen mode Exit fullscreen mode

Edge Runtime Compatibility

The @neondatabase/serverless driver works in Vercel Edge Functions and Cloudflare Workers — it uses fetch/WebSockets, not Node.js net. The standard pg driver does not.

// app/api/fast/route.ts — edge runtime
export const runtime = 'edge'

import { neon } from '@neondatabase/serverless'

export async function GET() {
  const sql = neon(process.env.DATABASE_POOLED_URL!)
  const [row] = await sql`SELECT count(*) FROM users`
  return Response.json({ count: row.count })
}
Enter fullscreen mode Exit fullscreen mode

If you're using Drizzle with edge, use drizzle-orm/neon-http not drizzle-orm/node-postgres.

Observability

Neon's dashboard shows active connections, query counts, and compute time. The metric to watch is active connections on the pooler — if it consistently stays near your limit (100 for free, 500 for Launch), you're close to the ceiling.

I log slow queries via Drizzle's logger:

class SlowQueryLogger implements Logger {
  logQuery(query: string, params: unknown[]) {
    const start = performance.now()
    // wrap in performance.now() post-execution to log > 1000ms
  }
}
Enter fullscreen mode Exit fullscreen mode

For production monitoring, Neon integrates directly with Datadog and exports to OpenTelemetry.

Summary

  • Always use the pooled URL (-pooler.neon.tech) in serverless functions
  • Use the standard URL for migrations only
  • Add ?pgbouncer=true to the pooled URL with Drizzle
  • Use Neon branches for preview environments — it's the feature that justifies the platform
  • @neondatabase/serverless is edge-compatible; pg is not

Shipping a Next.js SaaS with Postgres? The AI SaaS Starter Kit has Neon + Drizzle + Stripe + Claude pre-wired with correct pooling setup out of the box.

Top comments (0)