Warum Ihrer Datenbank auf Vercel die Verbindungen ausgehen
Eine Serverless-Funktionsinstanz kann keinen Verbindungspool mit der nächsten teilen. Das zu verstehen ist der Unterschied zwischen einer Treiberwahl und einem wiederkehrenden Vorfall.
Der Fehler kommt nach einem Deployment, das gut lief, meist beim ersten
Lastausschlag: too many connections, oder Timed out fetching a new connection from the connection pool. Die Datenbank ist im Leerlauf. Die
Anwendung tut nichts Ungewöhnliches. Derselbe Code läuft lokal seit Monaten.
Was sich geändert hat, ist, wie viele Kopien davon existieren.
Was tatsächlich anders ist
In der Entwicklung ist next dev ein Node-Prozess. Sie erzeugen einmal einen
Datenbank-Client, er öffnet einen Pool, und jede Anfrage in Ihrer Anwendung
teilt ihn. Das ist das Modell, das jedes Pooling-Tutorial annimmt, und es
stimmt.
Auf einer Serverless-Plattform läuft Ihr Route Handler in einer Funktionsinstanz. Jede Instanz hat ihren eigenen Modulbereich, ihren eigenen Speicher und damit ihren eigenen Pool. Zehn warme Instanzen mit einem Pool von zehn sind hundert Verbindungen, und nirgends in Ihrem Code steht die Zahl hundert.
Die Zahl wird nicht von gleichzeitigen Anfragen getrieben. Sie wird davon getrieben, wie viele Instanzen die Plattform warm hält - eine Zahl, die Sie nicht steuern und aus der Anwendung heraus nicht sehen.
Der Fehler, der es sofort auslöst
// app/api/orders/route.ts
export async function GET() {
const client = new PrismaClient(); // ← ein neuer Pool, je Anfrage
...
}Ein im Handler erzeugter Client öffnet bei jedem Aufruf einen Pool und überlässt ihn der Garbage Collection. Das erschöpft eine Datenbank unter echtem Verkehr in Minuten.
Die Korrektur auf Instanzebene erzeugt ihn einmal im Modulbereich, mit dem Vorbehalt, dass Hot Reloading Module neu auswertet und sonst Clients ansammeln würde:
// lib/db.ts
import { PrismaClient } from '@prisma/client';
const globalForDb = globalThis as unknown as { db?: PrismaClient };
export const db = globalForDb.db ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForDb.db = db;Das ist nötig und nicht hinreichend. Es begrenzt den Pool je Instanz. Es kann die Zahl der Instanzen nicht begrenzen.
Die drei echten Antworten
1. Ein Pooler vor der Datenbank. PgBouncer, oder wie Ihr Anbieter seinen nennt. Ihre Funktionen verbinden sich damit; er multiplext sie auf wenige echte Backends. Das ist die Standardantwort und die, nach der zuerst zu greifen ist.
Richtig zu treffen ist der Modus. Transaction Pooling erzeugt die Verbindungszahlen und gibt Ihnen ein Backend nur für die Länge einer Transaktion - alles, was über Anweisungen hinweg reicht, hört also auf zu funktionieren. Praktisch heißt das serverseitig vorbereitete Statements, die die meisten ORMs standardmäßig nutzen. Jeder Anbieter dokumentiert dafür ein Flag; setzen Sie es bewusst, statt es zu entdecken.
Behalten Sie eine zweite, direkte Verbindungszeichenkette für Migrationen. Schemaänderungen brauchen eine echte Sitzung, und sie durch einen Transaction-Pooler laufen zu lassen scheitert auf schwer lesbare Weise.
2. Ein Treiber, der gar keine Verbindung hält. Manche Anbieter stellen ihre Datenbank über HTTP bereit, eine Abfrage ist also eine Anfrage und es gibt nichts zu poolen:
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
const rows = await sql`select id, total from orders limit 20`;Das passt am saubersten zum Laufzeitmodell und ist das Einzige, was in einer Edge-Laufzeit ohne TCP-Sockets funktioniert. Der Tausch: eine einmalige HTTP-Abfrage ist keine Sitzung - interaktive Transaktionen brauchen den WebSocket-Weg, und ein Handler mit sechs aufeinanderfolgenden Abfragen zahlt sechs Roundtrips, wo eine gepoolte Verbindung einen zahlte.
Nutzen Sie ihn für Lesepfade mit einer oder zwei Abfragen. Nutzen Sie eine gepoolte Verbindung für alles Transaktionale.
3. Weniger Verbindungen, weil weniger Abfragen. Unterschätzt und meist verfügbar. Eine Seite mit einer statt vier Abfragen braucht ein Viertel der Verbindungszeit. In einer Server Component zu laden statt über einen Route Handler, den Ihr Client aufruft, entfernt einen Sprung ganz. Eine Abfrage zu cachen, die sich stündlich ändert, entfernt sie aus dem heißen Pfad.
Die schnellste Verbindung ist die, die niemand öffnet.
Wo die Abfragen liegen, der Next.js-Teil
Der App Router gibt Ihnen mehrere Orte, an denen eine Abfrage laufen kann, und sie verhalten sich bei Verbindungen unterschiedlich.
Server Components laufen während des Renderns, auf dem Server, und können direkt abfragen. Zwei Geschwisterkomponenten, die je laden, heißt zwei Abfragen im selben Aufruf - für einen Pool in Ordnung, für einen HTTP-Treiber schlechter.
Route Handlers sind der konventionelle Fall und verhalten sich wie jeder Endpunkt.
Server Actions laufen je Absendung. Eine schreibende Action will eine echte Transaktion, und das ist der Fall, in dem die Einschränkung des HTTP-Treibers zählt.
Middleware läuft bei jeder passenden Anfrage, vor allem anderen. Von dort abzufragen multipliziert Ihren Verbindungsdruck mit Ihrem Verkehr, einschließlich Anfragen, die aus dem Cache bedient worden wären - und sie ist auch aus anderen Gründen teuer.
Wenn Sie selbst hosten
Das meiste davon verschwindet. Ein langlebiger Node-Prozess heißt ein Modulbereich, ein Pool und eine Verbindungszahl, die Sie setzen und begründen können. Sie sind zurück in dem Modell, das jeder Node-Artikel seit 2015 beschreibt.
Das ist ein echter Punkt für den Eigenbetrieb, und ein kleiner im Vergleich zu dem, was Sie sich damit aufladen. Erwähnen Sie ihn in der Abwägung; wählen Sie kein Deployment-Modell deswegen.
Die Diagnose, der Reihe nach
- Fragen Sie die Datenbank, wie viele Verbindungen bestehen und woher, statt es abzuleiten.
- Prüfen Sie, dass der Client einmal je Modul erzeugt wird, nicht je Anfrage.
- Prüfen Sie, dass Sie sich mit dem Pooler verbinden - und Migrationen nicht.
- Prüfen Sie, ob vorbereitete Statements dort abgeschaltet sind, wo Transaction Pooling es verlangt.
- Erst dann das Limit erhöhen - und schreiben Sie auf, worauf die neue Zahl beruht, denn wer als Nächstes eine Route hinzufügt, muss wissen, dass es ein Budget gibt.
