Claude Code with Neon: Serverless Postgres Done Right
Why Neon without CLAUDE.md causes connection pool exhaustion
Neon is serverless Postgres that separates compute from storage, scales to zero when idle, and provisions instant copy-on-write database branches. For most Next.js and TypeScript teams in 2026 it is the default Postgres choice. The developer experience is clean: one connection string, automatic branching, usage-based pricing. The problem is that Claude Code's default Postgres patterns were built for long-running servers, and Neon has a fundamentally different connection model.
The most common mistake Claude makes: it generates a new pg.Pool or drizzle() client at the module level with no awareness of serverless function lifecycles. In a traditional server this is correct. In a Next.js API route or Vercel Edge Function, each cold invocation creates a new connection that is never released because the function exits before the pool can drain. Neon's free and launch tiers have connection limits. A few hundred concurrent invocations will exhaust them. The queries stop returning errors. They just queue and time out silently.
The second mistake is using Neon's direct (non-pooled) connection string in production API routes. The direct endpoint is for migrations and admin queries. The pooled endpoint (which routes through PgBouncer in transaction mode) is for application traffic. Claude does not know which URL you pasted into .env.local.
The third mistake involves database branching. Neon lets you create instant copy-on-write branches of your database, which is ideal for preview environments and feature branches. Claude does not know branching exists, so it never suggests it and never configures your preview deploy pipeline to use a branch connection string automatically.
A CLAUDE.md that declares Neon's actual model prevents all three. This guide covers what that file should contain, including the @neondatabase/serverless driver for edge runtimes, the Drizzle and Prisma adapter setup patterns, and the migration workflow that keeps your branches consistent.
If you are connecting a Node.js API layer to Neon, the Claude Code with Prisma guide covers the migration and schema patterns that sit on top of any Postgres provider. For deploying the full application, Claude Code with Vercel shows how preview environments map to Neon branches.
The Neon CLAUDE.md template
# Neon database rules
## Stack
- Neon serverless Postgres (project at console.neon.tech)
- @neondatabase/serverless ^0.x for edge/serverless runtimes
- pg or postgres.js for traditional Node.js server contexts
- Drizzle ORM ^0.30+ OR Prisma ^5.x (pick one per project)
- DATABASE_URL (pooled, PgBouncer) in .env.local
- DATABASE_URL_UNPOOLED (direct) in .env.local
## Connection rules (CRITICAL)
- API routes and edge functions: ALWAYS use DATABASE_URL (pooled endpoint)
- Migrations (drizzle-kit push / prisma migrate): ALWAYS use DATABASE_URL_UNPOOLED
- NEVER create a new Pool() per request in serverless functions
- NEVER use the unpooled connection string for production application traffic
- For edge runtimes (Vercel Edge, Cloudflare Workers): use @neondatabase/serverless with neon() helper
## Edge/serverless driver pattern
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
// Execute: const rows = await sql`SELECT * FROM users WHERE id = ${id}`;
// NEVER use pg.Pool or postgres() in edge contexts
## Drizzle setup (if using Drizzle)
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql);
// For server-side Node.js: use drizzle-orm/neon-serverless with Pool
## Prisma setup (if using Prisma)
- Add to schema.prisma:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DATABASE_URL_UNPOOLED")
}
- url = pooled endpoint (application traffic)
- directUrl = unpooled endpoint (migrations only)
## Database branching
- Development and preview: each feature branch gets a Neon database branch
- Branch connection string: set DATABASE_URL to the branch's pooled endpoint in preview env vars
- Production: main branch connection string only
- NEVER run schema migrations against the production branch from local dev
- NEVER share connection strings across branches
## Migration rules
- Drizzle: drizzle-kit push uses DATABASE_URL_UNPOOLED
- Prisma: prisma migrate deploy uses DATABASE_URL_UNPOOLED (directUrl)
- Run migrations in CI against the target branch, not the production branch
- ALWAYS test migrations on a Neon branch before merging to main
## Hard rules
- NEVER hardcode connection strings in source files
- NEVER use pool.end() patterns in serverless functions (pool lifecycle is managed externally)
- NEVER connect to the direct endpoint from Vercel Edge Functions (protocol incompatibility)
- NEVER ignore Neon's connection limit (free: 10, launch: 100, scale: 500)
- ALWAYS use parameterised queries or ORM methods, never string interpolation
Connection pooling in serverless environments
The pooling distinction matters most in production. Neon exposes two connection strings per project:
The pooled endpoint uses PgBouncer in transaction mode. Each query borrows a connection from the pool for the duration of the transaction, then releases it. This means 100 concurrent Lambda or Edge Function invocations can share a pool of 10 connections. The connection limit is never reached.
The unpooled (direct) endpoint connects straight to the Postgres compute. It supports session-level features that PgBouncer strips: SET LOCAL, prepared statements, advisory locks, and LISTEN/NOTIFY. For migrations, these features matter. For CRUD queries in API routes, they do not.
Claude's default pattern without guidance is to create a pg.Pool at module scope. In a long-running Express server, this is correct and efficient. In a Next.js API route deployed to Vercel, the module is cached between invocations but the pool never fully drains because functions can be killed by the platform at any time. Over time this leads to leaked connections that count against your Neon project limit.
The fix is to use the @neondatabase/serverless driver with the neon() tagged template literal helper. Each invocation creates one connection, runs the query, and exits cleanly. No pool management.
// src/lib/db.ts (edge/serverless)
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import * as schema from './schema';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
// src/lib/db.ts (Node.js server with connection pool)
import { Pool } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-serverless';
import * as schema from './schema';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });
The distinction is in the import path: drizzle-orm/neon-http for stateless edge invocations, drizzle-orm/neon-serverless with a Pool for persistent server processes. Claude does not choose correctly by default.
Database branching for feature workflows
Neon branches are instant copy-on-write clones of your database at the point of the last commit. They create a new compute endpoint with a new connection string. The parent data is shared via storage until the branch diverges. Branching is free and takes under a second.
The practical workflow:
- When you start a feature branch in git, create a matching Neon branch:
neonctl branches create --name feature/my-feature --parent main
- Get the branch connection strings:
neonctl connection-string feature/my-feature --pooled
neonctl connection-string feature/my-feature --pooled --role-name neondb_owner
Set the branch connection string in your preview environment (Vercel branch deploys, Railway PR environments, or local
.env.local).Run schema migrations against the branch:
DATABASE_URL=$(neonctl connection-string feature/my-feature --unpooled) npx drizzle-kit push
- Merge the feature branch in git. Run the same migration against the production branch in CI:
DATABASE_URL=$PROD_DATABASE_URL_UNPOOLED npx drizzle-kit push
This means your preview deployment always has schema parity with your feature branch, and production migrations are never run from a local machine.
The CLAUDE.md should declare this workflow explicitly so Claude Code generates migration commands with the right environment variable, not a hardcoded string.
Prisma with Neon
Prisma requires two separate environment variables for Neon: url for the pooled PgBouncer endpoint (application traffic) and directUrl for the unpooled endpoint (migrations).
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DATABASE_URL_UNPOOLED")
}
Without directUrl, prisma migrate deploy routes migration SQL through PgBouncer's transaction pooler. Multi-statement migration files fail because PgBouncer cannot preserve session state across the statements. The migration appears to succeed but leaves the schema in a partially-applied state.
The runtime query client uses DATABASE_URL (pooled), so application queries remain efficient. Only migrations use the direct endpoint. This is the pattern Prisma's own documentation recommends for Neon, but Claude does not configure it unless the CLAUDE.md declares both variables.
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
The global singleton pattern prevents Prisma from creating a new client on every hot-reload in development. Claude generates a bare new PrismaClient() call without this guard by default, which produces "There are already 10 instances of Prisma Client" warnings within minutes of development.
Scale-to-zero and cold start behaviour
Neon compute scales to zero after a configurable period of inactivity (default: 5 minutes on free tier, configurable on paid plans). The first query after a cold start wakes the compute. This adds latency: typically 100-500ms depending on your region.
For most applications this is acceptable. For latency-sensitive endpoints (health checks, webhook handlers, real-time features), it is not.
Mitigation strategies Claude should know about:
Disable scale-to-zero for production computes. On paid plans you can set the autosuspend delay to 0 (never suspend) on a per-compute basis. Do this for your production compute if cold start latency affects SLAs.
Use connection string warm-up in edge middleware. A lightweight query in Next.js middleware runs before your route handlers, ensuring the compute is warm by the time the real query arrives.
// middleware.ts
import { NextResponse } from 'next/server';
import { neon } from '@neondatabase/serverless';
export async function middleware() {
// Warm the Neon compute before requests reach route handlers
const sql = neon(process.env.DATABASE_URL!);
await sql`SELECT 1`;
return NextResponse.next();
}
export const config = {
matcher: ['/api/:path*'],
};
This doubles the overhead for cold starts but eliminates cold starts from hitting your actual business logic.
This is where Claudify makes a difference: your CLAUDE.md template includes the scale-to-zero warm-up pattern pre-configured so Claude Code generates resilient database access by default, not after you hit the first cold-start timeout in production.
Common Neon mistakes Claude makes without guidance
Wrong driver for the runtime. Using pg.Pool in an edge function generates a node:net dependency error at deploy time. Cloudflare Workers and Vercel Edge Functions do not have access to Node.js TCP sockets. The @neondatabase/serverless driver uses HTTP or WebSocket transport and works in any runtime. The CLAUDE.md must specify which driver to use based on runtime context.
Missing error handling on the SQL tag. The neon() tagged template literal throws on query errors rather than returning an error object. Claude often omits try/catch around SQL tag calls because it treats them like ORM methods that return undefined on no results. A syntax error or constraint violation throws an unhandled promise rejection that crashes the Edge Function.
Ignoring Neon's NEON_CONNECTION_ERRORS category. Neon-specific connection errors (compute not ready, connection limit reached, branch not found) have distinct error codes. Blanket catch blocks that swallow all errors hide these from logging. The CLAUDE.md should specify that Neon errors should be logged with error.code attached.
Hardcoded region in connection string. Neon project URLs contain the region identifier (ep-cool-name-us-east-2). Claude copies this string directly into code. When you later change your project's region or create a new project, all hardcoded references break silently. Environment variables prevent this.
Running migrations on the pooled endpoint. prisma migrate dev against the PgBouncer pooler fails with a "prepared statement already exists" error. This is a PostgreSQL session-level feature that PgBouncer transaction mode does not preserve. The error message is confusing and has no obvious connection to the endpoint type. The CLAUDE.md must route migration commands to the unpooled URL.
FAQ
Can I use Neon with Drizzle on the edge and Prisma in server-side Node.js in the same project?
You can, but it is not recommended. Two ORM clients against the same database create schema management complexity (which tool runs migrations?). Pick one ORM per project and use the appropriate Neon driver adapter for the runtime. Drizzle has first-class Neon adapters for both HTTP (edge) and WebSocket (Node.js) transports. Prisma uses directUrl for migrations and url for queries across all runtimes.
How many database branches can I create?
Neon's free tier supports 10 branches. Paid plans support 500+ branches. For a typical development workflow with one branch per feature/PR, the free tier supports a team of 10 working developers with active PRs. Branches that are not used for 7 days on the free tier are automatically suspended (not deleted).
What is the Neon connection limit and how do I check current usage?
Free tier: 10 concurrent connections. Launch: 100. Scale: 500. Enterprise: custom. Check current usage in the Neon console under your project's monitoring tab. The pg_stat_activity view also shows active connections: SELECT count(*) FROM pg_stat_activity WHERE datname = current_database();.
Does Neon support read replicas?
Neon's architecture has a single primary compute and separate read replicas (available on paid plans). Read replicas share the same storage as the primary and are created with their own connection strings. Use them for analytics queries and reporting workloads to avoid competing with OLTP traffic on the primary compute.
Get Claudify. Your CLAUDE.md ships pre-configured for Neon with the correct driver, pooling rules, and branching workflow so Claude Code writes production-safe database code from the first query.
Ready to upgrade your Claude Code setup?
Get Claudify