fix(webapp): honor the configured database connect timeout (#4513)

## Summary

Every Prisma client built its connection URL with a `connection_timeout`
query param, but the Postgres connector's parameter is
`connect_timeout`. The misspelled param is silently ignored, so all
clients fell back to Prisma's 5s default instead of the configured
timeout. When establishing a new connection briefly took longer than 5s
(for example during connection spikes), it failed with `Can't reach
database server` even though the database was healthy.

## Fix

All four client builders now construct their connection URL through one
shared helper (`buildPrismaConnectionUrl`) that sets `connect_timeout`,
so the configured value actually applies, and the parameter name lives
in exactly one place. Covered by a unit test.
This commit is contained in:
Eric Allam
2026-08-05 15:52:12 +01:00
committed by GitHub
parent 85f5b37c68
commit 3039bc14d6
4 changed files with 75 additions and 34 deletions
@@ -0,0 +1,6 @@
---
area: webapp
type: fix
---
Honor the configured database connection timeout so brief connection spikes no longer surface as spurious "can't reach database server" errors.
+21 -34
View File
@@ -14,6 +14,7 @@ import { z } from "zod";
import { env } from "./env.server";
import { logger } from "./services/logger.server";
import { isValidDatabaseUrl } from "./utils/db";
import { buildPrismaConnectionUrl } from "./utils/prismaConnectionUrl";
import {
captureInfrastructureErrors,
infraErrorAlreadyLogged,
@@ -395,11 +396,11 @@ export function buildWriterClient({
url: string;
clientType: string;
}): PrismaClient {
const databaseUrl = extendQueryParams(url, {
connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(),
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
application_name: env.SERVICE_NAME,
const databaseUrl = buildPrismaConnectionUrl(url, {
connectionLimit: env.DATABASE_CONNECTION_LIMIT.toString(),
poolTimeout: env.DATABASE_POOL_TIMEOUT.toString(),
connectTimeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
applicationName: env.SERVICE_NAME,
});
console.log(`🔌 setting up prisma client to ${redactUrlSecrets(databaseUrl)}`);
@@ -542,11 +543,11 @@ export function buildReplicaClient({
url: string;
clientType: string;
}): PrismaClient {
const replicaUrl = extendQueryParams(url, {
connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(),
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
application_name: env.SERVICE_NAME,
const replicaUrl = buildPrismaConnectionUrl(url, {
connectionLimit: env.DATABASE_CONNECTION_LIMIT.toString(),
poolTimeout: env.DATABASE_POOL_TIMEOUT.toString(),
connectTimeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
applicationName: env.SERVICE_NAME,
});
console.log(`🔌 setting up read replica connection to ${redactUrlSecrets(replicaUrl)}`);
@@ -672,11 +673,11 @@ function buildRunOpsWriterClient({
url: string;
clientType: string;
}): RunOpsPrismaClient {
const databaseUrl = extendQueryParams(url, {
connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(),
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
application_name: env.SERVICE_NAME,
const databaseUrl = buildPrismaConnectionUrl(url, {
connectionLimit: env.DATABASE_CONNECTION_LIMIT.toString(),
poolTimeout: env.DATABASE_POOL_TIMEOUT.toString(),
connectTimeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
applicationName: env.SERVICE_NAME,
});
console.log(`🔌 setting up run-ops prisma client to ${redactUrlSecrets(databaseUrl)}`);
@@ -723,14 +724,13 @@ function buildRunOpsReplicaClient({
url: string;
clientType: string;
}): RunOpsPrismaClient {
const replicaUrl = extendQueryParams(url, {
// The new run-ops replica connects unpooled, so allow capping it independently of the writer.
connection_limit: (
const replicaUrl = buildPrismaConnectionUrl(url, {
connectionLimit: (
env.RUN_OPS_DATABASE_READ_REPLICA_CONNECTION_LIMIT ?? env.DATABASE_CONNECTION_LIMIT
).toString(),
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
application_name: env.SERVICE_NAME,
poolTimeout: env.DATABASE_POOL_TIMEOUT.toString(),
connectTimeout: env.DATABASE_CONNECTION_TIMEOUT.toString(),
applicationName: env.SERVICE_NAME,
});
console.log(`🔌 setting up run-ops read replica connection to ${redactUrlSecrets(replicaUrl)}`);
@@ -789,19 +789,6 @@ export function sameDatabaseTarget(a: string | undefined, b: string | undefined)
}
}
function extendQueryParams(hrefOrUrl: string | URL, queryParams: Record<string, string>) {
const url = new URL(hrefOrUrl);
const query = url.searchParams;
for (const [key, val] of Object.entries(queryParams)) {
query.set(key, val);
}
url.search = query.toString();
return url;
}
function redactUrlSecrets(hrefOrUrl: string | URL) {
const url = new URL(hrefOrUrl);
url.password = "";
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { buildPrismaConnectionUrl } from "./prismaConnectionUrl";
describe("buildPrismaConnectionUrl", () => {
it("sets connect_timeout (the Postgres connector parameter), not the ignored connection_timeout", () => {
const url = buildPrismaConnectionUrl("postgresql://u:p@host:5432/db?schema=public", {
connectionLimit: "10",
poolTimeout: "0",
connectTimeout: "20",
applicationName: "svc",
});
expect(url.searchParams.get("connect_timeout")).toBe("20");
expect(url.searchParams.has("connection_timeout")).toBe(false);
expect(url.searchParams.get("connection_limit")).toBe("10");
expect(url.searchParams.get("pool_timeout")).toBe("0");
expect(url.searchParams.get("application_name")).toBe("svc");
});
it("preserves existing base query params", () => {
const url = buildPrismaConnectionUrl(
"postgresql://u:p@host:5432/db?schema=public&sslmode=require",
{ connectionLimit: "5", poolTimeout: "10", connectTimeout: "20", applicationName: "svc" }
);
expect(url.searchParams.get("schema")).toBe("public");
expect(url.searchParams.get("sslmode")).toBe("require");
expect(url.searchParams.get("connect_timeout")).toBe("20");
});
});
@@ -0,0 +1,18 @@
export type PrismaConnectionParams = {
connectionLimit: string;
poolTimeout: string;
connectTimeout: string;
applicationName: string;
};
export function buildPrismaConnectionUrl(
baseUrl: string | URL,
params: PrismaConnectionParams
): URL {
const url = new URL(baseUrl);
url.searchParams.set("connection_limit", params.connectionLimit);
url.searchParams.set("pool_timeout", params.poolTimeout);
url.searchParams.set("connect_timeout", params.connectTimeout);
url.searchParams.set("application_name", params.applicationName);
return url;
}