fix(db): share one Prisma client across the server bundles

A production build loads src/lib/db.ts once per server bundle, and the
client was only parked on globalThis outside production. The running
process held three clients: about 28 MB of query-compiler state each on
a heap capped near 520 MB, and three pg pools each sized to the full
per-process connection budget. Park the client on globalThis in every
environment so each copy of the module resolves the same one.
This commit is contained in:
Marc-André Bombeck committed 2026-10-04 15:29:01 +02:00
1 parent 4cad43e4d5
commit 5cbb080262
2 files changed
+39 -5

No files matched your search

+30 -1
View File
@@ -2,7 +2,7 @@
* Pool configuration stays unit-tested here so connection ceilings and wait
* timeouts cannot silently fall back to separate library defaults.
*/
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
buildSessionOptions,
@@ -129,3 +129,32 @@ describe("buildSessionOptions", () => {
expect(buildSessionOptions()).toBeUndefined();
});
});
describe("process-wide Prisma client", () => {
const globalSlot = globalThis as unknown as { prisma?: unknown };
afterEach(() => {
vi.unstubAllEnvs();
delete globalSlot.prisma;
vi.resetModules();
});
it("shares one client across separate copies of the module in production", async () => {
// A production build loads this module once per server bundle; each copy
// must resolve the same client rather than constructing its own.
vi.stubEnv("NODE_ENV", "production");
vi.stubEnv(
"DATABASE_URL",
"postgresql://healthlog:test@db:5432/healthlog?connection_limit=20",
);
delete globalSlot.prisma;
vi.resetModules();
const first = (await import("../db")).prisma;
vi.resetModules();
const second = (await import("../db")).prisma;
expect(second).toBe(first);
expect(globalSlot.prisma).toBe(first);
});
});
+9 -4
View File
@@ -106,11 +106,16 @@ function createPrismaClient() {
return new PrismaClient({ adapter });
}
// One client per process, in production too. A production build gives every
// server bundle its own copy of this module (the route handlers, the RSC/SSR
// renderer, and the instrumentation bundle that boots the workers), so a
// module-scoped client alone meant three of them: three query-compiler states
// of roughly 28 MB each on a heap that V8 caps near 520 MB in a 1 GiB
// container, and three pg pools each sized to the whole per-process
// connection budget above. Parking the client on `globalThis` makes every
// copy of the module resolve the same one.
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
globalForPrisma.prisma = prisma;
// Prisma's `InputJsonValue` requires an explicit index signature that
// typed application shapes (Zod-validated, hand-written interfaces)