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 1/2] 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(); } } From f80ed10a8b82eca36b3c150a8a44cd623d4a89d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Mon, 6 Apr 2026 22:40:24 +0200 Subject: [PATCH 2/2] Remove unused journal health route file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api.health.ts was never registered in routes.ts — the server.ts handler intercepts /api/health before React Router sees it. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/journal/app/routes/api.health.ts | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 apps/journal/app/routes/api.health.ts diff --git a/apps/journal/app/routes/api.health.ts b/apps/journal/app/routes/api.health.ts deleted file mode 100644 index 6603a65..0000000 --- a/apps/journal/app/routes/api.health.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { data } from "react-router"; -import { withDb } from "@trails-cool/db"; - -const version = process.env.SENTRY_RELEASE ?? "dev"; - -export async function loader() { - try { - await withDb(async () => {}); - return data({ status: "ok", version, db: "connected" }); - } catch { - return data({ status: "degraded", version, db: "unreachable" }, { status: 503 }); - } -}