Neon Serverless PostgreSQL Patterns
Quick Guide: Use@neondatabase/serverlessfor edge/serverless database access. Prefer theneon()HTTP function for single queries (faster, stateless) andPool/Clientfor interactive transactions. Use pooled connection strings (-poolersuffix) for serverless workloads, direct connections only for migrations. Branch your database for dev/preview environments using copy-on-write semantics. Always handle cold starts from scale-to-zero (200-500ms wake-up).
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)
(You MUST use the neon() HTTP function for single queries in edge/serverless runtimes -- it is 2-3x faster than WebSocket for one-shot operations)
(You MUST close Pool/Client connections within the same request handler in serverless environments -- WebSocket connections cannot outlive a single request)
(You MUST use pooled connection strings (-pooler suffix) for serverless workloads -- direct connections exhaust the limited connection slots)
(You MUST handle scale-to-zero wake-up latency (200-500ms) with appropriate connection timeouts and retry logic)
(You MUST use sql.unsafe() only for trusted, known-safe strings like table/column names -- never for user input)
</critical_requirements>
Auto-detection: Neon, @neondatabase/serverless, neon(), neonConfig, neon serverless driver, neon database, neon branch, neonctl, neon connection pooling, neon scale-to-zero, neon autoscaling, neon postgres, ep-\*-pooler
When to use:
- Querying Postgres from edge/serverless functions (edge runtimes, serverless platforms)
- Setting up connection strings (pooled vs direct) for different workloads
- Creating database branches for dev, preview, or CI environments
- Managing scale-to-zero behavior and cold start optimization
- Running transactions in serverless contexts (HTTP batch or WebSocket)
- Programmatic branch management via Neon API or neonctl CLI
Key patterns covered:
neon()HTTP queries with SQL tagged templates and composable fragmentsPool/ClientWebSocket connections with proper lifecycle management- Pooled (
-pooler) vs direct connection strings and when to use each - Database branching (dev branches, PR preview branches, schema-only branches)
- Scale-to-zero behavior, cold start mitigation, and autoscaling
sql.transaction()for non-interactive HTTP transactions- Neon API and neonctl CLI for programmatic branch management
When NOT to use:
- Traditional long-lived server connections (use standard
pgdriver with TCP) - Complex ORM-specific patterns (use your ORM's own skill)
- General PostgreSQL query syntax (use a SQL/Postgres skill)
Detailed Resources:
- For decision frameworks and quick lookup tables, see [reference.md](reference.md)
Driver & Queries:
- [examples/core.md](examples/core.md) -- Driver setup, HTTP queries, WebSocket connections, transactions
Branching & Operations:
- [examples/branching.md](examples/branching.md) -- Dev branches, PR previews, neonctl CLI, Neon API, CI/CD workflows
<philosophy>
Philosophy
Neon separates storage and compute for PostgreSQL, enabling serverless features impossible with traditional Postgres: scale-to-zero, instant branching, and autoscaling. The @neondatabase/serverless driver replaces TCP with HTTP and WebSockets, making Postgres accessible from edge runtimes that lack TCP support.
Core principles:
- HTTP for speed, WebSocket for sessions -- The
neon()function uses HTTP fetch (~3 round trips) for single queries.Pool/Clientuse WebSockets (~8 round trips) when you need sessions or interactive transactions. Pick the right transport for the job. - Pooled by default -- Pooled connections route through PgBouncer (transaction mode), handling up to 10,000 concurrent clients. Direct connections are limited by compute size (100-4,000) and should only be used for migrations or features requiring session state.
- Branches are cheap -- Copy-on-write means a branch of a 500GB database allocates no extra storage until data diverges. Use branches freely for dev, preview, testing, and CI.
- Scale-to-zero is the default -- Computes suspend after 5 minutes of inactivity. Cold starts take 200-500ms. Design for this with timeouts, retries, and connection pooling.
- SQL injection safety built in -- The tagged template function parameterizes automatically. Since v1.0, calling
neon()as a regular function is a type error, preventing accidental injection.
When to use Neon serverless driver:
- Edge/serverless functions that cannot open TCP connections
- Applications benefiting from database branching (preview environments per PR)
- Cost-sensitive workloads that benefit from scale-to-zero
- High-concurrency serverless apps needing connection pooling
When NOT to use:
- Long-running server processes with persistent connections (use standard
pgover TCP) - Workloads requiring session-level features through PgBouncer (LISTEN/NOTIFY, SET, temporary tables)
- Databases larger than 16 CU that need always-on compute (scale-to-zero not available above 16 CU)
</philosophy>
<patterns>
Core Patterns
Pattern 1: HTTP Queries with neon()
The neon() function creates an HTTP-based query function using SQL tagged templates. It is the fastest path for single, non-interactive queries.
import { neon } from "@neondatabase/serverless";
const DATABASE_URL = process.env.DATABASE_URL!;
const sql = neon(DATABASE_URL);
// Tagged template -- parameters are auto-parameterized (safe from injection)
const userId = "abc-123";
const posts =
await sql`SELECT id, title FROM posts WHERE author_id = ${userId}`;
Why good: Tagged template auto-parameterizes values preventing SQL injection, HTTP is ~3 round trips vs ~8 for WebSocket, stateless (no connection to manage)
// BAD: Calling neon() as a regular function (v1.0+ type error)
const sql = neon(DATABASE_URL);
const result = await sql(`SELECT * FROM posts WHERE id = ${id}`); // TYPE ERROR + SQL injection risk
Why bad: Since v1.0, calling the query function as a regular function (not a tagged template) is a runtime and type error -- this was changed specifically to prevent SQL injection from string interpolation
sql.query() for Dynamic SQL
When the query string is in a variable (not a template literal), use the .query() method with numbered placeholders:
const q = "SELECT * FROM posts WHERE id = $1 AND status = $2";
const posts = await sql.query(q, [postId, "published"]);
When to use: Dynamic SQL strings built at runtime, or queries stored in variables. Parameters are still safely parameterized via $1, $2, etc.
Pattern 2: Composable Query Fragments
Template queries support composition with automatic parameter numbering across fragments.
import { neon } from "@neondatabase/serverless";
const sql = neon(DATABASE_URL);
// Build queries from reusable fragments
const whereClause = sql`WHERE status = ${"active"} AND role = ${"admin"}`;
const orderClause = sql`ORDER BY created_at DESC`;
const PAGE_SIZE = 20;
const limitClause = sql`LIMIT ${PAGE_SIZE}`;
const users =
await sql`SELECT id, name, email FROM users ${whereClause} ${orderClause} ${limitClause}`;
Why good: Parameters are renumbered automatically across composed fragments, named constant for page size, fragments are reusable across queries
Dynamic Table/Column Names with sql.unsafe()
// ONLY for trusted, known-safe values -- never user input
const TABLE_NAME = "posts";
const results =
await sql`SELECT * FROM ${sql.unsafe(TABLE_NAME)} WHERE id = ${postId}`;
When to use: Only when you need to interpolate trusted identifiers (t