"Vercel Postgres" began as a Postgres database you could create from the Vercel dashboard, billed by Vercel and accessed through the @vercel/postgres package. It was always Neon underneath. Between the fourth quarter of 2024 and the first quarter of 2025, Vercel moved every Vercel Postgres store to its native Neon integration in the Vercel Marketplace, and it now offers Postgres through that marketplace rather than as its own product. According to Neon's transition guide, @vercel/postgres still works but is no longer maintained by Vercel.

So in practice, "Vercel Postgres" now means a serverless Neon Postgres database provisioned and billed through Vercel, wired into your project's environment variables and preview deployments. This page explains what that system is, how it behaves when called from serverless functions, how to write code that will not exhaust connections or stall on cold starts, how to migrate off the old package, and when the combination is the wrong choice. Plan prices and quotas are left out on purpose: they changed after Databricks acquired Neon in 2025 and will change again, so read them from the current pricing pages.

What runs underneath

Neon separates Postgres into stateless compute and durable storage. The compute is ordinary Postgres with its buffer cache and a local cache, but it does not keep the authoritative copy of anything. Instead of writing write-ahead log to a local disk, it streams WAL to a set of safekeepers, and a transaction is committed once a quorum of safekeepers has acknowledged it through a Paxos protocol. A pageserver consumes the WAL and can return any page as of any log sequence number (LSN), reconstructing it from a base image plus WAL if needed. Object storage holds the long-term history. Neon's documentation now also calls this engine Lakebase Postgres, the name Databricks uses for it.

Vercel Functionroute handler, per requestPreview deploymentbranch preview/feature-xNeon proxyTLS, HTTP and WebSocketPgBouncer (-pooler)transaction modePostgres computestateless, scales to zeroSafekeepersWAL quorum (Paxos)Pageserverpages at any LSNObject storagedurable history, branches share itHTTP fetchTCP/WSWAL streampage reads
A request path from a Vercel function to Neon. Compute is disposable; durability lives in the safekeeper quorum and the pageserver, which serve all branches from shared history.

Three product features fall out of that design. Scale to zero: because compute holds no unique state, Neon stops it after 5 minutes of inactivity by default and restarts it on the next connection, which Neon says takes a few hundred milliseconds. Branching: a branch is a new pointer into the shared history at some LSN, so creating a full copy of a large database for a preview deployment is copy-on-write and nearly instant. Point-in-time restore: the history is retained, so restoring means starting a branch at an earlier LSN. If the WAL concepts are unfamiliar, the Postgres WAL deep dive covers them, and cloud-native databases compared places this design next to Aurora and AlloyDB.

Connecting from serverless functions

Serverless functions break the classic Postgres connection model. A traditional application server opens a pool of perhaps 20 connections at startup and reuses them for hours. A Vercel function may run as hundreds of concurrent instances that live for seconds, and each one that opens its own TCP connection costs a Postgres backend process, a TLS handshake and an authentication round trip. Postgres's max_connections is a few hundred on small computes, so a traffic spike can exhaust it in seconds. The general patterns are in connection pooling architecture; Neon gives you three specific tools.

PathHow to select itUse it forLimits
Pooled TCPDATABASE_URL (hostname contains -pooler)Most application queries from functionsPgBouncer transaction mode: no SET/RESET, LISTEN/NOTIFY, SQL PREPARE, session advisory locks
Direct TCPDATABASE_URL_UNPOOLEDMigrations, pg_dump/pg_restore, logical replication, session featuresCounts against max_connections directly
HTTP (serverless driver)neon(DATABASE_URL)One-shot queries and non-interactive transactionsNo session; 64 MB request/response cap
WebSocket (serverless driver)new Pool(...) inside the handlerInteractive transactions, node-postgres compatibilityMust open and close within one request

Neon's PgBouncer accepts up to 10,000 client connections and multiplexes them onto a server pool sized from the compute's max_connections. Transaction mode means a server connection is lent to a client only for the duration of one transaction, which is why session state does not survive. Protocol-level prepared statements issued by drivers are supported; SQL-level PREPARE is not.

A worked example shows why this matters. Neon's pooling page gives max_connections of 419 for a 1 CU compute, with the pooler's per-user, per-database pool set to 90% of that, 377. Suppose a product launch drives 600 concurrent function invocations, each holding a connection for a 40 ms query. On the direct URL, connections somewhere past 400 fail with Postgres's sorry, too many clients already error and the launch page returns errors. On the pooled URL, all 600 clients are accepted, and because each server connection is busy only for the 40 ms of its transaction, 377 server connections could in principle complete more than 9,000 such transactions per second, counting connections only; the compute's CPU limits you well before that. The pooler turns a hard failure into, at worst, added waiting time, which is the behaviour you want under a spike.

Code: queries, transactions and migrations

For most route handlers the HTTP driver is the right default: each query is a single HTTPS request, so there is no connection to leak and nothing to warm up.

// app/api/orders/[id]/route.ts  (Next.js App Router on Vercel)
import { neon } from "@neondatabase/serverless";

const sql = neon(process.env.DATABASE_URL!);   // safe at module scope: no socket is opened

export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  // Tagged template: values are sent as parameters, never spliced into SQL text.
  const rows = await sql`SELECT id, status, total_cents FROM orders WHERE id = ${id}`;
  if (rows.length === 0) return Response.json({ error: "not found" }, { status: 404 });
  return Response.json(rows[0]);
}

export async function POST(req: Request) {
  const { userId, items } = await req.json();
  // Non-interactive transaction: all statements sent in one HTTP request.
  const [order] = await sql.transaction([
    sql`INSERT INTO orders (user_id, status) VALUES (${userId}, 'pending') RETURNING id`,
    sql`UPDATE carts SET checked_out = true WHERE user_id = ${userId}`,
  ]);
  return Response.json(order[0], { status: 201 });
}

When a transaction needs to read a value and decide what to write next, use the WebSocket Pool and keep its whole lifetime inside the request, because Neon's documentation is explicit that Pool and Client objects must be connected, used and closed within a single request handler.

import { Pool } from "@neondatabase/serverless";

export async function POST(req: Request) {
  const { accountId, amount } = await req.json();
  const pool = new Pool({ connectionString: process.env.DATABASE_URL });
  const client = await pool.connect();
  try {
    await client.query("BEGIN");
    const { rows } = await client.query(
      "SELECT balance FROM accounts WHERE id = $1 FOR UPDATE", [accountId]);
    if (rows[0].balance < amount) { await client.query("ROLLBACK"); return new Response("insufficient", { status: 409 }); }
    await client.query("UPDATE accounts SET balance = balance - $1 WHERE id = $2", [amount, accountId]);
    await client.query("COMMIT");
    return new Response(null, { status: 204 });
  } catch (e) {
    await client.query("ROLLBACK");
    throw e;
  } finally {
    client.release();
    await pool.end();
  }
}

Schema migrations should use the direct connection string, because migration tools rely on session features and advisory locks that the pooler strips. Point the migration tool at DATABASE_URL_UNPOOLED: in Prisma 7 that is the datasource url in prisma.config.ts (Prisma 6 used directUrl in the schema), and for Drizzle it is the URL drizzle-kit uses.

Migrating off @vercel/postgres

Applications written against @vercel/postgres keep working because the integration still provides the legacy POSTGRES_* variables such as POSTGRES_URL, alongside the newer DATABASE_URL, DATABASE_URL_UNPOOLED and the PGHOST, PGHOST_UNPOOLED, PGUSER, PGDATABASE and PGPASSWORD pieces. Running on an unmaintained client is still a liability. There are two exits.

  1. Drop-in: replace the package with @neondatabase/vercel-postgres-compat, which keeps the old API. Neon describes it as being in maintenance mode, so treat it as a bridge.
  2. Port to the serverless driver. The visible difference is the result shape. @vercel/postgres's sql template resolves to an object with a rows property, whereas neon()'s template resolves to the rows array itself unless you pass fullResults: true. Search for .rows after porting; a missed one returns undefined rather than throwing.

Port one route at a time behind the same environment variables, and diff responses in a preview deployment before merging.

A database branch per preview deployment

Preview branching is the feature that justifies the integration for many teams. With it enabled, each preview deployment triggers a webhook to Neon, which creates a branch named preview/<git-branch> from the parent, and the branch's connection strings are injected into that deployment only. They override the preview environment variables for that deployment and do not appear in the project's environment settings. Every pull request therefore gets a full-size, writable copy of the data, created by copy-on-write in seconds.

Two operational details follow. First, migrations should run against the preview branch during the preview build, so schema changes are tested on real data before they touch production. Second, cleanup follows deployment retention: under the Vercel-managed integration, branches are deleted when their deployments are removed, and Vercel keeps preview deployments for 6 months by default, so branches can accumulate for months. The Neon-managed integration ties cleanup to Git branch deletion instead. If production data is sensitive, branch from an anonymised or seeded parent rather than from production.

Cold starts and distance

Two latencies dominate. The first is the cold start of the compute after scale-to-zero: a few hundred milliseconds by Neon's account, added to the first query after an idle period. For a dashboard used during office hours that is invisible; for an API with a strict p99 and bursty traffic, disable scale-to-zero on production on a paid plan. Any session state, temporary tables and warm cache are gone after a suspend, so do not design anything that depends on them.

The second is distance. Every query is a network round trip between the function's region and the database's region. A page that issues ten sequential queries from Washington against a database in Frankfurt pays ten transatlantic round trips. Put the function region and the database region together, batch independent queries with sql.transaction([...]) or Promise.all, and be careful with Edge runtime functions that run in many locations: they reach a single-region database from wherever the user is. The trade-offs between edge and regional execution are discussed in Vercel Edge Functions and serverless architecture.

Failure modes

  • Connection exhaustion. Direct URLs used from functions run out of backends under load. Use the pooled URL or the HTTP driver for all request traffic.
  • Leaked WebSocket pools. A Pool created at module scope or never closed holds sockets across invocations. Create and end it inside the handler.
  • Session features through the pooler. SET search_path, LISTEN and advisory locks silently stop working. Use the unpooled URL for code that needs them.
  • Migration tools hanging. Migrations run through PgBouncer can fail on locks or prepared statements. Point migration tools at the direct URL.
  • Cold-start timeouts. A client timeout shorter than the resume time fails the first request after idle. Set timeouts with a margin, or keep production compute always on.
  • Stale preview branches. Hundreds of old preview branches consume storage and clutter the project. Audit and delete them.

When it fits and when it does not

Neon through Vercel is a strong fit for web applications with spiky or low traffic, many environments and a team that wants a database per pull request. It is a weaker fit for workloads that need long-lived sessions, heavy LISTEN/NOTIFY use, sustained high write throughput, or strict single-digit-millisecond latency, where an always-on managed Postgres such as RDS, Cloud SQL or a dedicated Neon compute in the same region may be simpler. Because it is standard Postgres, the exit is pg_dump over the direct URL or logical replication, so lock-in is mostly in tooling and branching workflow rather than in data.

What to do next

  1. Check which package your code imports. If it is @vercel/postgres, plan the port to @neondatabase/serverless.
  2. Make sure all request-path code uses DATABASE_URL (pooled) or the HTTP driver, and only migrations use DATABASE_URL_UNPOOLED.
  3. Put the function region and the database region together, and count sequential queries per request on your slowest page.
  4. Decide whether production can tolerate cold starts after idle; if not, disable scale-to-zero on a paid plan.
  5. Enable preview branching, run migrations in the preview build, and schedule a cleanup of stale branches.
  6. Read current plan limits and prices on the Vercel and Neon pricing pages before sizing, rather than relying on older figures.
Key takeaway: Vercel Postgres is now Neon Postgres provisioned through the Vercel Marketplace, and the old @vercel/postgres package is no longer maintained. Neon's separation of stateless compute from WAL-based storage gives scale-to-zero, instant branches and point-in-time restore, but serverless functions must reach it through the pooled URL or the HTTP driver, keep WebSocket pools inside one request, and use the direct URL only for migrations. Co-locate regions, plan for cold starts, and use preview branches with deliberate cleanup.