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.
██████╗ ██████╗ █████╗ ███╗ ██╗ ██████╗██╗ ██╗██╗███╗ ██╗ ██████╗ ██╔══██╗██╔══██╗██╔══██╗████╗ ██║██╔════╝██║ ██║██║████╗ ██║██╔════╝ ██████╔╝██████╔╝███████║██╔██╗ ██║██║ ███████║██║██╔██╗ ██║██║ ███╗ ██╔══██╗██╔══██╗██╔══██║██║╚██╗██║██║ ██╔══██║██║██║╚██╗██║██║ ██║ ██████╔╝██║ ██║██║ ██║██║ ╚████║╚██████╗██║ ██║██║██║ ╚████║╚██████╔╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═════╝
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.
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.
Postgres compute is ephemeral. Storage is durable, independent, and replicated by Paxos quorum — never tied to a single VM.
Branch your database like a Git repo. Branches are instant, isolated, and carry all parent data without copying a single byte at creation time.
Idle computes suspend automatically after 5 minutes. Reactivation is sub-second. Pay only for active CU-time.
HTTP and WebSocket transports replace TCP, enabling Postgres queries from Vercel Edge Functions, Cloudflare Workers, and other edge runtimes that ban raw sockets.
PgBouncer-based connection pooling absorbs serverless connection storms. Each invocation doesn't need its own Postgres backend process.
Vector embeddings via the pgvector extension. HNSW indexing on up to 2,000 dimensions; switch to halfvec for 3072-dim models.
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 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.
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.
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.
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.
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.
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.
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:
connect_timeout=10 to the connection string as a query param| 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 |
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.
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 }}
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.
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.
directUrl = env("DIRECT_URL") for prisma migrate; use pooled DATABASE_URL for runtime queriesdrizzle.config.ts at the unpooled URL; runtime queries use pooledprepare_database_migration rehearses on a throwaway branch; complete_database_migration runs on mainStandard 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 }
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.
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.
Neon provides two connection string variants. The pooled string adds -pooler to the endpoint hostname:
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.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.
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.
halfvec (for 3072-dim models like OpenAI text-embedding-3-large) stores at half precision, roughly halving storage vs vectorneon()) 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.neondatabase/delete-branch-action.