From be131450727639f3c02f91e9875aa1568fc13af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Mon, 6 Apr 2026 22:37:34 +0200 Subject: [PATCH] Fix DB connection leak in health check handlers Both server.ts files called createDb() on every /health request, creating a new postgres.js connection pool (~10 connections) that was never closed. Docker healthchecks hit this every 15s, exhausting max_connections within minutes and causing "too many clients" errors. Fix: use a fresh postgres.js client (max: 1) per health check that is properly closed in a finally block. This truly tests whether the DB is accepting new connections without leaking. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/journal/server.ts | 9 +++++---- apps/planner/server.ts | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/journal/server.ts b/apps/journal/server.ts index 3385112..2f696db 100644 --- a/apps/journal/server.ts +++ b/apps/journal/server.ts @@ -4,6 +4,7 @@ import { createReadStream, statSync } from "node:fs"; import { join, extname, resolve } from "node:path"; import { logger } from "./app/lib/logger.server.ts"; import { httpRequestDuration, registry } from "./app/lib/metrics.server.ts"; +import postgres from "postgres"; const port = Number(process.env.PORT ?? 3000); const CLIENT_DIR = resolve(import.meta.dirname, "build", "client"); @@ -56,16 +57,16 @@ async function handleMetrics(_req: IncomingMessage, res: ServerResponse): Promis const version = process.env.SENTRY_RELEASE ?? "dev"; async function handleHealth(_req: IncomingMessage, res: ServerResponse): Promise { + const client = postgres(process.env.DATABASE_URL ?? "postgres://trails:trails@localhost:5432/trails", { max: 1 }); try { - const { createDb } = await import("@trails-cool/db"); - const { sql } = await import("drizzle-orm"); - const db = createDb(); - await db.execute(sql`SELECT 1`); + await client`SELECT 1`; res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ status: "ok", version, db: "connected" })); } catch { res.writeHead(503, { "Content-Type": "application/json" }); res.end(JSON.stringify({ status: "degraded", version, db: "unreachable" })); + } finally { + await client.end(); } } diff --git a/apps/planner/server.ts b/apps/planner/server.ts index cc984c8..457d8da 100644 --- a/apps/planner/server.ts +++ b/apps/planner/server.ts @@ -6,6 +6,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht import { createReadStream, statSync } from "node:fs"; import { join, extname, resolve } from "node:path"; import { setupYjsWebSocket } from "./app/lib/yjs-server.ts"; +import postgres from "postgres"; const sentryEnvironment = process.env.CI ? "ci" : (process.env.NODE_ENV ?? "development"); @@ -74,16 +75,16 @@ async function handleMetrics(_req: IncomingMessage, res: ServerResponse): Promis const version = process.env.SENTRY_RELEASE ?? "dev"; async function handleHealth(_req: IncomingMessage, res: ServerResponse): Promise { + const client = postgres(process.env.DATABASE_URL ?? "postgres://trails:trails@localhost:5432/trails", { max: 1 }); try { - const { createDb } = await import("@trails-cool/db"); - const { sql } = await import("drizzle-orm"); - const db = createDb(); - await db.execute(sql`SELECT 1`); + await client`SELECT 1`; res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ status: "ok", version, db: "connected" })); } catch { res.writeHead(503, { "Content-Type": "application/json" }); res.end(JSON.stringify({ status: "degraded", version, db: "unreachable" })); + } finally { + await client.end(); } }