Overview Lakebase Safekeepers Pageserver Scale-to-Zero Branching Serverless Driver Pooling Cost Reality Takeaways
Systems · Database · Serverless
🐘
Neon · Lakebase Postgres

Neon Serverless Postgres Architecture: Branching, Scale-to-Zero, and the Lakebase Stack

Neon is not a managed Postgres wrapper. It is a full re-architecture of where Postgres lives, how durability works, and what a "database" even means for serverless and edge workloads. The compute is ephemeral. The storage is independent. And branching your database is now as cheap as branching your Git repo.

📅 August 10, 2026 🕐 14 min read ✍️ Barnabas Waweru 🏷 Systems · Serverless · Postgres
ansi · wordmark · branching
██████╗ ██████╗  █████╗ ███╗   ██╗ ██████╗██╗  ██╗██╗███╗   ██╗ ██████╗ 
██╔══██╗██╔══██╗██╔══██╗████╗  ██║██╔════╝██║  ██║██║████╗  ██║██╔════╝ 
██████╔╝██████╔╝███████║██╔██╗ ██║██║     ███████║██║██╔██╗ ██║██║  ███╗
██╔══██╗██╔══██╗██╔══██║██║╚██╗██║██║     ██╔══██║██║██║╚██╗██║██║   ██║
██████╔╝██║  ██║██║  ██║██║ ╚████║╚██████╗██║  ██║██║██║ ╚████║╚██████╔╝
╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═══╝ ╚═════╝╚═╝  ╚═╝╚═╝╚═╝  ╚═══╝ ╚═════╝ 

What Neon Really Is

Not Managed Postgres — Redesigned Postgres

Every other "managed Postgres" provider takes a standard Postgres installation and runs it for you inside a VM, with an EBS volume attached and a read-replica or two for HA. Scale means a bigger VM. Backup means a periodic snapshot. Branching is science fiction.

Neon takes a different path entirely. They disassembled Postgres's storage assumptions, replaced them with a cloud-native storage layer, and wrapped the compute in an ephemeral execution environment that can spin up or down in under a second. The result: branching is a metadata operation, scale-to-zero is automatic, and cold starts are measured in milliseconds, not seconds.

Neon calls this the lakebase architecture — a category of OLTP database where compute and storage are decoupled, storage is built on object storage for infinite scale, and the database can serve both transactional workloads and analytics without forklift upgrades.

Key Distinction: Neon and Databricks Lakebase

Neon and Databricks run the same underlying database engine — Lakebase Postgres — on the same infrastructure. What differs is the surrounding platform: on Neon it anchors a complete backend for apps and AI agents; on Databricks it integrates with the Data Intelligence Platform for combined OLTP + analytics. The architecture described here applies to both.

Compute/Storage Split

Postgres compute is ephemeral. Storage is durable, independent, and replicated by Paxos quorum — never tied to a single VM.

Copy-on-Write Branching

Branch your database like a Git repo. Branches are instant, isolated, and carry all parent data without copying a single byte at creation time.

Scale to Zero

Idle computes suspend automatically after 5 minutes. Reactivation is sub-second. Pay only for active CU-time.

Serverless Driver

HTTP and WebSocket transports replace TCP, enabling Postgres queries from Vercel Edge Functions, Cloudflare Workers, and other edge runtimes that ban raw sockets.

10,000 Connections

PgBouncer-based connection pooling absorbs serverless connection storms. Each invocation doesn't need its own Postgres backend process.

pgvector Native

Vector embeddings via the pgvector extension. HNSW indexing on up to 2,000 dimensions; switch to halfvec for 3072-dim models.

The Lakebase Architecture: Two Independent Layers

How Postgres Was Split

Traditional Postgres is a monolith: one process owns the WAL, the buffer manager, the heap files, and the connection sockets. Storage lives on the same server that computes queries. This works until you try to scale elastically, run zero-downtime branching, or pay only for what you use.

Neon solves this with a strict split: a compute layer that runs Postgres and is entirely ephemeral, and a storage layer that holds all durability and is entirely independent. They communicate via a stream of WAL records over the network. Compute can restart, scale, or disappear — the storage layer doesn't care.

Object storage (S3-compatible) holds the long-term, immutable page history. But critically, object storage is never on the critical query path. Latency-sensitive reads stay in the compute layer's RAM and NVMe cache. Object storage only handles writes from the pageserver and long-term recovery.

╔══════════════════════════════════════════════════════════════════════════╗ NEON LAKEBASE ARCHITECTURE ╠══════════════════════════════════════════════════════════════════════════╣ CLIENT APPLICATION (Vercel / Cloudflare / Node.js) │ HTTP (neon driver) │ WebSocket (Pool) │ TCP (direct pg) ▼ ▼ ▼ ┌────────────────────────────────────────────────────────────────────┐ COMPUTE LAYER (Ephemeral — starts/stops freely) │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Postgres Process (standard SQL, MVCC, query planner) │ │ │ │ RAM: shared_buffers, session state, hot pages │ │ │ │ NVMe: local page cache (avoids network reads on hits) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ WAL stream (writes) ↑ Page fetch (cache miss) │ └──────────┼───────────────────────────┼────────────────────────────┘ │ │ ┌──────────┼───────────────────────────┼────────────────────────────┐ │ │ STORAGE LAYER (Durable — always on) │ │ │ ▼ │ │ │ ┌───────────────────┐ ┌──────────┴──────────┐ │ │ │ Safekeepers (3x) │ │ Pageserver │ │ │ │ WAL quorum │───▶│ WAL → page recon. │ │ │ │ Paxos consensus │ │ Serves page reads │ │ │ │ Defines commit │ │ Manages branches │ │ │ └───────────────────┘ └──────────┬──────────┘ │ │ │ async upload │ │ ┌──────────▼──────────┐ │ │ │ Object Storage │ │ │ │ S3 / immutable log │ │ │ │ Long-term history │ │ │ │ NOT on query path │ │ │ └─────────────────────┘ │ └────────────────────────────────────────────────────────────────────┘ ╚══════════════════════════════════════════════════════════════════════════╝

Resource Hierarchy

Neon organizes resources into a logical hierarchy that maps onto the physical architecture. An Organization contains one or more Projects. Each Project holds Branches (copy-on-write database clones). Each Branch has an attached Compute Endpoint — a running Postgres instance — plus Databases and Roles. Branching is a property of the storage layer; computes are attached to branches on demand.

Safekeepers: WAL Quorum and the Definition of Commit

Replacing the Filesystem with Consensus

In standard Postgres, a commit is durable when WAL has been flushed to disk. In Neon, the compute node doesn't have persistent disk in the traditional sense. So where does durability come from?

The answer is the safekeeper fleet. When a transaction commits, the compute node streams its WAL records to three safekeepers in parallel. A transaction is considered committed only once a quorum of safekeepers (2 of 3) has acknowledged receipt, using the Paxos consensus protocol. This is mathematically equivalent to the durability guarantee of flushing to a redundant RAID array — except it's network-distributed and survives the loss of an entire AZ.

1
Transaction Executes
Postgres compute executes the transaction in memory. Writes applied to shared buffers. WAL records generated.
2
WAL Streamed to Safekeepers
Compute node streams WAL records to all three safekeepers simultaneously. Each safekeeper writes to its own durable log.
3
Paxos Quorum Ack
Once 2 of 3 safekeepers acknowledge, the WAL LSN is committed. The compute node sends the commit confirmation to the client.
4
Pageserver Ingests WAL
The pageserver reads from the safekeeper quorum and applies WAL records to reconstruct page versions asynchronously.
5
Object Storage Upload
Pageserver periodically offloads immutable page history to S3-compatible object storage. Not on the commit critical path.
Why Paxos Beats a Local Disk Here

Local disk dies with the server. A Paxos quorum across three safekeepers in three zones is durable even if an entire AZ goes dark. The commit latency tradeoff is real — safekeeper replication adds a network round-trip — but Neon optimizes this with message pipelining that batches WAL records before the round-trip, keeping p99 commit latency competitive with single-AZ setups.

The Pageserver: On-Demand Page Reconstruction

The Storage Engine Postgres Never Had

Standard Postgres stores heap files as fixed-size pages (8KB by default) on local disk. The buffer manager fetches pages from disk into shared_buffers, manages dirty pages, and fsyncs them back. This entire model assumes co-location of compute and storage.

Neon replaces the local disk with the pageserver. When a compute node needs a page that isn't in its local RAM or NVMe cache, it sends a page request to the pageserver with the requested page number and LSN. The pageserver reconstructs the correct version of that page by replaying WAL records from the relevant base image forward to that LSN — on demand, in real time.

This is what makes branching possible. A branch is just a pointer to an LSN in the WAL history. When you create a branch, the pageserver doesn't copy any pages. It just records: "branch B diverges from main at LSN X." Subsequent writes to branch B produce their own WAL stream; the pageserver tracks which WAL to apply for which branch independently.

Read Path: Local Cache First, Pageserver on Miss

The compute node's local NVMe acts as a hot page cache. For pages that haven't changed recently (most of a production OLTP database), reads are served at NVMe speed without touching the network. Only cache misses hit the pageserver. This design keeps p99 read latency within a tight band for workloads with reasonable data locality.

  • L1: RAM (shared_buffers) — sub-microsecond, hot working set
  • L2: Local NVMe — microseconds, recently accessed pages
  • L3: Pageserver (network) — single-digit milliseconds, cold pages
  • Never: direct S3 read — object storage never appears on the read critical path

Scale to Zero: How Cold Starts Actually Work

The Compute Goes Away. The Database Doesn't.

After 5 minutes of inactivity, Neon suspends the Postgres compute endpoint entirely. The process terminates. RAM is freed. The VM slot is released. From the cloud provider's billing perspective, nothing is running — which is why you pay zero compute cost during the idle period.

The storage layer doesn't care. Safekeepers hold the latest WAL. The pageserver holds its page cache and branch state. Object storage holds the immutable history. All of it continues humming along, waiting for the next query.

When the next query arrives, Neon's control plane spins up a fresh compute node, attaches it to the storage layer for the right branch, and lets Postgres initialize. The compute knows where the WAL left off (the LSN is stored in the storage layer) and picks up without a full base backup restore. Cold start to first query: a few hundred milliseconds, not minutes.

Cold Start Gotchas in Serverless Frameworks

Prisma users sometimes see P1001 connection timeout errors on the first query after a cold start — the compute is spinning up while the query arrives. Mitigate with:

  • Add connect_timeout=10 to the connection string as a query param
  • Implement a short exponential-backoff retry at the application layer (2-3 attempts, 200ms initial delay)
  • On paid plans, disable scale-to-zero for production endpoints that can't tolerate the latency spike
  • Scale-to-zero is only available for computes ≤ 16 CU; larger computes remain always active
Scenario Compute State First Query Latency Notes
Active (recent queries) Running Normal (RAM warm) No cold start; pages likely cached
Scale-to-zero wake Suspended → Starting ~200–500 ms overhead Compute restarts; NVMe cold
Free plan (forced STZ) Always suspended when idle ~200–500 ms on wake Cannot disable; plan accordingly
Large compute (>16 CU) Always active Normal Not eligible for scale-to-zero
Logical replication active Kept active Normal Active subscribers prevent STZ

Branching: Git for Your Database

Copy-on-Write Without Copying Anything

Database branching is the Neon feature that sounds too good to be true until you understand the architecture. Creating a branch from your production database is an O(1) metadata operation — no data is copied, no dump/restore happens, no seed script runs. The branch is immediately ready with all parent data at the point of creation, and completely isolated for writes.

The mechanism: a branch is a pointer to an LSN in the WAL stream. The pageserver tracks branches as a DAG of LSN ranges. When branch B writes new data, the WAL for those writes is tagged to branch B's WAL stream. When the pageserver serves a read from branch B, it applies: parent's page history up to the branch point LSN, then branch B's own WAL deltas from that point forward.

Parent branches are completely unaffected — zero load, zero performance impact. You can create 10 branches simultaneously on a production database and the production compute won't notice.

Branch Per Pull Request: The Developer Superpower

The canonical pattern: each GitHub PR gets its own Neon branch via the neondatabase/create-branch-action GitHub Action. The branch is spun up when the PR opens, its connection string is injected as a Vercel preview environment variable, and the branch is deleted when the PR closes. Developers get a full copy of the production schema and data — isolated, throwaway, always fresh.

# .github/workflows/preview.yml (simplified)
- uses: neondatabase/create-branch-action@v6
  id: neon
  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
  with:
    env: DATABASE_URL=${{ steps.neon.outputs.db_url_with_pooler }}

# On PR close — delete the branch
- uses: neondatabase/delete-branch-action@v3
  with:
    project_id: ${{ secrets.NEON_PROJECT_ID }}
    api_key: ${{ secrets.NEON_API_KEY }}
    branch: preview/pr-${{ github.event.pull_request.number }}
Schema-Only Branching + Managed Auth Branching

Neon supports schema-only branches for sensitive data environments — the branch inherits the schema but not the row data, useful for teams that can't replicate production PII into every developer environment. Additionally, if you're using Managed Better Auth, the neon_auth schema (users, sessions, auth config) branches with your data automatically — each branch gets isolated authentication state, not shared production auth.

Migrations: Always Run on the Direct (Unpooled) URL

This is the most common Neon misconfiguration. DDL migrations hold session state — they open a transaction, apply schema changes, and commit. PgBouncer in transaction-pooling mode cannot safely route session-state queries; it may route different statements in the same migration to different backend connections.

  • Prisma: set directUrl = env("DIRECT_URL") for prisma migrate; use pooled DATABASE_URL for runtime queries
  • Drizzle: point drizzle.config.ts at the unpooled URL; runtime queries use pooled
  • Claude Code / MCP: use the two-phase pattern — prepare_database_migration rehearses on a throwaway branch; complete_database_migration runs on main

The Serverless Driver: Why TCP Doesn't Work at the Edge

The Edge Runtime Problem

Standard Postgres uses TCP, with a persistent connection handshake that includes auth, parameter negotiation, and session setup. This works fine in a long-lived Node.js process. It breaks completely in edge runtimes (Vercel Edge Functions, Cloudflare Workers) that prohibit raw TCP sockets, have no persistent connections, and may run in dozens of data centers simultaneously.

The @neondatabase/serverless driver solves this by replacing TCP with HTTP or WebSockets — both universally available in edge environments. The driver is a GA package (v1.0+), requires Node.js 19+, and ships TypeScript types bundled (no @types/pg needed).

Transport API Transactions Session State Best For
HTTP neon() template literal No (single statements) No Edge reads/writes, route handlers, server actions
WebSocket Pool / Client Yes Yes Multi-statement transactions, node-postgres compat
TCP (standard pg) pg Pool/Client Yes Yes Traditional Node.js, non-edge runtimes only
// HTTP transport — edge-safe, lowest cold-start overhead
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL!)

// Template literal — ${}values are auto-parameterized, safe from SQLi
const rows = await sql`
  SELECT id, email FROM users WHERE id = ${userId}
`

// WebSocket transport — for transactions (create Pool inside handler)
import { Pool } from '@neondatabase/serverless'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const client = await pool.connect()
try {
  await client.query('BEGIN')
  // ... transaction statements ...
  await client.query('COMMIT')
} finally {
  client.release()
  ctx.waitUntil(pool.end()) // drain after response, don't block
}
HTTP Transport Performance: Message Pipelining

The HTTP transport achieves lower cold-start overhead than WebSockets because there's no WebSocket handshake. Neon also applies message pipelining — batching multiple Postgres protocol messages into a single HTTP request — which cuts the number of round-trips significantly. For typical single-query route handlers, the neon() function is the right default. Only drop down to Pool when you genuinely need multi-statement transactions.

Connection Pooling: PgBouncer Under the Hood

Why Serverless Destroys Postgres Connection Limits

Every Postgres connection forks a new OS process. Memory per connection scales linearly. On a 1 CU (1 vCPU, 4GB RAM) Neon compute, max_connections is capped at 419. A medium-sized Next.js deployment on Vercel can easily create hundreds of concurrent Lambda invocations, each trying to open its own Postgres connection — slamming into that limit immediately.

Neon's solution: a managed PgBouncer instance running in transaction-pooling mode sits between client connections and the actual Postgres backend. PgBouncer accepts up to 10,000 client connections and routes them through a much smaller pool of real Postgres connections. Client connections are held only for the duration of a transaction, then returned to the pool.

CLIENT CONNECTIONS (up to 10,000) ┌──────────────────────────────────────────────────────────────────┐ Lambda#1 Lambda#2 Lambda#3 ... Lambda#5000 │ │ │ │ └─────┼───────────┼───────────┼────────────────┼──────────────────┘ │ │ │ │ └───────────┴─────┬─────┴────────────────┘ │ ▼ POOLED ENDPOINT (-pooler hostname) ┌─────────────────────────────────────────────────────────┐ PgBouncer (transaction-pooling mode) Per-user, per-database pools Pool size = 90% of max_connections SET, TEMP TABLES, LISTEN/NOTIFY → not supported └────────────────────────┬────────────────────────────────┘ │ ~419 real connections (1 CU) ▼ POSTGRES BACKEND ┌─────────────────────────────────────────────────────────┐ Neon Compute (Postgres process) max_connections determined by RAM/CU size 7 connections reserved for Neon superuser └─────────────────────────────────────────────────────────┘

Pooled vs Direct URL: The Most Common Neon Mistake

Neon provides two connection string variants. The pooled string adds -pooler to the endpoint hostname:

  • Pooled (ep-cool-name-pooler.us-east-2.aws.neon.tech): Use for all runtime queries in serverless/edge. Handles connection storms. PgBouncer in transaction mode.
  • Direct / Unpooled (ep-cool-name.us-east-2.aws.neon.tech): Use only for migrations (prisma migrate, drizzle-kit), pg_dump, logical replication, and anything requiring session state (SET, LISTEN, PREPARE).

Transaction-mode pooling specifically breaks: SET (session params), temporary tables, SQL-level PREPARE/DEALLOCATE, and LISTEN/NOTIFY. Protocol-level prepared statements (e.g. libpq extended query protocol) are supported.

Cost Reality Check

💜 Cost Reality Check

Neon bills on two dimensions: compute units (CU) and storage (GiB-month). Understanding both prevents bill surprises.

Compute: 1 CU = 1 vCPU + 4 GB RAM. You pay per CU-hour of active compute time only. Scale-to-zero means idle databases cost $0 in compute. The Free plan includes 191.9 compute hours/month (equivalent to a 0.25 CU compute running ~32 hours). Paid plans unlock autoscaling, bigger computes, and disabling scale-to-zero.

Storage: Billed on logical data size plus WAL history retained for branching and PITR. The Free plan includes 0.5 GiB. Paid plans start with 10 GiB included. Branches are cheap — they only store deltas — but long-running branches on active databases accumulate WAL history.

  • Free Plan: 191.9 compute hours, 0.5 GiB storage, 10 branches, 1 project — for solo dev and low-traffic apps
  • Scale Plan ($69/mo base): 750 compute hours, 50 GiB storage, unlimited projects, autoscaling up to 16 CU
  • Business Plan ($700/mo base): 1000 hours, 500 GiB, up to 56 CU, SLA, IP Allow
  • Branch limit: Free tier caps at 10 branches per project — delete PR branches on close or you'll hit this
  • pgvector cost: halfvec (for 3072-dim models like OpenAI text-embedding-3-large) stores at half precision, roughly halving storage vs vector
  • Cold start trade-off: Disabling scale-to-zero on a small CU size costs ~$2–5/month per compute endpoint — usually worth it for production

Key Takeaways

✅ Key Takeaways

  1. The lakebase split is the foundation of every Neon capability. Compute is ephemeral; storage is durable. Without this separation, branching, scale-to-zero, and instant clones would be impossible or prohibitively expensive.
  2. Commit durability is defined by safekeeper quorum, not local disk. Three safekeepers, Paxos consensus, 2-of-3 acknowledgement before commit confirmation. This is why Neon can terminate a compute node mid-flight without data loss.
  3. The pageserver reconstructs pages on demand — branching is metadata-only. A branch is a pointer to an LSN. No data is copied at branch creation time. Writes to a branch produce deltas; the parent branch is unaffected in performance and data.
  4. Use HTTP (neon()) for most edge queries; WebSocket (Pool) only for transactions. The neon() function is edge-safe, has the lowest cold-start overhead, and auto-parameterizes template literal values against SQL injection.
  5. Two connection strings, two purposes — never mix them. Pooled for runtime queries, direct for migrations and session-state operations. Mixing them causes silent data errors or migration failures.
  6. Scale-to-zero is a cost lever, not a reliability feature. It eliminates idle compute cost but adds ~200–500ms latency on the first wake query. For production endpoints, evaluate whether the cost saving (typically $2–5/month) is worth the latency tradeoff before disabling it.
  7. Delete PR branches when the PR closes. Free tier caps at 10 branches per project. Even on paid plans, branches accumulate WAL history over time if they stay open — automate cleanup with neondatabase/delete-branch-action.