Przejdź do treści
PS

4 lipca 2026 ·

Drizzle ORM + Neon na Vercel: konfiguracja serverless, która nie przecieka połączeniami


Kiedy pierwszy raz wystawiłem na Vercel aplikację Next.js opartą o Postgres i trafiła na nią realna ilość ruchu, położyła się z błędem sorry, too many clients already. Baza danych praktycznie nic nie robiła — problem polegał na tym, że serverless i tradycyjne połączenia do Postgresa do siebie nie pasują, a nikt cię o tym nie ostrzega, dopóki nie zrobi tego produkcja.

Oto warstwa bazodanowa, którą teraz wdrażam w każdym projekcie na God-Stacku: Drizzle ORM + Neon na Vercel, skonfigurowana tak, żeby nie wyciekały połączenia. Poniżej całość — dlaczego się psuje, po który sterownik Neona sięgnąć i te kilka decyzji, które sprawiają, że działa niezawodnie.

Dlaczego serverless wyczerpuje Postgresa

Klasyczny serwer Postgres ma sztywny limit max_connections — na małej instancji często 100. To wystarcza dla jednego, długo żyjącego procesu Node, który trzyma pool, powiedzmy, 10 połączeń.

Serverless łamie to założenie. Każde wywołanie funkcji może być osobną, krótko żyjącą instancją, a jeśli każde wywołanie otwiera własne połączenie do bazy, skok ruchu otwiera ich naraz setki. Uderzasz w max_connections, nowe żądania nie mogą dostać połączenia i aplikacja rzuca wyjątkiem. Baza nie jest przeciążona — przeciążona jest liczba połączeń.

Są z tego dwa wyjścia, a dobra konfiguracja korzysta z obu:

  1. Postaw connection pooler przed Postgresem, żeby tysiące klientów współdzieliło niewielki pool realnych połączeń.
  2. Użyj sterownika, który nie trzyma trwałego połączenia na ścieżce request/response.

Neon daje ci to pierwsze za darmo, a jego serverless driver daje to drugie.

Dwa sterowniki Neona — i kiedy używać którego

Pakiet @neondatabase/serverless od Neona dostarcza dwa sposoby łączenia się, a Drizzle ma adapter do każdego z nich. Wybór właściwego to 90% sukcesu.

Sterownik HTTP (drizzle-orm/neon-http) — każde zapytanie to bezstanowe żądanie HTTPS. Nie ma połączenia do trzymania, nie ma czego wyciekać. To domyślny wybór, po który sięgam w Next.js: przytłaczająca większość route handlerów i Server Components wykonuje jedno czy dwa zapytania i zwraca wynik.

// 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 });

Sterownik WebSocket Pool (drizzle-orm/neon-serverless) — prawdziwa sesja po WebSockecie, której potrzebujesz do transakcji interaktywnych: wielu instrukcji, które muszą wykonać się na tym samym połączeniu, z logiką aplikacji pomiędzy nimi (db.transaction(async (tx) => { ... })). Sterownik HTTP tego nie potrafi.

// db-pool.ts — tylko tam, gdzie potrzebujesz transakcji interaktywnych
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 });

Zasada, którą się kieruję: HTTP domyślnie, Pool tylko tam, gdzie naprawdę potrzebujesz transakcji. Większość aplikacji wdraża pojedynczy db.ts na sterowniku HTTP i nigdy nawet nie dotyka tego z Poolem.

Pooled connection string nie jest opcjonalny

Neon daje ci dwa connection stringi, a różnica między nimi to sedno całej sprawy:

  • Pooled — host wygląda jak ...-pooler.region.aws.neon.tech. Prowadzi przez PgBouncera Neona, więc wielu klientów współdzieli garstkę realnych połączeń do Postgresa. To ten, którego twoja aplikacja używa w runtime.
  • Direct — bez -pooler. Surowe połączenie do Postgresa. Ten chcesz mieć do migracji (więcej o tym poniżej).

Ustaw DATABASE_URL na string pooled. Jeśli skierujesz działającą aplikację na ten direct, wyrzuciłeś pooler do kosza i wracasz do wyczerpywania połączeń pod obciążeniem.

Zdefiniuj schemat

Nic egzotycznego — zwykły plik schematu Drizzle. To także to, co daje ci pełne, end-to-end bezpieczeństwo typów: wywnioskowane typy przepływają wprost do twoich zapytań.

// 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(),
});

Migracje — i dlaczego korzystają z URL-a direct

Drizzle Kit generuje wersjonowany SQL na podstawie twojego schematu i go aplikuje. Konfiguracja:

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  dialect: "postgresql",
  schema: "./schema.ts",
  out: "./drizzle",
  dbCredentials: {
    // URL DIRECT (unpooled) — nie host z -pooler
    url: process.env.DATABASE_URL_UNPOOLED!,
  },
});
npx drizzle-kit generate   # diff schematu -> pliki migracji SQL
npx drizzle-kit migrate    # zastosuj je

Migracje działają na połączeniu direct, ponieważ PgBouncer w trybie transaction-pooling nie obsługuje operacji na poziomie sesji, których wymaga część DDL. Trzymaj więc dwie zmienne środowiskowe: DATABASE_URL (pooled, dla aplikacji) i DATABASE_URL_UNPOOLED (direct, dla drizzle-kit). Neon pokazuje ci obie w panelu.

Fragment, który Fluid Compute po cichu naprawia

Jest jeszcze drugi, subtelniejszy wyciek: tworzenie klienta wewnątrz handlera żądania. Każde żądanie buduje nowego klienta, a przy sterowniku Pool oznacza to za każdym razem nowy pool.

Stwórz instancję klienta raz, na poziomie modułu — tak jak w db.ts powyżej — i importuj ten singleton wszędzie. Kod na poziomie modułu wykonuje się raz na instancję, a nie raz na żądanie.

To właśnie tutaj Fluid Compute od Vercela pomaga, a nie przeszkadza. Fluid ponownie wykorzystuje ciepłą instancję funkcji dla wielu wywołań, zamiast starego modelu jedno-żądanie-na-instancję, więc klient na poziomie modułu (a dla sterownika Pool — jego pool) tworzony jest raz i wykorzystywany ponownie we wszystkich żądaniach, które obsługuje ta instancja. Mniej zimnych startów, mniej nowych połączeń. Stara serverless'owa rada, by "otwierać i zamykać połączenie na każde żądanie", jest tu dokładnie odwrotna od słusznej — chcesz przeciwieństwa.

Nadal zostajesz przy pooled connection stringu. Fluid ogranicza, jak często tworzysz klientów; pooler Neona ogarnia rozłożenie ruchu, gdy przychodzi jego skok i kilka instancji jest ciepłych naraz. Podwójne zabezpieczenie.

Podpięcie env na Vercel

Ustaw obie zmienne w projekcie na Vercel (albo pobierz je lokalnie):

vercel env pull .env.local

.env.local do lokalnego developmentu, panel Vercela (lub vercel env add) do preview i produkcji. Integracja Neona z Vercelem może wstrzyknąć je za ciebie, a nawet dać każdemu preview deployment własną gałąź bazy danych — ale to temat na osobny wpis.

Cała decyzja, na jednej stronie

  • Domyślny sterownik: neon-http. Bezstanowy, nie ma czego wyciekać.
  • Tylko do transakcji interaktywnych: Pool z neon-serverless.
  • Connection string aplikacji: URL pooled (-pooler), w DATABASE_URL.
  • Migracje: URL direct, przez drizzle-kit i osobną zmienną środowiskową.
  • Czas życia klienta: budowany raz, na poziomie modułu, importowany wszędzie — nigdy wewnątrz handlera.

Zrób te pięć rzeczy, a liczba połączeń do Postgresa przestaje być czymś, o czym w ogóle myślisz, nawet gdy kampania wysyła skok ruchu o 9 rano. To nie jest żadna sztuczka — to po prostu dopasowanie sterownika i connection stringu do tego, jak serverless naprawdę uruchamia twój kod.