Skip to main content
Technology & EngineeringServerless177 lines

Serverless Databases

Expert guidance for using serverless databases like PlanetScale, Neon, and Turso in serverless applications

Quick Summary19 lines
You are an expert in serverless databases (PlanetScale, Neon, Turso) for building serverless applications. You help teams choose the right serverless database for their consistency, latency, and compatibility requirements, and configure connection strategies that work correctly in ephemeral compute environments.

## Key Points

- Use the HTTP/fetch-based query drivers (not TCP connection pools) for short-lived serverless functions — they avoid connection overhead and work in edge runtimes that lack TCP socket support.
- Adopt database branching (Neon, PlanetScale) to give every preview deployment its own isolated database — run migrations safely without affecting production.
- Opening a new TCP connection on every Lambda invocation exhausts connection limits fast — use HTTP-based drivers or external poolers (PgBouncer, Neon's built-in pooler) in serverless contexts.

## Quick Example

```bash
npm install @neondatabase/serverless
```

```bash
npm install @planetscale/database
```
skilldb get serverless-skills/Serverless DatabasesFull skill: 177 lines
Paste into your CLAUDE.md or agent config

Serverless Databases — Serverless

You are an expert in serverless databases (PlanetScale, Neon, Turso) for building serverless applications. You help teams choose the right serverless database for their consistency, latency, and compatibility requirements, and configure connection strategies that work correctly in ephemeral compute environments.

Core Philosophy

The fundamental problem serverless databases solve is the impedance mismatch between stateless, ephemeral compute and stateful, persistent databases. Traditional databases expect long-lived TCP connections from a stable pool of application servers; serverless functions spin up thousands of short-lived instances that each try to open their own connection. Serverless databases address this with HTTP-based query protocols, built-in connection pooling, and scale-to-zero pricing that eliminates idle costs. Choosing the right approach to connectivity — HTTP driver vs. pooled TCP vs. direct connection — is the single most important decision.

Data locality determines user-perceived latency. A serverless function running at the edge that queries a database in a single region pays the round-trip latency on every query, negating the edge advantage. Match your database topology to your compute topology: Turso's embedded replicas for sub-millisecond edge reads, Neon's read replicas for regional distribution, or PlanetScale's Vitess-based sharding for horizontal scale. The database's replication model and consistency guarantees should drive the architectural decision, not just the SQL dialect.

Database branching is a workflow revolution for teams using serverless databases with preview deployments. Every pull request gets its own isolated database branch with production schema and optionally seeded data, making it safe to run migrations, test schema changes, and review data-dependent features without touching production. Adopt branching as a core part of the development workflow, not an afterthought.

Anti-Patterns

  • Opening a new TCP connection per function invocation — Serverless functions can scale to thousands of concurrent instances. Each one opening a direct TCP connection quickly exhausts the database's connection limit. Use HTTP-based drivers or external connection poolers (PgBouncer, Neon's pooler) for serverless workloads.
  • Using connection pools inside short-lived functions — A connection pool inside a Lambda function that runs for 200 ms and then freezes wastes connections that sit idle in the frozen execution environment. Use HTTP/fetch-based drivers for short-lived functions; reserve pooled connections for long-running servers.
  • Assuming foreign key support on PlanetScale — PlanetScale uses Vitess, which does not support database-level foreign key constraints. Relying on foreign keys and discovering this after migration causes data integrity issues. Evaluate this constraint early and enforce referential integrity in the application layer if using PlanetScale.
  • Ignoring consistency models — Neon read replicas and Turso edge replicas provide eventual consistency. Reading your own writes immediately after a write may return stale data unless you route read-after-write queries to the primary. Understand the replication lag and design read paths accordingly.
  • Skipping database branching in CI/CD — Running migrations directly against production or a shared staging database is risky and blocks parallel development. With Neon and PlanetScale offering zero-cost branching, there is no reason not to give every preview deployment its own isolated database.

Overview

Traditional databases struggle in serverless environments due to connection limits and idle costs. Serverless databases solve these problems with HTTP-based query protocols, connection pooling, branching workflows, and scale-to-zero pricing. PlanetScale offers MySQL-compatible Vitess, Neon provides serverless Postgres with branching, and Turso delivers embedded SQLite at the edge via libSQL.

Setup & Configuration

Neon (Serverless Postgres)

npm install @neondatabase/serverless
import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_URL!);

// Simple query over HTTP — no persistent connection needed
const users = await sql`SELECT * FROM users WHERE active = true`;

// Parameterized query
const user = await sql`SELECT * FROM users WHERE id = ${userId}`;

PlanetScale (Serverless MySQL)

npm install @planetscale/database
import { connect } from '@planetscale/database';

const conn = connect({
  host: process.env.DATABASE_HOST,
  username: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD,
});

const results = await conn.execute('SELECT * FROM products WHERE category = ?', [category]);

Turso (Edge SQLite via libSQL)

npm install @libsql/client
import { createClient } from '@libsql/client';

const db = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN,
});

const result = await db.execute({
  sql: 'SELECT * FROM posts WHERE author_id = ?',
  args: [authorId],
});

Using Drizzle ORM across providers

// drizzle.config.ts — works with Neon, PlanetScale, or Turso
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql', // or 'mysql' for PlanetScale, 'sqlite' for Turso
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});
// src/db/schema.ts
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: timestamp('created_at').defaultNow(),
});

Core Patterns

Connection pooling with Neon

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

// Use Pool when you need transactions or multiple queries in sequence
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 balance = balance - $1 WHERE id = $2', [amount, fromId]);
  await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toId]);
  await client.query('COMMIT');
} catch (e) {
  await client.query('ROLLBACK');
  throw e;
} finally {
  client.release();
}

Database branching for preview deployments (Neon)

# Create a branch from production for a pull request
neonctl branches create --name pr-42 --parent main

# Each preview deployment gets its own isolated database copy
# Set DATABASE_URL in the preview environment to the branch's connection string

Turso embedded replicas for edge reads

import { createClient } from '@libsql/client';

const db = createClient({
  url: 'file:local-replica.db',           // local embedded replica
  syncUrl: process.env.TURSO_DATABASE_URL, // remote primary
  authToken: process.env.TURSO_AUTH_TOKEN,
  syncInterval: 60,                        // sync every 60 seconds
});

// Reads from local replica — sub-millisecond latency
const posts = await db.execute('SELECT * FROM posts ORDER BY created_at DESC LIMIT 20');

Best Practices

  • Use the HTTP/fetch-based query drivers (not TCP connection pools) for short-lived serverless functions — they avoid connection overhead and work in edge runtimes that lack TCP socket support.
  • Adopt database branching (Neon, PlanetScale) to give every preview deployment its own isolated database — run migrations safely without affecting production.
  • Choose the database that matches your latency requirements: Turso for globally distributed reads at the edge, Neon for Postgres compatibility with branching, PlanetScale for MySQL compatibility with horizontal sharding.

Common Pitfalls

  • Opening a new TCP connection on every Lambda invocation exhausts connection limits fast — use HTTP-based drivers or external poolers (PgBouncer, Neon's built-in pooler) in serverless contexts.
  • PlanetScale does not support foreign key constraints at the database level (Vitess limitation) — enforce referential integrity in your application layer or use Neon/Turso if foreign keys are critical.

Install this skill directly: skilldb add serverless-skills

Get CLI access →