50% off SaaS Starter Kit — only for the first 100 buildersGrab it →
← Back to blog
databaseMay 26, 2026·7 min read

Neon Postgres: The Serverless Database That Actually Works

We've tried a lot of serverless databases. Most are frustrating. Neon is different — here's why we use it and what you need to know before you do too.

Ștefan Binisor

Ștefan Binisor

Co-founder, peal.dev

Neon Postgres: The Serverless Database That Actually Works

We've burned enough time on flaky database setups that we've started treating 'serverless database' as a red flag. Fauna confused us. PlanetScale changed their pricing and then their product. Supabase is great until you hit the edge cases. And don't get us started on trying to manage RDS from a laptop at a gas station at 2am because a migration went sideways.

Neon is different. We started using it about a year ago and it's now the default database we ship in our templates. Not because we got paid to say that, but because it's the first serverless Postgres that actually behaves like Postgres. No proprietary query language. No weird connection semantics. Just Postgres, with a branching model that makes you wonder why databases didn't always work this way.

What Makes Neon Actually Serverless

Most 'serverless' databases are just managed databases with a friendlier dashboard. Neon actually separates compute from storage. Your database can scale to zero when nobody's using it, and spin back up in milliseconds when a request comes in. That cold start used to be a dealbreaker — early Neon had noticeable latency on first connection. They've fixed this. In 2024/2025 the cold start is fast enough that you won't notice it on the first real-world request.

The storage layer is shared and durable. The compute layer (the Postgres process) spins up on demand and is billed per second of actual use. For a side project getting 50 requests a day, you pay almost nothing. For a production app with steady traffic, it scales without you touching anything. This model maps well to how most SaaS products actually grow — slowly at first, then suddenly.

Setting Up Neon with Next.js (The Right Way)

Neon has a Node.js driver that handles connection pooling correctly for serverless environments. This is important. The default `pg` package opens persistent connections, which is fine for a long-running server but disastrous in serverless where every function invocation might create a new connection. Neon's driver uses HTTP under the hood for single queries and WebSockets for transactions, which plays nicely with serverless function lifecycles.

pnpm add @neondatabase/serverless drizzle-orm drizzle-kit
pnpm add -D @types/pg

Here's how we set up the database client. We use Drizzle ORM on top of Neon because it gives us type-safe queries without the heaviness of Prisma. The setup is minimal:

// lib/db.ts
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import * as schema from './schema';

if (!process.env.DATABASE_URL) {
  throw new Error('DATABASE_URL is not set');
}

const sql = neon(process.env.DATABASE_URL);
export const db = drizzle(sql, { schema });

// Usage in a Server Component or Route Handler:
// const users = await db.select().from(schema.users).limit(10);

One thing to pay attention to: if you need transactions, you switch from `neon-http` to `neon-serverless` which uses WebSockets. You can't do multi-statement transactions over HTTP. It's not a huge deal but it trips people up the first time.

// lib/db-pool.ts — use this when you need transactions
import { Pool } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-serverless';
import * as schema from './schema';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });

// Now you can do transactions:
await db.transaction(async (tx) => {
  await tx.insert(schema.orders).values({ userId, total });
  await tx.update(schema.users)
    .set({ orderCount: sql`order_count + 1` })
    .where(eq(schema.users.id, userId));
});

Database Branching: The Feature That Changes How You Work

This is the thing that genuinely surprised us. Neon has Git-style branching for your database. You create a branch and it instantly gets a copy-on-write snapshot of your main database. You can test migrations against real production data (or a sanitized version) without touching production. When you're done, you delete the branch.

The practical workflow we use: every pull request gets its own Neon branch via a GitHub Action. When the PR is merged, the branch is deleted. This means every preview deployment has its own isolated database with current schema. No more 'it works on my machine' database issues. No more preview deployments sharing a dev database and corrupting each other's test data.

# .github/workflows/preview.yml
name: Preview Deployment

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  create-branch:
    runs-on: ubuntu-latest
    steps:
      - name: Create Neon branch
        uses: neondatabase/create-branch-action@v5
        id: create-branch
        with:
          project_id: ${{ secrets.NEON_PROJECT_ID }}
          api_key: ${{ secrets.NEON_API_KEY }}
          branch_name: preview/pr-${{ github.event.number }}
          
      - name: Run migrations on branch
        env:
          DATABASE_URL: ${{ steps.create-branch.outputs.db_url }}
        run: pnpm drizzle-kit migrate

  cleanup:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - name: Delete Neon 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.number }}
Database branching sounds like a gimmick until you're on a team and someone's PR migration broke everyone's local dev database. Then it sounds like a religion.

The Connection Pooling Question

Neon has built-in connection pooling via PgBouncer. You get two connection strings: one that goes directly to Postgres, and one that goes through the pooler. For serverless workloads — Next.js API routes, Server Actions, Vercel Functions — always use the pooled connection string. It ends in `-pooler.neon.tech` in the hostname.

The direct connection is useful for migrations (you don't want migrations going through a connection pooler — it can cause weird behavior with prepared statements) and for local development. We set two env variables:

# .env.local
# Pooled — use this in your app
DATABASE_URL=postgresql://user:pass@ep-xxx-pooler.region.aws.neon.tech/dbname?sslmode=require

# Direct — use this for migrations only  
DATABASE_URL_UNPOOLED=postgresql://user:pass@ep-xxx.region.aws.neon.tech/dbname?sslmode=require
// drizzle.config.ts
import type { Config } from 'drizzle-kit';

export default {
  schema: './lib/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    // Use direct connection for migrations — NOT the pooler
    url: process.env.DATABASE_URL_UNPOOLED!,
  },
} satisfies Config;

What Neon Gets Wrong (Being Honest)

Nothing is perfect. Here's what actually annoys us about Neon:

  • The free tier only gets you one project. If you're experimenting with multiple side projects, you'll hit this quickly.
  • Scale-to-zero means your first request after a period of inactivity has slightly more latency. In practice this is fine for most apps, but if you have strict p99 SLAs on every request, you might want to keep compute warm.
  • The dashboard is good but Neon's UI has had some rough patches — there were a few months where the query editor was buggy. It's better now, but we've gotten used to just using psql directly.
  • Region selection is limited compared to RDS. If your users are in South America or Southeast Asia, you're picking the least-bad option rather than the right option.
  • Autoscaling compute can spike costs if you have a genuinely spiky workload. Set a max compute units cap so you don't get surprised.

None of these are dealbreakers for the kind of apps we build — B2B SaaS, developer tools, template-based projects. But they're real, and you should know about them before you commit.

Migrations in Production Without Losing Sleep

The workflow we've settled on: generate migrations locally with Drizzle Kit, review the SQL, commit it, then run it against production as part of the deployment pipeline. Not during the deployment (that's a race condition waiting to happen), but before the new code goes live.

// scripts/migrate.ts — run this before deploying new code
import { drizzle } from 'drizzle-orm/neon-http';
import { migrate } from 'drizzle-orm/neon-http/migrator';
import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_URL_UNPOOLED!);
const db = drizzle(sql);

async function main() {
  console.log('Running migrations...');
  await migrate(db, { migrationsFolder: './drizzle' });
  console.log('Migrations complete.');
  process.exit(0);
}

main().catch((err) => {
  console.error('Migration failed:', err);
  process.exit(1);
});

Add this to your Vercel build command or as a separate job in GitHub Actions that runs before your deploy job. The critical thing is using the unpooled direct connection for migrations — we learned this the hard way when a migration seemed to succeed but the schema change wasn't fully applied because PgBouncer was doing something weird with the transaction.

Should You Use Neon?

If you're building a Next.js app on Vercel or similar serverless infrastructure, yes. The combination of proper serverless connection handling, branching for preview environments, and actual Postgres compatibility makes it the best option we've found. The fact that you can use every Postgres feature — full-text search, JSONB, pg_vector for AI embeddings, PostGIS if you need it — without compromise is genuinely valuable.

If you're self-hosting on a VPS with a long-running Node process, you don't need Neon specifically. Just use regular Postgres with a connection pooler like PgBouncer or pgpool. Neon's value proposition is specifically around serverless workloads and developer experience.

For what it's worth, all the database-backed templates we ship on peal.dev come wired up with Neon + Drizzle by default, with the branching workflow and migration scripts already set up. We made those choices because we use them ourselves and they've caused us the least amount of 2am headaches.

The best database is the one you don't have to think about. Neon is as close as we've gotten to that.

Start on the free tier, get branching working in CI, and switch to the paid tier when you have real users. That's the playbook. The migration to paid is easy because the API is the same — you're just changing a connection string and picking a region.

Newsletter

Liked this post? There's more where it came from.

Dev guides, honest build stories, and the occasional 2am debugging confession — straight to your inbox. No spam, unsubscribe anytime.

Browse templates
Written by humansWeekly dropsSubscriber perks

Join the Discord

Ask questions, share builds, get help from founders