You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
GET /api/health currently returns a hardcoded { "status": "ok" } regardless of whether Sourcebot's actual dependencies (Postgres, Redis, Zoekt) are reachable. Self-hosted operators using Kubernetes, Docker Compose with a healthcheck, an external load balancer, or a monitoring system cannot distinguish a healthy Sourcebot from one whose database connection has dropped, whose Redis is unreachable, or whose Zoekt indexer is dead.
The previous FR for this (#727) was closed because the endpoint already existed, but the original ask was for a "structured response with details on its inner components' health status" — which was never delivered. PR #728 was opened to document the existing endpoint and closed without merging.
This is a 12-line stub in packages/web/src/app/api/(server)/health/route.ts. It runs on every orchestrator probe interval, logs a debug line, and returns the same payload whether or not Postgres, Redis, or Zoekt are reachable. A pod that has lost its database connection will still report "ok" to a Kubernetes readiness probe, and traffic will continue to be routed to a broken instance.
Expected behavior
A new GET /api/health/ready endpoint that:
Checks Postgres via __unsafePrisma.$queryRaw\SELECT 1``
Checks Redis via redis.ping()
Checks Zoekt via a lightweight gRPC call (a List request with q: { and: { children: [] } } and a small opts: { max_wall_time: 1s })
Runs the three checks in parallel with a per-check timeout (default 2s)
Returns 200 with {"status":"ok","checks":{...}} if all pass
Returns 503 with {"status":"degraded","checks":{...}} if any fail; per-check payload includes {status, latencyMs, error?}
The existing GET /api/health stays exactly as it is (liveness probe) — a process-up signal that does not depend on any external service.
The two endpoints give operators the standard Kubernetes liveness/readiness pair out of the box.
import{apiHandler}from"@/lib/apiHandler";import{__unsafePrisma}from"@/prisma";import{getRedisClient}from"@/lib/redis";import{createLogger}from"@sourcebot/shared";import*asgrpcfrom"@grpc/grpc-js";import*asprotoLoaderfrom"@grpc/proto-loader";importpathfrom"path";import{env}from"@sourcebot/shared";constREADINESS_TIMEOUT_MS=2000;constlogger=createLogger("health-ready");typeCheckResult={status: "ok"|"error";latencyMs: number;error?: string};typeReadinessResponse={status: "ok"|"degraded";checks: Record<string,CheckResult>};// Each check returns a CheckResult after at most READINESS_TIMEOUT_MS.asyncfunctioncheckPostgres(): Promise<CheckResult>{ ... }asyncfunctioncheckRedis(): Promise<CheckResult>{ ... }asyncfunctioncheckZoekt(): Promise<CheckResult>{ ... }exportconstGET=apiHandler(async()=>{const[postgres,redis,zoekt]=awaitPromise.all([checkPostgres(),checkRedis(),checkZoekt(),]);constchecks={ postgres, redis, zoekt };consthealthy=Object.values(checks).every(c=>c.status==="ok");constbody: ReadinessResponse={status: healthy ? "ok" : "degraded",
checks,};returnResponse.json(body,{status: healthy ? 200 : 503});},{track: false});
Tests: packages/web/src/app/api/(server)/health/ready/route.test.ts covering the healthy / degraded paths with mocked prisma + redis + gRPC client.
Docs: a new docs/docs/api-reference/health.mdx describing both endpoints and the liveness-vs-readiness split, with example probes for Docker Compose and Kubernetes. Add it to the API Reference group in docs/docs.json.
Alternatives considered
A single/api/healththat does both. Rejected: makes Kubernetes-style splitting impossible, and a liveness probe that touches the database can cause cascading restarts when the database is briefly unreachable.
Prometheus metrics-only (the spirit ofFR: Add Prometheus support #119**).** Rejected for this PR: not actionable from an orchestrator probe. Could be a follow-up — checkZoekt and the timing data are already in the right shape to emit as metrics.
AWebserverService.HealthgRPC method. Not available in the vendored Zoekt proto; would require an upstream change. A List with a 1s wall-time cap is the smallest thing that proves the gRPC channel is alive and the server is responsive.
Scope
One new route file + one new test file + one new docs file + one line in docs/docs.json
No schema changes
No new dependencies
No changes to the existing /api/health liveness endpoint
No changes to the existing API key, auth, or entitlement systems — this endpoint is unauthenticated and matches the existing /api/health policy (no user data returned)
Acceptance criteria
GET /api/health/ready returns 200 with {"status":"ok",...} when all three dependencies are reachable
GET /api/health/ready returns 503 with {"status":"degraded",...} and the failing check's {status:"error", error:...} when any dependency is unreachable
All three checks are bounded by a 2s timeout; the worst-case request time is ~2s even when one dependency hangs
All three checks run in parallel (Promise.all) — total request time is max(timeouts), not sum(timeouts)
GET /api/health (liveness) is unchanged
Tests pass for both healthy and each-of-three-degraded permutations
Docs page describes both endpoints with example Docker Compose healthcheck: and Kubernetes livenessProbe/readinessProbe blocks
No PostHog tracking on the new endpoint (track: false, matching the existing liveness endpoint)
A CHANGELOG.md entry under [Unreleased] → Added references this PR
Backward compatibility
Fully backwards compatible. The existing /api/health endpoint is unchanged. The new endpoint is purely additive. Operators who do not use it see no change.
Risks
A misconfigured /api/health/ready could cause Kubernetes to pull all pods out of rotation if the probe interval collides with a database blip. Mitigated by the per-check timeout and the standard recommendation of a 3-failure failureThreshold on the readiness probe.
The Zoekt gRPC check calls List with a 1s wall-time cap; under load this could add to Zoekt's request rate. The check runs at most once per probe interval per pod; for the default 10s interval on a 3-pod deployment this is ~0.3 RPS, negligible.
The getRedisClient() lazy singleton means the first readiness probe on a cold pod will create the Redis connection. The 2s timeout is more than enough headroom for the first connect.
Future work (out of scope for this PR)
Prometheus metrics for each check (sourcebot_readiness_check_duration_seconds{check="postgres"}, etc.) — would build on the same per-check helpers introduced here.
A /api/health/ready?strict=true mode that treats Zoekt shards-empty as a degraded state (currently List returns success even with zero repos).
A /api/health/metrics JSON endpoint that returns Prometheus-style text without exposing /metrics directly.
Cache the readiness result for ~1s to absorb thundering-herd probes from multiple orchestrators.
Happy to take this on. Implementation plan, test plan, and docs update all fit in one focused PR.
Problem
GET /api/healthcurrently returns a hardcoded{ "status": "ok" }regardless of whether Sourcebot's actual dependencies (Postgres, Redis, Zoekt) are reachable. Self-hosted operators using Kubernetes, Docker Compose with a healthcheck, an external load balancer, or a monitoring system cannot distinguish a healthy Sourcebot from one whose database connection has dropped, whose Redis is unreachable, or whose Zoekt indexer is dead.The previous FR for this (#727) was closed because the endpoint already existed, but the original ask was for a "structured response with details on its inner components' health status" — which was never delivered. PR #728 was opened to document the existing endpoint and closed without merging.
Current behavior
$ curl -sS https://sourcebot.example.com/api/health {"status":"ok"}This is a 12-line stub in
packages/web/src/app/api/(server)/health/route.ts. It runs on every orchestrator probe interval, logs a debug line, and returns the same payload whether or not Postgres, Redis, or Zoekt are reachable. A pod that has lost its database connection will still report "ok" to a Kubernetes readiness probe, and traffic will continue to be routed to a broken instance.Expected behavior
A new
GET /api/health/readyendpoint that:__unsafePrisma.$queryRaw\SELECT 1``redis.ping()Listrequest withq: { and: { children: [] } }and a smallopts: { max_wall_time: 1s })200with{"status":"ok","checks":{...}}if all pass503with{"status":"degraded","checks":{...}}if any fail; per-check payload includes{status, latencyMs, error?}The existing
GET /api/healthstays exactly as it is (liveness probe) — a process-up signal that does not depend on any external service.The two endpoints give operators the standard Kubernetes liveness/readiness pair out of the box.
Proposed solution
Add
packages/web/src/app/api/(server)/health/ready/route.ts:Tests:
packages/web/src/app/api/(server)/health/ready/route.test.tscovering the healthy / degraded paths with mocked prisma + redis + gRPC client.Docs: a new
docs/docs/api-reference/health.mdxdescribing both endpoints and the liveness-vs-readiness split, with example probes for Docker Compose and Kubernetes. Add it to the API Reference group indocs/docs.json.Alternatives considered
/api/healththat does both. Rejected: makes Kubernetes-style splitting impossible, and a liveness probe that touches the database can cause cascading restarts when the database is briefly unreachable.checkZoektand the timing data are already in the right shape to emit as metrics.WebserverService.HealthgRPC method. Not available in the vendored Zoekt proto; would require an upstream change. AListwith a 1s wall-time cap is the smallest thing that proves the gRPC channel is alive and the server is responsive.Scope
docs/docs.json/api/healthliveness endpoint/api/healthpolicy (no user data returned)Acceptance criteria
GET /api/health/readyreturns200with{"status":"ok",...}when all three dependencies are reachableGET /api/health/readyreturns503with{"status":"degraded",...}and the failing check's{status:"error", error:...}when any dependency is unreachablemax(timeouts), notsum(timeouts)GET /api/health(liveness) is unchangedhealthcheck:and KuberneteslivenessProbe/readinessProbeblockstrack: false, matching the existing liveness endpoint)CHANGELOG.mdentry under[Unreleased] → Addedreferences this PRBackward compatibility
Fully backwards compatible. The existing
/api/healthendpoint is unchanged. The new endpoint is purely additive. Operators who do not use it see no change.Risks
/api/health/readycould cause Kubernetes to pull all pods out of rotation if the probe interval collides with a database blip. Mitigated by the per-check timeout and the standard recommendation of a 3-failurefailureThresholdon the readiness probe.Listwith a 1s wall-time cap; under load this could add to Zoekt's request rate. The check runs at most once per probe interval per pod; for the default 10s interval on a 3-pod deployment this is ~0.3 RPS, negligible.getRedisClient()lazy singleton means the first readiness probe on a cold pod will create the Redis connection. The 2s timeout is more than enough headroom for the first connect.Future work (out of scope for this PR)
sourcebot_readiness_check_duration_seconds{check="postgres"}, etc.) — would build on the same per-check helpers introduced here./api/health/ready?strict=truemode that treats Zoekt shards-empty as a degraded state (currentlyListreturns success even with zero repos)./api/health/metricsJSON endpoint that returns Prometheus-style text without exposing/metricsdirectly.Happy to take this on. Implementation plan, test plan, and docs update all fit in one focused PR.