โโโโ โโโโโโโโโโโ โโโโโโโ โโโโ โโโ โโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโ โโโ โโโโโโ โโโโโโโโโ โโโ โโโโโโโโโ โโโ โโโโโโโโโโโโโโโโ โโโ โโโโโโโโโโโโโ โโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโ โโโ โโโโโโโโโโโโโ โโโโโโโ โโโ โโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโ โโโโโโโ โโโโโโ โโโโโโโ โโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโ โโโโโโโโ โโโ โโโ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโ โโโโโโโโ โโโ โโโ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโ โโโโโโโโ โโโ โโโโโโโโโโโโ โโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโ โโโ โโโโโโโ โโโ โโโโโโ โโโ โโโโโโโ โโโโโโโโ
Traditional Postgres thinking: you rent a VM, Postgres owns the disk, connections arrive on port 5432, the WAL is written locally. That model is twenty years old, and it's the reason your dev database costs $40/month to sit idle, your CI pipeline has to hydrate a fresh database from scratch on every run, and you lose data the moment your disk fills up.
Neon's answer is architectural, not cosmetic. It takes the Postgres query engine and removes it from the storage layer entirely. Compute and storage become independently scalable, separately billed, separately operated resources. The query engine runs in ephemeral VMs, computes, and the data lives in a distributed, object-storage-backed system called Lakebase (Neon's internal storage engine, based on the open-source Neon Storage project).
This separation is why Neon can offer things that were architecturally impossible on traditional Postgres hosts: copy-on-write database branches, scale-to-zero with millisecond wakeup, read replicas that cost no extra storage, and point-in-time restore without a full backup.
On RDS or DigitalOcean, deleting your dev instance frees your CPU, but the EBS volume is still there, still billed. On Neon, scale-to-zero suspends the compute while your data persists in shared storage. You pay for active CU-seconds, not idle machines. For dev databases that run 2 hours a day, that is a 10ร cost reduction by default.
When you send a query to a Neon endpoint, a Compute, a lightweight Postgres process, receives it. The compute runs your SQL just like standard Postgres: it parses, plans, and executes. But here's where the architecture diverges: instead of reading pages from a local disk, it fetches them from the Neon Storage Service (Lakebase) over the network.
Lakebase is a distributed, WAL-based object store. Every write your Postgres compute makes becomes a WAL record shipped to Lakebase's Safekeeper nodes, a quorum-replicated write-ahead log layer. Once durably written, the WAL is processed by Pageserver nodes, which materialise page versions and serve them to computes on demand.
The critical insight: the Pageserver maintains a full history of every page version since your project was created (bounded by your history window). This is what enables both branching and point-in-time restore, they are not separate backup mechanisms. They are natural consequences of a history-preserving storage layer.
CLIENT (Edge Function / Vercel / Cloudflare Worker)
โ
โ HTTP (neon() serverless driver, one-shot queries)
โ WS (Pool/Client, transactions, session state)
โ
โโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ NEON PROXY (SNI routing โ project endpoint) โ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Connection Poolโ โ PgBouncer (transaction โ โ
โ โ (10k max conns)โ โ mode, -pooler hostname) โ โ
โ โโโโโโโโโฌโโโโโโโโโ โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโ
โ โ
โโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโ
โ COMPUTE (ephemeral Postgres VM, scales to zero) โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Standard Postgres Query Engine โ โ
โ โ Parser โ Planner โ Executor โ Buffer Cache โ โ
โ โ โ page miss โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ page request (NRL protocol)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ LAKEBASE STORAGE (Neon Storage, distributed, durable)โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ SAFEKEEPER nodes โ โ PAGESERVER nodes โ โ
โ โ (WAL quorum, โ โ (materialise page vers., โ โ
โ โ 3-way repl.) โโโโบโ serve on demand) โ โ
โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโ โ
โ โ S3-compatible object store โ โ
โ โ (immutable page layers) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
When your Postgres compute commits a transaction, it ships the WAL record to a set of Safekeeper nodes, typically three, geographically distributed within the region. A write is acknowledged only when a quorum has confirmed receipt. This gives Neon the same durability guarantee as synchronous streaming replication without requiring a standby Postgres process.
Once durably written, the WAL flows to the Pageserver. The Pageserver reads page deltas, applies them to the base page versions, and caches the results in a tiered store: hot pages in memory, warm pages in fast SSD, cold pages in S3-compatible object storage. This tiered materialization is what keeps cold-start latency low, the Pageserver can reconstruct the latest page version without replaying the entire WAL.
The Pageserver does not overwrite old page versions. Instead, it stores deltas, the difference between consecutive page states at specific LSN (Log Sequence Number) positions. To serve a page at LSN N, the Pageserver finds the nearest base image and applies deltas forward to reach N.
This immutable-append design is what makes point-in-time restore a first-class primitive rather than an afterthought. And it is exactly what makes branching possible: to branch at LSN N, you simply tell the Pageserver to serve page requests for the new branch using the existing page history up to N. No data is copied. The branch's writes go into a new delta chain. The parent never sees them.
A Neon branch is a named pointer to an LSN in the storage layer. When you create a branch, Neon records the parent branch, the fork point LSN, and allocates a new endpoint, a compute, to serve queries against the branch. The endpoint gets its own connection string, its own scale-to-zero lifecycle, and its own compute size allocation.
The branch shares all page history below the fork LSN with its parent. Writes to the branch become new deltas in the branch's own timeline. The parent branch never sees branch writes, and the branch never sees post-fork writes to the parent unless you explicitly merge or rebase (via the Neon CLI's restore command).
Branch creation is instantaneous regardless of database size. Whether your database is 100 MB or 100 GB, the branch is available within a few seconds, almost all of that is compute spin-up, not data copying.
A branch is a storage concept. A compute is a runtime concept. You can have a branch with no running compute (it will start on demand). You can have multiple computes on the same branch. Read replicas are exactly this: additional read-only computes on the same branch. Do not conflate "branch" with "database instance", Neon separates them deliberately.
neondatabase/create-branch-action@v6 with the PR number as branch name.main at the current LSN. Action outputs db_url_with_pooler.DATABASE_URL pointing to the PR branch. Preview runs against isolated data.neondatabase/delete-branch-action@v3 removes the branch. Storage freed; compute was already idle.# .github/workflows/preview.yml
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: neondatabase/create-branch-action@v6
id: neon
if: github.event.action != 'closed'
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
api_key: ${{ secrets.NEON_API_KEY }}
branch_name: preview/pr-${{ github.event.pull_request.number }}
- uses: vercel/action@v1
if: github.event.action != 'closed'
with:
env: DATABASE_URL=${{ steps.neon.outputs.db_url_with_pooler }}
- uses: neondatabase/delete-branch-action@v3
if: github.event.action == 'closed'
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
api_key: ${{ secrets.NEON_API_KEY }}
branch: preview/pr-${{ github.event.pull_request.number }}
When working with sensitive production data, Neon supports schema-only branching, create a branch that carries the schema but not the row data. Useful for testing migrations on a realistic structure without exposing PII to developer machines.
Branches can also carry a TTL (expiration date). Set an expiration at branch creation and Neon will automatically delete the branch after that date, no cleanup cron required. Ideal for ephemeral feature branches and CI environments with known lifespans.
After 5 minutes of inactivity (configurable on paid plans, fixed on Free), the Neon control plane suspends the compute. "Suspend" means the Postgres process exits, the VM is deallocated, and billing stops. The data, safely in Lakebase, persists indefinitely.
When the next query arrives via the Neon Proxy, the proxy detects the suspended state, signals the control plane to start a compute, waits for readiness, and then routes the connection. The client sees a connection delay of a few hundred milliseconds on the first query, not seconds, not a timeout. The Pageserver's page cache is already warm from before the suspend, so the reactivated compute can serve data immediately.
Note: Computes larger than 16 CU are exempt from scale-to-zero and run always-active. Very large computes need stable performance characteristics that aren't compatible with the suspend/resume cycle.
ACTIVE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบ IDLE (5 min no queries)
โ โ
โ queries arrive โ control plane signals suspend
โ billed per CU-second โ
โ โผ
โ SUSPENDED
โ (compute deallocated,
โ data in Lakebase,
โ billing stopped)
โ โ
โ next query arrives โ proxy detects suspend
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโบ control plane provisions compute VM
โโโบ Postgres starts, connects to Pageserver
โโโบ Proxy routes connection (few hundred ms delay)
โโโบ ACTIVE, queries served normally
For production traffic, cold-start latency is real. Handle it at the application layer, not with hope. Two patterns:
?connect_timeout=10 to give the Postgres client enough time to wait for the compute to warm.Alternatively, on paid plans, disable scale-to-zero for production endpoints and enable it only for dev/test branches.
Scale-to-zero handles the lower bound (idle โ zero). Autoscaling handles the upper bound. You set a min and max CU range per compute. Neon monitors CPU and memory pressure and scales within the range without restart, this is live compute resize, not instance replacement.
This combination means your database automatically tracks your workload: zero CUs at night, 0.25 CU for light morning traffic, scaling to 4 CU during a traffic spike, then back down. You pay only for what the workload actually consumed, measured in CU-seconds.
Standard Postgres creates a new OS process for each connection. On a 0.25 CU compute (1 GB RAM), max_connections is 104. Subtract 7 reserved for Neon's superuser: you have 97. That's fine for a monolith, disastrous for serverless functions where every request may spawn a new connection.
Neon bundles PgBouncer at every endpoint. Enable it by switching to the pooled connection string, identical to the standard one except the hostname includes -pooler:
# Standard (unpooled, for migrations, long-lived connections) postgresql://user:[email protected]/neondb?sslmode=require # Pooled (PgBouncer, for serverless, Vercel Functions, Edge) postgresql://user:[email protected]/neondb?sslmode=require
PgBouncer runs in transaction mode: it holds a real Postgres connection only for the duration of a transaction, then returns it to the pool. Up to 10,000 client connections can share the underlying pool.
| Scenario | Use | Why |
|---|---|---|
| Vercel / Netlify Functions | Pooled | Connection-per-request; pooler prevents max_connections exhaustion |
| Cloudflare Workers / Edge | HTTP (neon() driver) | No TCP; HTTP fetch is the only viable transport at the edge |
| Prisma / Drizzle migrations | Unpooled | DDL holds session state; transaction-mode pooler drops SET/PREPARE |
| pg_dump / logical replication | Unpooled | Requires persistent session state (LISTEN/NOTIFY, replication slots) |
| Multi-statement transactions | Pooled or Unpooled | Pooled works in transaction mode; avoid SET/PREPARE within transaction |
| Long-lived Node.js server | Unpooled + own pool | Manage your own pg.Pool for session affinity and full Postgres features |
@neondatabase/serverless is a drop-in replacement for the pg npm package that adds two new transports: HTTP and WebSocket. The standard pg package requires TCP, which is unavailable in most edge runtimes. The serverless driver solves this without changing your query syntax.
// lib/db.ts, HTTP transport (default: use for everything without transactions)
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL!)
export { sql }
// Route Handler: one-shot query, safe, parameterized
const rows = await sql`SELECT id, email FROM users WHERE id = ${userId}`
// Multiple non-interactive queries in one round-trip (transaction: false)
const [users, posts] = await sql.transaction([
sql`SELECT * FROM users LIMIT 10`,
sql`SELECT * FROM posts LIMIT 10`
], { isolationLevel: 'ReadCommitted' })
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// lib/db-pool.ts, WebSocket transport (transactions only)
import { Pool } from '@neondatabase/serverless'
export async function POST(req: Request, ctx: { waitUntil: (p: Promise) => void }) {
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const client = await pool.connect()
try {
await client.query('BEGIN')
await client.query('UPDATE accounts SET bal = bal - $1 WHERE id = $2', [amount, from])
await client.query('UPDATE accounts SET bal = bal + $1 WHERE id = $2', [amount, to])
await client.query('COMMIT')
return Response.json({ ok: true })
} catch (err) {
await client.query('ROLLBACK')
throw err
} finally {
client.release()
ctx.waitUntil(pool.end()) // close after response, non-blocking
}
}
Traditional read replicas involve streaming replication: WAL records flow from primary to standby, which applies them to a full copy of the data. You pay for double the storage, and replica lag is a function of network latency and write volume.
Neon read replicas work differently. Since all computes, primary and replicas, read from the same Lakebase Pageserver, there is no data to replicate. A read replica is just another compute pointed at the same storage branch. You pay only for the additional compute-hours the replica consumes.
Replica lag is architectural: the primary writes WAL to Safekeepers, which flow to the Pageserver. Replicas read from the Pageserver. The lag is the Pageserver's materialization latency, typically milliseconds, not seconds. And because replicas share the Pageserver's page cache, they benefit from pages already warmed by the primary.
WRITE TRAFFIC READ TRAFFIC
โ โ
โโโโโโผโโโโโโโโโโโโโโโ โโโโโโโโผโโโโโโโโโโโโโโโ
โ PRIMARY COMPUTE โ โ READ REPLICA #1 โ
โ (read-write) โ โ (read-only compute) โ
โโโโโโโโโโฌโโโโโโโโโโโ โโโโโโโโโโโโฌโโโโโโโโโโโโ
โ WAL โ page requests
โผ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโ
โ LAKEBASE PAGESERVER โ
โ (single source of truth for all pages) โ
โ โ
โ Pages at LSN N are identical whether read by the โ
โ primary or any replica. No physical replication needed. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Neon's Free tier is genuinely useful for development, but understand its limits before you hit them in production.
| Feature | Free | Launch ($19/mo) | Scale ($69/mo) |
|---|---|---|---|
| Compute included | 0.25 CU (fixed) | 300 CU-hr/mo | 750 CU-hr/mo |
| Storage included | 0.5 GiB | 10 GiB | 50 GiB |
| Branches (max) | 10 | Unlimited | Unlimited |
| Scale-to-zero | Forced (cannot disable) | Optional | Optional |
| History window | 6 hours | 7 days | 30 days |
| Max compute size | 0.25 CU | 8 CU | 56 CU |
Compute: You pay for active CU-seconds. A 1 CU compute running 8 hours/day at $0.16/CU-hr costs ~$39/month, less than most managed Postgres services that run idle 24/7. With scale-to-zero, a dev database that runs 2h/day costs under $5/month.
Storage: Billed per GiB-month of data stored. This includes your current data and the WAL history within your window. Long history windows increase storage cost, a database with heavy writes and a 30-day history window stores a lot of deltas. Monitor storage on the Neon console.
Branches: Branches themselves are free, they share storage with the parent. You pay for compute on branch endpoints if they're running. Delete PR branches when PRs close; an open PR branch with an idle endpoint is just storage, no compute cost.
Data transfer: Egress from Neon to your application is billed. Edge-native deployments (Vercel Edge, Cloudflare Workers) that co-locate with Neon's AWS regions benefit from lower latency and lower egress costs.
neon()) for read-heavy paths, avoids WS connection overheaddirectUrl or Drizzle's migration config to the unpooled URL.neon() for reads, Pool for transactions. The HTTP transport has the lowest overhead and is edge-compatible. Use it by default in Next.js Route Handlers and Server Actions. Only reach for the WebSocket Pool when you genuinely need multi-statement transactions, and remember to close the Pool with ctx.waitUntil(pool.end()).?connect_timeout=10 to your connection string and implement a single retry with short backoff for Prisma P1001 / PG ETIMEDOUT errors. On paid plans, disable scale-to-zero on your production compute if sub-100ms p99 is a requirement.text-embedding-3-large (3072 dimensions), use the halfvec type, same semantics, half the storage, within the HNSW ceiling.