# Neon

> Use when you have picked Neon (serverless Postgres) and need to connect correctly from a serverless or edge runtime, wire database branching into dev, preview and CI, or stop being burned by scale-to-zero cold starts and pooler limits — including choosing HTTP versus WebSocket connections and pooled versus direct endpoints for migrations. NOT generic Postgres schema, index, EXPLAIN, RLS or migration-engine work (that is `postgresdb`).

- Skill: `ericrisco/neon` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add ericrisco/neon`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ericrisco/neon/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: ericrisco (https://skillmd.com/u/ericrisco)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ericrisco/neon

---


# Neon — serverless Postgres as a platform

Neon is Postgres with three platform features the engine doesn't have: a **serverless driver** that
talks over HTTP/WebSocket (so you can query from edge runtimes), **built-in connection pooling**, and
**copy-on-write branching** that makes a fork of production data appear instantly. This skill owns
that platform layer. The Postgres *engine* underneath — schema, indexes, EXPLAIN, RLS, zero-downtime
DDL — is identical to any Postgres 16 and belongs to `../postgresdb/SKILL.md`. Don't re-derive engine
craft here; connect correctly and branch correctly.

## When to use / When NOT to use

**When to use:**

- Code imports `@neondatabase/serverless`, or `DATABASE_URL` points at `*.neon.tech` / `*.aws.neon.tech`.
- Connecting from Vercel (Edge/Node), Cloudflare Workers, AWS Lambda, or any serverless/edge function — choosing HTTP `neon()` vs WebSocket `Pool`.
- Setting up branching: dev branch off production, a branch per PR in CI, `neonctl` usage, the Neon + Vercel preview integration.
- Neon-specific symptoms: cold start after scale-to-zero, "too many connections" despite being serverless, pooled-vs-direct string confusion, autoscaling CU sizing/cost.
- Picking the connection string for migrations (direct) vs app queries (pooled).

**When NOT to use — route to the sibling:**

- Schema design, index choice, EXPLAIN/ANALYZE, query tuning, RLS, VACUUM, partitioning, expand-contract migrations → `../postgresdb/SKILL.md`. Neon adds nothing to the engine; defer.
- Drizzle schema/migrations → `../drizzle-orm/SKILL.md`; Prisma → `prisma-orm`. Neon supplies only the *driver adapter* line.
- A different managed Postgres/BaaS: Supabase (auth + storage + realtime) → `supabase`; PlanetScale → `planetscale`; Turso/libSQL → `sqlite-turso`.
- Where to deploy the app talking to Neon → `../vercel/SKILL.md` / `cloudflare` / `railway`.
- Backup/PITR strategy as a discipline → `backups` (Neon branch-as-restore is mentioned here, not owned).

## Non-negotiables

1. A WebSocket `Pool`/`Client` is **created and closed inside one request handler** — never at module scope in a serverless function. Module scope = a connection that outlives the invocation and leaks.
2. **App queries use the pooled string** (host contains `-pooler`). **Migrations / DDL / advisory locks / session features use the direct string** (no `-pooler`). Mixing them is the #1 Neon foot-gun.
3. The HTTP `neon()` driver **always** uses the pooled endpoint — don't hand it a direct string and expect session state.
4. HTTP `neon()` has **no interactive transactions**. Need `BEGIN ... COMMIT` across round-trips? Use `sql.transaction([...])` (batched, non-interactive) or a WebSocket `Pool`.
5. On Node.js ≤ 21 you **must** set `neonConfig.webSocketConstructor = ws` before opening a Pool/Client. Node 22+ and edge runtimes have a global `WebSocket`; don't set it there.
6. Pin the driver: `@neondatabase/serverless` is at **v1.1.0** (npm latest, 2026-04-17). It is a drop-in replacement for `pg`.

## Pick your connection method

Keyed by runtime and what the query needs. Pick the lightest transport that satisfies the need.

| You need | Use | Why |
| --- | --- | --- |
| One independent read/write per request (edge, Lambda, RSC) | HTTP `neon()` | Single fetch round-trip (~3 round trips vs ~8 for TCP setup); no connection to manage or leak. |
| Several queries that must be atomic, no logic between them | `sql.transaction([q1, q2])` over HTTP | One round-trip, real transaction, still no socket lifecycle. |
| Interactive transaction — read a row, branch in app code, then write | WebSocket `Pool` (open+close in handler) | HTTP can't hold `BEGIN` open; WebSocket keeps the session alive within the request. |
| Full `pg`-API compatibility (cursors, LISTEN/NOTIFY, COPY) | WebSocket `Client`/`Pool` | HTTP is one-shot; node-postgres semantics need the socket. |
| Migrations / DDL / `pg_advisory_lock` / session GUCs | direct (non-`-pooler`) string with `pg` or WebSocket `Client` | PgBouncer transaction-pooling breaks session-level state. |

If you're behind Drizzle or Prisma, you don't call these directly — you pass the driver to the
adapter. See the ORM pointers below.

## Connection strings: pooled vs direct

Neon gives every branch two connection strings that differ in **one** thing: the host of the pooled
one contains `-pooler`.

```text
pooled:  postgresql://user:pass@ep-cool-name-123456-pooler.us-east-2.aws.neon.tech/db?sslmode=require
direct:  postgresql://user:pass@ep-cool-name-123456.us-east-2.aws.neon.tech/db?sslmode=require
```

| Use case | String | Reason |
| --- | --- | --- |
| App runtime (queries from your functions) | **pooled** (`-pooler`) | PgBouncer fans many short-lived serverless invocations onto a small pool; up to 10,000 concurrent client connections per project. |
| HTTP `neon()` driver | **pooled** (it forces it regardless) | The HTTP path is stateless; pooling is the right model. |
| Migrations, DDL, schema introspection | **direct** | Transaction pooling drops session state; advisory locks and prepared statements need a stable session. |
| `psql` interactive / debugging | **direct** | You want one real session, not a pooled handle. |

Bad → Good for a typical setup:

```bash
# Bad: one string everywhere — migrations sporadically fail under PgBouncer.
DATABASE_URL="postgresql://...-pooler.../db?sslmode=require"

# Good: split them. App uses the pooled one; the migration tool uses DIRECT.
DATABASE_URL="postgresql://...-pooler.../db?sslmode=require"          # app runtime
DIRECT_URL="postgresql://....../db?sslmode=require"                    # migrations/DDL
```

## Serverless driver patterns

All examples assume `@neondatabase/serverless@1.1.0`.

HTTP one-shot — the default for edge/serverless reads and writes:

```ts
import { neon } from "@neondatabase/serverless";

const sql = neon(process.env.DATABASE_URL!); // pooled endpoint, no socket to manage
export async function getUser(id: string) {
  const [row] = await sql`select id, email from users where id = ${id}`;
  return row; // parameterized; ${id} is bound, not interpolated
}
```

Batched atomic writes over HTTP — atomicity without a WebSocket and without interactivity:

```ts
import { neon } from "@neondatabase/serverless";

const sql = neon(process.env.DATABASE_URL!);
await sql.transaction([
  sql`insert into orders (id, total) values (${id}, ${total})`,
  sql`update inventory set qty = qty - 1 where sku = ${sku}`,
]); // one round-trip, both succeed or both roll back
```

Interactive transaction — WebSocket `Pool`, opened **and closed inside the handler**:

```ts
import { Pool, neonConfig } from "@neondatabase/serverless";
import ws from "ws"; // Node ≤21 only; omit on edge / Node 22+

neonConfig.webSocketConstructor = ws; // required on Node ≤21; do NOT set on edge runtimes

export async function transfer(from: string, to: string, cents: number) {
  const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // inside the handler
  const client = await pool.connect();
  try {
    await client.query("begin");
    const { rows } = await client.query("select balance from accounts where id=$1 for update", [from]);
    if (rows[0].balance < cents) throw new Error("insufficient");
    await client.query("update accounts set balance=balance-$1 where id=$2", [cents, from]);
    await client.query("update accounts set balance=balance+$1 where id=$2", [cents, to]);
    await client.query("commit");
  } catch (e) {
    await client.query("rollback");
    throw e;
  } finally {
    client.release();
    await pool.end(); // close before the function returns — no leaked connection
  }
}
```

Edge runtime (Vercel Edge, Cloudflare Workers): use HTTP `neon()`; the global `WebSocket` exists, so a
`Pool` works without `ws`, but prefer HTTP unless you truly need an interactive transaction.

ORM adapters — you wire the driver, the ORM owns the query API:

- Drizzle: pass `neon()`/`Pool` into `drizzle-orm/neon-http` or `neon-serverless` → `../drizzle-orm/SKILL.md`.
- Prisma: use the `@prisma/adapter-neon` driver adapter → `prisma-orm`.

## Branching for dev / preview / CI

A Neon branch is **copy-on-write**: created instantly with no data copied, it forks production at a
point in time, gets its own compute endpoint, and scales to zero independently. That makes a branch a
cheap, isolated, full-data environment — not a backup substitute (test the restore).

| Situation | Do | Why |
| --- | --- | --- |
| Per-developer or per-PR isolated data | **branch off production** | Instant, real prod-shaped data, separate endpoint, auto scale-to-zero → near-free idle. |
| A genuinely separate product/tenant with its own billing & limits | **new project** | Branches share the project's quotas and pooled-connection ceiling. |
| Throwaway query you can run against staging | **same branch** | Don't manufacture a branch for a one-off SELECT. |

`neonctl` (alias `neon`) drives create/connect/teardown:

```bash
neonctl branches create --name pr-$PR_NUMBER --parent main
neonctl connection-string pr-$PR_NUMBER --pooled   # feed to the preview app
neonctl branches delete pr-$PR_NUMBER              # on PR merge/close
```

**Vercel preview integration:** with the Neon + Vercel integration, each preview deployment gets its
own branch forked from production, auto-deleted when the PR merges or closes — zero CI code. If you're
not on Vercel, replicate it in CI: create branch on PR open → run migrations (direct string) → seed →
expose the pooled connection string to the app → delete on PR close. Full GitHub Actions workflow,
`neonctl` reference, and the Neon API endpoints: [branching-ci](references/branching-ci.md).

**Branch-as-restore (PITR):** to recover, branch from an earlier point in time and either read from it
or promote it. It's a fast undo, not a tested backup discipline — own that in `backups`.

## Autoscaling & scale-to-zero

Each branch's compute endpoint **autoscales** between a min and max CU (vCPU/RAM) on its own, with no
noisy neighbors across branches. Idle compute **scales to zero** after **5 minutes** of inactivity
(default), then resumes on the next connection with a millisecond-to-sub-second cold start.

Cost mental model: non-prod branches sit at zero almost always, so a dozen PR branches cost close to
nothing. The cold start is the price.

Prod endpoint sizing checklist:

- [ ] Set a sane **min/max CU** for the prod branch — not maxed "just in case" (you pay for the max headroom you actually use).
- [ ] Latency-sensitive prod with sparse traffic? **Disable scale-to-zero** or set **min CU > 0** so there's no cold start in p99.
- [ ] Treat the cold start as real: warm the connection on deploy, or accept it on non-prod and account for it in latency SLOs.
- [ ] Keep non-prod branches on scale-to-zero — that's where the near-free economics come from.

## Anti-patterns

| Anti-pattern | Why it bites | Do instead |
| --- | --- | --- |
| `new Pool(...)` at module scope in a serverless function | Connection outlives the invocation; you exhaust the pool → "too many connections". | Create and `await pool.end()` inside the handler, or use HTTP `neon()`. |
| Pooled (`-pooler`) string for migrations/DDL | PgBouncer transaction-pooling drops session state; advisory locks/prepared statements fail intermittently. | Use the **direct** string for migrations; pooled only for app runtime. |
| Direct (non-`-pooler`) string as the app `DATABASE_URL` | No pooling → serverless concurrency exhausts raw connections fast. | App runtime always uses the **pooled** string. |
| Expecting `BEGIN`/interactive txns from HTTP `neon()` | HTTP is one-shot, stateless — there's no open session to hold a transaction. | `sql.transaction([...])` for batched atomicity, or a WebSocket `Pool` for interactive. |
| Opening a `Pool` without `neonConfig.webSocketConstructor = ws` on Node ≤21 | No global `WebSocket` → the connection silently fails. | Set `webSocketConstructor = ws` on Node ≤21; omit on edge / Node 22+. |
| Ignoring scale-to-zero in p99 latency | First request after 5 min idle pays the resume cold start; users see a slow request. | Disable scale-to-zero or set min CU > 0 on latency-sensitive prod. |
| One shared branch for all PRs | PRs clobber each other's data; no isolation. | One branch per PR, deleted on close. |
| Treating a branch as a backup without testing restore | Branches share project quotas and aren't an exercised recovery path. | Verify branch-as-restore; own real backup strategy in `backups`. |
| Maxing CU "to be safe" | You pay for headroom you don't use; autoscaling already handles spikes. | Set a realistic min/max; let autoscaling expand. |
| Doing schema/index design in this skill | Engine craft is identical to any Postgres; duplicating it drifts. | Defer to `../postgresdb/SKILL.md`. |

## Cross-references

- Engine craft (schema, indexes, EXPLAIN, RLS, zero-downtime DDL): `../postgresdb/SKILL.md`.
- Next.js App Router data layer that calls Neon: `../nextjs/SKILL.md`.
- ORM query APIs over the Neon driver: `../drizzle-orm/SKILL.md`, `prisma-orm`.
- Deployment targets: `../vercel/SKILL.md`, `cloudflare`, `railway`. Backups discipline: `backups`.
- Other managed DBs (route away): `supabase`, `planetscale`, `sqlite-turso`.

