Μία εβδομάδα μετά την κυκλοφορία του klawsfx.com, η παραγωγή άρχισε να εμφανίζει σφάλματα P1001: Can't reach database server. Όχι σε κάθε αίτηση — διαλείποντα, συνήθως υπό ελαφρά κυκλοφορία, κάτι που δυσκόλεψε τη διάγνωση. Ο πίνακας ελέγχου της Neon έδειχνε ότι η βάση δεδομένων λειτουργούσε καλά. Τα αρχεία καταγραφής συναρτήσεων του Vercel έδειχναν το σφάλμα να συμβαίνει κατά το prisma.$connect().
Η αιτία είναι ένας συγκεκριμένος συνδυασμός του serverless runtime του Vercel, του serverless Postgres της Neon, και του τρόπου με τον οποίο η Prisma διαχειρίζεται τις συνδέσεις. Εδώ είναι τι συμβαίνει και πώς το διόρθωσα.
Γιατί αυτός ο συνδυασμός είναι επικίνδυνος
Το Vercel αναπτύσσει το Next.js ως serverless συναρτήσεις. Κάθε αίτηση μπορεί να καταλήξει σε διαφορετική instance συνάρτησης. Κάθε instance εκκινεί μια νέα διεργασία Node.js, και η προεπιλεγμένη συμπεριφορά της Prisma είναι να ανοίγει μια δεξαμενή συνδέσεων όταν δημιουργείται ο PrismaClient.
Η Neon είναι serverless Postgres. Κάθε κλάδος βάσης δεδομένων Neon έχει όριο συνδέσεων. Το δωρεάν επίπεδο περιορίζει σε 100 συνδέσεις συνολικά· τα πληρωμένα επίπεδα είναι υψηλότερα αλλά εξακολουθούν να έχουν πεπερασμένο αριθμό.
Το πρόβλημα: αν 20 instances συναρτήσεων Vercel εκκινήσουν ταυτόχρονα (μια μέτρια έκρηξη κυκλοφορίας), και κάθε μία ανοίγει μια δεξαμενή 10 συνδέσεων από προεπιλογή, αυτό είναι 200 συνδέσεις — ήδη πάνω από το όριο του δωρεάν επιπέδου. Οι συνδέσεις δεν αποδεσμεύονται γρήγορα επειδή οι serverless συναρτήσεις παραμένουν ζεστές για λίγα λεπτά μετά τον χειρισμό μιας αίτησης.
Η λάθος λύση
Το πρώτο πράγμα που δοκιμάζουν οι περισσότεροι προγραμματιστές είναι να ορίσουν connection_limit=1 στο URL βάσης δεδομένων:
postgresql://user:pass@host/db?connection_limit=1
Αυτό λειτουργεί — περιορίζει κάθε instance σε μία σύνδεση. Αλλά επίσης σειριοποιεί όλα τα ερωτήματα μέσα σε μία instance συνάρτησης, κάτι που αυξάνει αισθητά τον χρόνο απόκρισης για σελίδες που εκτελούν πολλά παράλληλα ερωτήματα Prisma.
Η σωστή λύση: PgBouncer μέσω του pooler συνδέσεων της Neon
Η Neon παρέχει έναν ενσωματωμένο pooler συνδέσεων (PgBouncer σε transaction mode) σε ξεχωριστό hostname. Χρησιμοποιείς το pooled connection string για την εφαρμογή σου και το direct connection string μόνο για migrations.
Στον πίνακα ελέγχου της Neon, στις ρυθμίσεις του κλάδου σου, θα βρεις δύο connection strings:
- Direct:
postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/dbname - Pooled:
postgresql://user:pass@ep-xxx-pooler.us-east-2.aws.neon.tech/dbname?pgbouncer=true
Το pooled string δρομολογείται μέσω PgBouncer. Ο PgBouncer πολυπλέκει πολλές βραχύβιες συνδέσεις πελατών σε ένα μικρό σύνολο μακροχρόνιων συνδέσεων διακομιστή.
Ορίσε δύο μεταβλητές περιβάλλοντος:
DATABASE_URL=postgresql://user:pass@ep-xxx-pooler.us-east-2.aws.neon.tech/dbname?pgbouncer=true&connection_limit=1
DIRECT_URL=postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/dbname
Στη συνέχεια ενημέρωσε το schema.prisma για να χρησιμοποιεί και τα δύο:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
Το url χρησιμοποιείται για όλα τα ερωτήματα. Το directUrl χρησιμοποιείται μόνο από το prisma migrate — τα migrations απαιτούν άμεση σύνδεση επειδή εκτελούν DDL statements που ο transaction mode του PgBouncer δεν υποστηρίζει.
Singleton PrismaClient στο Next.js
Ακόμα και με PgBouncer, θέλεις να αποφύγεις τη δημιουργία νέου PrismaClient σε κάθε αίτηση. Κατά την ανάπτυξη, το hot-reloading του Next.js καταστρέφει και αναδημιουργεί modules, κάτι που δημιουργεί πολλές instances client και εξαντλεί γρήγορα το όριο συνδέσεων ανάπτυξης.
Η τυπική λύση είναι ένα singleton module:
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
Το globalThis παραμένει κατά τα hot-reloads κατά την ανάπτυξη. Στην παραγωγή, κάθε serverless instance είναι ξεχωριστή διεργασία — αλλά αυτό είναι εντάξει, επειδή ο PgBouncer χειρίζεται την πολυπλεξία.
Μεταβλητές περιβάλλοντος Vercel για κλάδους preview
Μια ακόμα παγίδα: τα preview deployments και τα production deployments πρέπει να χρησιμοποιούν το ίδιο pooled URL, αλλά αν έχεις επίσης έναν κλάδο Neon preview για staging, βεβαιώσου ότι το DIRECT_URL σε αυτόν τον κλάδο δείχνει στο preview Neon endpoint, όχι στην παραγωγή.
Μετά τη λύση
Σφάλματα σύνδεσης: μηδέν από την αλλαγή. Ο μέσος χρόνος ερωτήματος μειώθηκε ελαφρώς επειδή ο PgBouncer βρίσκεται σε γειτνίαση με τον υπολογισμό Neon. Ο συνολικός αριθμός συνδέσεων που φαίνεται στα μετρικά της Neon πήγε από αιχμές πάνω από 80 σε παραμονή κάτω από 10, ανεξάρτητα από την κυκλοφορία.
