Skip to content

HARDEN: Postgres pool config + dependency-aware /ready#161

Open
erikolsson wants to merge 1 commit into
mainfrom
harden/02-postgres-pool-and-ready
Open

HARDEN: Postgres pool config + dependency-aware /ready#161
erikolsson wants to merge 1 commit into
mainfrom
harden/02-postgres-pool-and-ready

Conversation

@erikolsson

@erikolsson erikolsson commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Two production-readiness gaps closed:

1. Postgres pool was running on defaults. postgres() was instantiated with no options, so postgres.js's defaults of max=10, idle_timeout=0 (never close idle connections), and no statement_timeout applied. Under sustained load idle connections accumulated until the database refused new ones, and a runaway query could pin a pool slot for hours. This PR sets explicit max / idle_timeout / connect_timeout / max_lifetime, and binds statement_timeout on every new connection so PostgreSQL itself kills runaway queries. All five values are env-tunable (DB_POOL_MAX, DB_IDLE_TIMEOUT_SEC, DB_CONNECT_TIMEOUT_SEC, DB_MAX_LIFETIME_SEC, DB_STATEMENT_TIMEOUT_MS) with conservative defaults.

2. /health pinged neither dependency. An orchestrator (Render, k8s) would route traffic to a replica whose pool was saturated or whose Redis client had silently disconnected, because /health only checks that the HTTP listener responds. Now split:

  • /health stays cheap (liveness — "process is up enough to answer HTTP"). Orchestrators should use this only for restart decisions.
  • New /ready pings Postgres + Redis in parallel with a 2 s ceiling per probe, returning 503 on either failure. Orchestrators should gate routing on this.

Both pingDatabase (apps/server/src/db/index.ts) and RedisBridge.ping clear their timeout timers on the fast path so a healthy probe doesn't leave a 2 s timer pinned to the event loop.

No new external dependencies. Pure hardening.

Test plan

  • bun run typecheck (apps/server) — clean
  • bun run test (apps/server) — 222 pass, 0 fail (was 218; +4 from new health.test.ts)
  • Manual: curl /health → 200 {status: "ok"} regardless of dep state.
  • Manual: curl /ready against healthy stack → 200 {status: "ready", database: true, redis: true}.
  • Manual: SIGSTOP the Postgres container, curl /ready → 503 {status: "degraded", database: false, redis: true} within ~2 s.
  • Manual: tune DB_STATEMENT_TIMEOUT_MS=100, run a slow query through any handler, confirm Postgres returns canceling statement due to statement timeout instead of pinning the pool.

🤖 Generated with Claude Code


Note

Medium Risk
Changes database connection pool and query timeout behavior and adds a new readiness gate, which could affect production traffic routing or cause long-running queries to fail if tuned too aggressively.

Overview
Adds a new readiness endpoint (GET /ready) that pings Postgres and Redis with short timeouts and returns 503 when either dependency is unhealthy, while keeping /health as a cheap liveness check.

Hardens Postgres usage by making pool sizing/timeouts and statement_timeout configurable via new env vars, and introduces bounded pingDatabase/RedisBridge.ping helpers plus tests covering /health, /ready, and the ping functions.

Reviewed by Cursor Bugbot for commit 0ea0ee9. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a readiness probe endpoint that verifies database and Redis connectivity, returning appropriate health status
    • Introduced configurable database connection pool and timeout parameters for improved stability and performance
  • Tests

    • Added comprehensive health check test suite validating readiness and liveness endpoints

Two production-readiness gaps:

1. The Postgres client is instantiated without options — postgres.js then
   defaults to max=10, idle_timeout=0 (never close idle connections), no
   statement_timeout. Under sustained load idle connections accumulate
   until the database refuses new ones, and a runaway query can pin a
   pool slot for hours.

   Set explicit max/idle_timeout/connect_timeout/max_lifetime, and bind
   `statement_timeout` on every new connection so the database itself
   kills queries past the wall-clock budget. All five values are env-
   tunable with conservative defaults.

2. /health pinged neither dependency, so an orchestrator (Render, k8s)
   would route traffic to a replica whose pool was saturated or whose
   Redis client had silently disconnected.

   Split: /health stays cheap (liveness — process answering HTTP), and a
   new /ready pings Postgres + Redis in parallel with a 2 s ceiling per
   probe, returning 503 on either failure. Orchestrators should route
   on /ready and reserve /health for restart decisions.

Tests cover the happy path on both endpoints plus the underlying ping
helpers against the live test database and Redis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 4a2ed5c3-d404-49bc-a5c8-e986b314bc68

📥 Commits

Reviewing files that changed from the base of the PR and between 868e25a and 0ea0ee9.

📒 Files selected for processing (5)
  • apps/server/src/app.ts
  • apps/server/src/config.ts
  • apps/server/src/db/index.ts
  • apps/server/src/ws/redis-bridge.ts
  • apps/server/test/health.test.ts

Walkthrough

This pull request introduces a readiness check system for the server. New configuration parameters control database pool sizing and connection timeouts (dbPoolMax, dbIdleTimeoutSec, dbConnectTimeoutSec, dbMaxLifetimeSec, dbStatementTimeoutMs). A new GET /ready endpoint concurrently checks database and Redis availability using newly exported pingDatabase and RedisBridge.ping methods, both employing timeout-bounded liveness checks. The existing GET /health endpoint remains unchanged. Tests verify both endpoints and the underlying health check functions against live database and Redis instances.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant