4. Juli 2026 · Piotr Józef Schumann
Drizzle ORM + Neon auf Vercel: ein Serverless-Setup, das keine Verbindungen leakt
Als ich zum ersten Mal eine Postgres-gestützte Next.js-App unter echtem Traffic auf Vercel gestellt habe, ist sie mit sorry, too many clients already umgekippt. Die Datenbank hatte kaum etwas zu tun — das Problem war, dass serverless und klassische Postgres-Verbindungen einfach nicht zusammenpassen, und niemand warnt dich davor, bis die Production es tut.
Das ist die Datenbankschicht, die ich mittlerweile auf jedem God-Stack-Projekt ausliefere: Drizzle ORM + Neon auf Vercel, so aufgesetzt, dass sie keine Verbindungen leakt. Hier ist das Ganze — warum es bricht, welchen Neon-Treiber du greifen solltest und die überschaubare Zahl an Entscheidungen, die es zuverlässig machen.
Warum serverless Postgres auslaugt
Ein klassischer Postgres-Server hat ein hartes max_connections-Limit — oft 100 auf einer kleinen Instanz. Das ist völlig in Ordnung für einen einzelnen, langlebigen Node-Prozess, der einen Pool von, sagen wir, 10 Verbindungen hält.
Serverless bricht diese Annahme. Jeder Function-Aufruf kann seine eigene kurzlebige Instanz sein, und wenn jeder Aufruf seine eigene Datenbankverbindung öffnet, reißt ein Traffic-Spike Hunderte davon gleichzeitig auf. Du läufst gegen max_connections, neue Requests bekommen keine Verbindung mehr, und die App wirft Fehler. Die Datenbank ist nicht überlastet — die Verbindungsanzahl ist es.
Es gibt zwei Auswege, und das gute Setup nutzt beide:
- Setze einen Connection Pooler vor Postgres, damit Tausende Clients sich einen kleinen Pool echter Verbindungen teilen.
- Nutze einen Treiber, der keine persistente Verbindung hält für den Request/Response-Pfad.
Neon gibt dir das Erste kostenlos, und sein serverless Treiber gibt dir das Zweite.
Die zwei Neon-Treiber — und wann du welchen nutzt
Neons @neondatabase/serverless-Paket bringt zwei Wege zum Verbinden mit, und Drizzle hat für jeden einen Adapter. Den richtigen zu wählen ist 90 % davon, das hier richtig zu machen.
HTTP-Treiber (drizzle-orm/neon-http) — jede Query ist ein zustandsloser HTTPS-Request. Es gibt keine Verbindung, die offen gehalten werden muss, nichts zu leaken. Das ist der Default, zu dem ich in Next.js greife: Die überwältigende Mehrheit der Route Handler und Server Components macht ein, zwei Queries und liefert zurück.
// db.ts
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle({ client: sql, schema });
WebSocket-Pool-Treiber (drizzle-orm/neon-serverless) — eine echte Session über einen WebSocket, die du für interaktive Transaktionen brauchst: mehrere Statements, die auf derselben Verbindung laufen müssen, mit App-Logik dazwischen (db.transaction(async (tx) => { ... })). Der HTTP-Treiber kann das nicht.
// db-pool.ts — nur dort, wo du interaktive Transaktionen brauchst
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({ client: pool, schema });
Die Regel, der ich folge: HTTP als Default, Pool nur dort, wo du wirklich eine Transaktion brauchst. Die meisten Apps liefern eine einzige db.ts auf dem HTTP-Treiber aus und fassen die Pool-Variante nie an.
Die pooled Connection String ist nicht optional
Neon gibt dir zwei Connection Strings, und der Unterschied ist das ganze Spiel:
- Pooled — der Host sieht aus wie
...-pooler.region.aws.neon.tech. Läuft über Neons PgBouncer, sodass viele Clients sich eine Handvoll echter Postgres-Verbindungen teilen. Das ist der, den deine App zur Laufzeit nutzt. - Direct — kein
-pooler. Eine rohe Verbindung zu Postgres. Den willst du für Migrationen (dazu unten mehr).
Setze DATABASE_URL auf die pooled String. Wenn du deine laufende App auf die direkte richtest, hast du den Pooler weggeworfen und bist zurück beim Auslaugen von Verbindungen unter Last.
Das Schema definieren
Nichts Exotisches — eine schlichte Drizzle-Schema-Datei. Das ist auch, was dir End-to-End-Typsicherheit gibt: Die abgeleiteten Typen fließen direkt in deine Queries.
// schema.ts
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
export const posts = pgTable("posts", {
id: serial("id").primaryKey(),
title: text("title").notNull(),
slug: text("slug").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
Migrationen — und warum sie die direkte URL nutzen
Drizzle Kit generiert versioniertes SQL aus deinem Schema und wendet es an. Die Config:
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "postgresql",
schema: "./schema.ts",
out: "./drizzle",
dbCredentials: {
// DIREKTE (unpooled) URL — nicht der -pooler-Host
url: process.env.DATABASE_URL_UNPOOLED!,
},
});
npx drizzle-kit generate # Schema-Diff -> SQL-Migrationsdateien
npx drizzle-kit migrate # anwenden
Migrationen laufen gegen die direkte Verbindung, weil PgBouncer im Transaction-Pooling-Modus die Session-Level-Operationen nicht unterstützt, die manches DDL braucht. Halte also zwei Env-Variablen: DATABASE_URL (pooled, für die App) und DATABASE_URL_UNPOOLED (direkt, für drizzle-kit). Neon zeigt dir beide im Dashboard.
Der Teil, den Fluid Compute leise behebt
Es gibt ein zweites, subtileres Leak: den Client innerhalb eines Request Handlers zu erzeugen. Jeder Request baut einen neuen Client, und beim Pool-Treiber heißt das jedes Mal einen neuen Pool.
Instanziiere den Client einmal auf Modul-Ebene — so wie in der db.ts oben — und importiere dieses Singleton überall. Code auf Modul-Ebene läuft einmal pro Instanz, nicht einmal pro Request.
Hier hilft Vercels Fluid Compute, statt zu schaden. Fluid verwendet eine warme Function-Instanz über viele Aufrufe hinweg wieder, statt des alten Ein-Request-pro-Instanz-Modells, sodass ein Client auf Modul-Ebene (und, beim Pool-Treiber, sein Pool) einmal erzeugt und über alle Requests wiederverwendet wird, die diese Instanz bedient. Weniger Cold Starts, weniger neue Verbindungen. Der alte serverless Ratschlag, „pro Request eine Verbindung zu öffnen und zu schließen", ist hier genau falsch — du willst das Gegenteil.
Die pooled Connection String behältst du trotzdem. Fluid reduziert, wie oft du Clients erzeugst; der Neon-Pooler übernimmt die Verteilung, wenn der Traffic explodiert und mehrere Instanzen gleichzeitig warm sind. Doppelt hält besser.
Das Env auf Vercel verdrahten
Setze beide Variablen im Vercel-Projekt (oder zieh sie lokal):
vercel env pull .env.local
.env.local für die lokale Entwicklung, das Vercel-Dashboard (oder vercel env add) für Preview und Production. Neons Vercel-Integration kann diese für dich injizieren und jedem Preview-Deployment sogar einen eigenen Datenbank-Branch geben — aber das ist ein eigener Beitrag.
Die ganze Entscheidung, auf einer Seite
- Default-Treiber:
neon-http. Zustandslos, nichts zu leaken. - Nur für interaktive Transaktionen:
neon-serverlessPool. - App-Connection-String: die pooled (
-pooler) URL, inDATABASE_URL. - Migrationen: die direkte URL, über
drizzle-kitund eine separate Env-Variable. - Client-Lebensdauer: einmal auf Modul-Ebene gebaut, überall importiert — niemals innerhalb eines Handlers.
Mach diese fünf Dinge, und die Postgres-Verbindungsanzahl hört auf, etwas zu sein, worüber du nachdenkst — selbst wenn eine Kampagne um 9 Uhr morgens einen Traffic-Spike schickt. Es ist nicht clever — es ist einfach, den Treiber und die Connection String daran anzupassen, wie serverless deinen Code tatsächlich ausführt.