Skip to content

feat(web): add /api/health/ready endpoint with dependency health checks#1507

Open
Harsh23Kashyap wants to merge 5 commits into
sourcebot-dev:mainfrom
Harsh23Kashyap:feat/health-readiness
Open

feat(web): add /api/health/ready endpoint with dependency health checks#1507
Harsh23Kashyap wants to merge 5 commits into
sourcebot-dev:mainfrom
Harsh23Kashyap:feat/health-readiness

Conversation

@Harsh23Kashyap

@Harsh23Kashyap Harsh23Kashyap commented Jul 24, 2026

Copy link
Copy Markdown

Summary

Adds GET /api/health/ready, a Kubernetes-style readiness probe that returns per-dependency health (Postgres, Redis, Zoekt). The existing GET /api/health liveness endpoint is unchanged.

Self-hosted operators can now wire Sourcebot into a Kubernetes readinessProbe or a load balancer health check and have the probe reflect whether the instance can actually serve traffic.

Motivation

GET /api/health currently returns a hardcoded { "status": "ok" } regardless of whether Postgres, Redis, or Zoekt are reachable. A pod that has lost its database connection still reports "ok" to a Kubernetes readiness probe, so traffic keeps being routed to a broken instance.

The previous FR for this (#727) was closed because the endpoint existed, but the original ask was for "structured response with details on its inner components' health status" — which was never delivered.

Closes #1506.

Changes

  • packages/web/src/app/api/(server)/health/ready/route.ts — new endpoint. Three checks run in parallel via Promise.all; each is bounded by a 2s timeout, so the worst-case request time is ~2s even when one dependency hangs. Returns 200 with {status:ok, checks:{...}} on success, 503 with {status:degraded, checks:{...}} on any failure. Per-check payload includes status, latencyMs, and (on error) error.
  • packages/web/src/lib/zoektClient.ts — extracted the lazy Zoekt gRPC client construction into its own module so the readiness route can mock it in tests without pulling the @grpc/grpc-js + @opentelemetry CJS chain at test-load time. The dynamic-import + cached-promise pattern also avoids the boot-time cost of loading gRPC on the happy path where only some requests actually need it.
  • packages/web/src/app/api/(server)/health/ready/route.test.ts — six test cases covering the healthy path, each of the three degraded paths, the non-PONG response path, and the parallel-execution invariant.
  • docs/docs/api-reference/health.mdx — new docs page describing both endpoints, the response shape, and example Docker Compose healthcheck: and Kubernetes livenessProbe / readinessProbe blocks. Wired into the existing System group in docs/docs.json.
  • CHANGELOG.md — entry under [Unreleased] → Added.

Design decisions

  • Liveness vs readiness split, not a combined endpoint. A liveness probe that touches the database can cause cascading pod restarts when the database is briefly unreachable. The split lets Kubernetes restart on liveness failure (process hung) but keep the pod in rotation only when the dependencies are healthy.
  • Three checks in parallel, not serially. Promise.all keeps worst-case latency at max(timeouts), not sum(timeouts). A test asserts this explicitly.
  • Empty List with a 1s wall-time cap for Zoekt. This is the smallest gRPC request that proves the channel is alive end-to-end. A dedicated WebserverService.Health method would be cleaner but doesn't exist in the vendored Zoekt proto.
  • Empty List returns success even with zero repos indexed. This is the right behavior for a liveness/readiness probe (the question is "can we talk to Zoekt?", not "are there repos to search?"); a strict=true query param for "shards must be non-empty" is noted in the issue's future-work section.
  • No auth, no PostHog tracking. Matches the existing /api/health policy and the documented contract that orchestrator probes should be free to hit the endpoint at any frequency.

Verification

  • yarn workspace @sourcebot/web test --run "health/ready" — 6/6 tests pass
  • yarn workspace @sourcebot/web lint — 0 errors (4 pre-existing warnings in unrelated files)
  • tsc --noEmit -p packages/web/tsconfig.json — 0 errors in the new files (pre-existing errors in auth.ts and a few EE files are documented in HANDOFF.md §21, out of scope)
  • Manual sanity check: the response shape is what Kubernetes-style health probes and load-balancer configs expect

Backward compatibility

Fully backwards compatible. The existing /api/health endpoint is unchanged. The new endpoint is purely additive. Operators who do not configure it see no change.

Risks

  • A misconfigured probe could cause Kubernetes to pull all pods out of rotation if the probe interval collides with a database blip. The docs recommend a 3-failure failureThreshold; the per-check 2s timeout absorbs brief blips.
  • The Zoekt 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 (~0.3 RPS for the default 10s interval on a 3-pod deployment), which is negligible.

Out of scope (tracked in #1506 as future work)

  • 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 an empty Zoekt shard set as degraded.
  • Caching the readiness result for ~1s to absorb thundering-herd probes from multiple orchestrators.

Note

Low Risk
Additive public endpoint and docs; existing /api/health is untouched. Readiness probes add periodic DB/Redis/Zoekt traffic—misconfigured aggressive intervals could increase load or briefly drain pods from rotation.

Overview
Adds GET /api/health/ready, a public readiness probe that reports Postgres, Redis, and Zoekt health with per-check latencyMs and errors. Checks run in parallel with a 2s cap each; the handler returns 200 when all pass and 503 with status: degraded otherwise. GET /api/health is unchanged for liveness.

Zoekt gRPC client setup moves into packages/web/src/lib/zoektClient.ts (lazy load + cache) so the route can be unit-tested without pulling gRPC at test time. Vitest covers healthy, each failure mode, non-PONG Redis, and parallel execution.

Docs add docs/api-reference/health.mdx (liveness vs readiness, response shape, K8s/Docker examples) and wire it in docs/docs.json; CHANGELOG records the feature.

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

Summary by CodeRabbit

  • New Features

    • Added a readiness health endpoint that reports the status of PostgreSQL, Redis, and Zoekt.
    • Returns HTTP 200 when all dependencies are available and HTTP 503 with per-service details when any check fails.
    • Dependency checks run in parallel with bounded timeouts and include latency information.
  • Documentation

    • Added API documentation with Docker Compose and Kubernetes readiness/liveness probe examples.
    • Clarified the distinction between liveness and readiness health checks.

The vendored Zoekt webserver gRPC client construction needs node:path,
@grpc/grpc-js, @grpc/proto-loader, and the ZOEKT_WEBSERVER_URL env.
Pulling those at module load time also drags the @opentelemetry CJS
chain into anything that imports the client. Move the construction
behind a single loadZoektClient() helper that dynamically imports the
heavy deps, caches the resulting client, and lives in its own module
so readiness / search / stream-search callers can import it without
that boot-time cost and can mock it cleanly in tests.
Standard Kubernetes-style readiness probe. The existing /api/health
liveness endpoint is left untouched.

GET /api/health/ready runs three checks in parallel:
- postgres: prisma.$queryRaw SELECT 1
- redis:    getRedisClient().ping() (rejects non-PONG responses)
- zoekt:    an empty gRPC List with a 1s wall-time cap

Each check is bounded by a 2s timeout, so the worst-case request
time is bounded even when one dependency hangs. Returns 200 with
{status:ok, checks:{...}} when all three pass, 503 with
{status:degraded, checks:{...}} otherwise. Per-check payload includes
status, latencyMs, and an error message when degraded.

The endpoint is unauthenticated (matches the existing /api/health
policy) and PostHog tracking is disabled.
Six cases:
- 200 + status:ok when all three dependencies are reachable
- 503 + postgres error when Postgres is unreachable
- 503 + redis error when Redis ping fails
- 503 + zoekt error when the gRPC call errors
- 503 + redis error when Redis returns a non-PONG response
- all three checks run in parallel (Promise.all), not serially

The zoekt client is mocked via the new @/lib/zoektClient module so
the test does not pull in @grpc/grpc-js at test-load time.
New docs/docs/api-reference/health.mdx describes both endpoints, the
response shape, the per-dependency checks, and example Docker Compose
and Kubernetes probes. Wired into the existing System group in
docs/docs.json.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds an unauthenticated /api/health/ready endpoint that checks Postgres, Redis, and Zoekt in parallel with timeouts, returns detailed health results, and documents and tests the liveness/readiness separation.

Changes

Health readiness

Layer / File(s) Summary
Readiness checks and Zoekt client
packages/web/src/app/api/(server)/health/ready/route.ts, packages/web/src/lib/zoektClient.ts
Adds timeout-bounded Postgres, Redis, and Zoekt checks, aggregates their results into 200 or 503 responses, and lazily caches a Zoekt gRPC client.
Readiness route validation
packages/web/src/app/api/(server)/health/ready/route.test.ts
Tests healthy responses, each dependency failure mode, invalid Redis responses, and parallel check execution.
Health endpoint documentation
docs/docs/api-reference/health.mdx, docs/docs.json, CHANGELOG.md
Documents both health endpoints, response formats, probe configurations, operational guidance, and the unreleased changelog entry.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Probe
  participant ReadinessRoute
  participant Postgres
  participant Redis
  participant Zoekt
  Probe->>ReadinessRoute: GET /api/health/ready
  ReadinessRoute->>Postgres: SELECT 1
  ReadinessRoute->>Redis: PING
  ReadinessRoute->>Zoekt: List RPC
  Postgres-->>ReadinessRoute: Check result
  Redis-->>ReadinessRoute: Check result
  Zoekt-->>ReadinessRoute: Check result
  ReadinessRoute-->>Probe: 200 ok or 503 degraded
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a new /api/health/ready endpoint with dependency health checks.
Linked Issues check ✅ Passed The PR implements the readiness endpoint, parallel Postgres/Redis/Zoekt checks, timeout handling, tests, docs, and changelog requested in #1506.
Out of Scope Changes check ✅ Passed No clearly unrelated changes stand out; the Zoekt client extraction directly supports the new readiness route and the rest matches the stated scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 375f2dd. Configure here.

},
);
});
}, READINESS_TIMEOUT_MS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zoekt load skips timeout

Medium Severity

In checkZoekt, loadZoektClient() runs before withTimeout, so Zoekt client initialization isn't timed out by READINESS_TIMEOUT_MS. This can cause the readiness probe to exceed expected latency, leading to orchestrator probe failures.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 375f2dd. Configure here.

clearTimeout(timer);
}
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timeout race unhandled rejections

Medium Severity

The withTimeout function uses Promise.race. If the timeout wins, the underlying check promise continues to execute. A subsequent rejection from this orphaned promise can lead to an unhandled promise rejection in the Node process, particularly when dependencies are hanging.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 375f2dd. Configure here.

);
})();
}
return zoektClientPromise;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed Zoekt client cached forever

Medium Severity

loadZoektClient stores the first init attempt in zoektClientPromise and never clears it on failure. A transient startup error permanently marks the Zoekt readiness check as failed until the process restarts, unlike search code that rebuilds the client per call.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 375f2dd. Configure here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/web/src/app/api/(server)/health/ready/route.test.ts (1)

56-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for a hung dependency timeout.

The suite covers immediate failures but not the two-second timeout contract. Mock one dependency to never settle, advance fake timers, and assert its check reports error and the route returns 503.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/api/`(server)/health/ready/route.test.ts around lines 56
- 151, Add a test alongside the existing dependency failure cases that leaves
one dependency promise pending, enables or uses fake timers, advances time past
the route’s two-second timeout, then awaits GET and asserts a 503 degraded
response with the hung dependency’s check marked error. Restore timer behavior
and mocks consistently with the surrounding tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 10-11: Update the changelog entry for GET /api/health/ready to
replace the issue `#1506` link with this pull request’s #<id> link using the
/pull/<id> URL format, keeping the PR reference at the end of the line.

In `@packages/web/src/app/api/`(server)/health/ready/route.ts:
- Around line 91-108: Move the loadZoektClient() call into the callback passed
to withTimeout('zoekt', ...) so both lazy client initialization and the
subsequent client.List readiness probe are covered by READINESS_TIMEOUT_MS.
Preserve the existing List request and error propagation behavior.
- Around line 29-50: Update the readiness dependency checks that use withTimeout
so the underlying Postgres raw query, ioredis PING, and Zoekt List operations
have socket/driver-level timeouts or native cancellation configured before
invocation. Ensure each operation terminates when its per-check timeout expires,
rather than merely allowing withTimeout to reject while the work continues in
the background.

---

Nitpick comments:
In `@packages/web/src/app/api/`(server)/health/ready/route.test.ts:
- Around line 56-151: Add a test alongside the existing dependency failure cases
that leaves one dependency promise pending, enables or uses fake timers,
advances time past the route’s two-second timeout, then awaits GET and asserts a
503 degraded response with the hung dependency’s check marked error. Restore
timer behavior and mocks consistently with the surrounding tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 660d10e5-c810-4023-a74b-ee4dc3591380

📥 Commits

Reviewing files that changed from the base of the PR and between 9491a13 and 375f2dd.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • docs/docs.json
  • docs/docs/api-reference/health.mdx
  • packages/web/src/app/api/(server)/health/ready/route.test.ts
  • packages/web/src/app/api/(server)/health/ready/route.ts
  • packages/web/src/lib/zoektClient.ts

Comment thread CHANGELOG.md
Comment on lines +10 to +11
### Added
- Added a `GET /api/health/ready` endpoint that returns per-dependency health (Postgres, Redis, Zoekt) for use as a Kubernetes `readinessProbe` or load-balancer health check. The existing `GET /api/health` endpoint is unchanged and remains the liveness probe. [#1506](https://github.com/sourcebot-dev/sourcebot/issues/1506)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Link the changelog entry to this PR, not issue #1506.

Replace the issue URL with this pull request’s [#<id>](https://github.com/sourcebot-dev/sourcebot/pull/<id>) link. As per coding guidelines, changelog entries must include the GitHub pull request id at the end of the line.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 10 - 11, Update the changelog entry for GET
/api/health/ready to replace the issue `#1506` link with this pull request’s #<id>
link using the /pull/<id> URL format, keeping the PR reference at the end of the
line.

Source: Coding guidelines

Comment on lines +29 to +50
// Wraps a check function in a per-check timeout. When the timeout fires
// first, the check resolves as an error result; the underlying promise is
// allowed to settle in the background (its result is discarded).
const withTimeout = async <T>(
label: string,
check: () => Promise<T>,
timeoutMs: number,
): Promise<T> => {
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => {
reject(new Error(`${label} check timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
try {
return await Promise.race([check(), timeout]);
} finally {
if (timer) {
clearTimeout(timer);
}
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate route and related files =="
fd -a 'route\.ts$' packages/web/src/app 2>/dev/null | sed 's#^\./##' | head -50
echo

echo "== find health readiness route =="
path="$(fd 'route\.ts$' packages/web/src/app 2>/dev/null | grep 'health/ready/route\.ts$' || true)"
echo "path=$path"
if [ -n "$path" ]; then
  wc -l "$path"
  cat -n "$path" | sed -n '1,220p'
fi

echo
echo "== search health checks and clients =="
rg -n "check(Postgres|postgres|Redis|redis|Zoekt|zoekt|withTimeout|Promise\.all|Promise\.race|loadZoektClient|setDeadline|deadline|AbortController|cancel)" packages/web/src -S || true

Repository: sourcebot-dev/sourcebot

Length of output: 4096


🏁 Script executed:

#!/bin/bash
set -u

echo "== health ready route =="
if [ -f 'packages/web/src/app/api/health/ready/route.ts' ]; then
  wc -l 'packages/web/src/app/api/health/ready/route.ts'
  cat -n 'packages/web/src/app/api/health/ready/route.ts' | sed -n '1,220p'
elif [ -f 'packages/web/src/app/api/(server)/health/ready/route.ts' ]; then
  wc -l 'packages/web/src/app/api/(server)/health/ready/route.ts'
  cat -n 'packages/web/src/app/api/(server)/health/ready/route.ts' | sed -n '1,220p'
else
  echo "not found"
fi

echo
echo "== health checks references =="
rg -n "checkPostgres|checkRedis|checkZoekt|loadZoektClient|Zoekt|withTimeout|Promise\.all|setDeadline|AbortController|deadline|cancel" packages/web/src packages -S --glob '!**/node_modules/**' | head -200 || true

echo
echo "== file tree relevant =="
fd -t f . packages/web/src | rg 'postgres|redis|zoekt|health|middleware|auth' | head -200 || true

Repository: sourcebot-dev/sourcebot

Length of output: 36357


🏁 Script executed:

#!/bin/bash
set -u

echo "== read client implementations =="
for f in packages/web/src/lib/zoektClient.ts packages/web/src/lib/redis.ts; do
  if [ -f "$f" ]; then
    echo "--- $f ($(wc -l < "$f") lines) ---"
    cat -n "$f" | sed -n '1,220p'
  fi
done

echo
echo "== package deps relevant to clients =="
python3 - <<'PY'
import json
from pathlib import Path
for p in [Path('packages/web/package.json'), Path('package.json')]:
    if p.exists():
        data=json.loads(p.read_text())
        print(f'--- {p} ---')
        for sec in ['dependencies','devDependencies','peerDependencies']:
            vals=data.get(sec, {})
            hits={k:v for k,v in vals.items() if any(x in k.lower() for x in ['nestjs','redis','prisma','grpc','grpc-js','`@grpc/grpc-js`'])}
            print(sec, hits)
PY

Repository: sourcebot-dev/sourcebot

Length of output: 3220


🏁 Script executed:

#!/bin/bash
set -u

echo "== find shared redis/prisma client implementations =="
rg -n "createRedisClient|create.*Prisma|PrismaClient|`@prisma/client`|redis-4/ioredis|ioredis|`@redis`" . --glob '!**/node_modules/**' --glob '!**/.git/**' | head -200

Repository: sourcebot-dev/sourcebot

Length of output: 22779


🏁 Script executed:

#!/bin/bash
set -u

echo "== shared redis implementation =="
cat -n packages/shared/src/redis.ts | sed -n '1,120p'

echo
echo "== prisma instance config =="
cat -n packages/web/src/prisma.ts | sed -n '1,120p'

echo
echo "== package versions relevant to ioredis/prisma =="
python3 - <<'PY'
import re, pathlib
for f in ["packages/shared/package.json", "packages/db/package.json", "packages/web/package.json", "yarn.lock", "package.json"]:
    p=pathlib.Path(f)
    if p.exists():
        tok=p.read_text(errors="replace")
        lines=[]
        for m in re.finditer(r'("ioredis"|"`@ioredis/ioredis`"|"`@prisma/client`"|"`@grpc/grpc-js`"|`@ioredis/commands`|ioredis@\S*)', tok):
            start=tok.count("\n",0,m.start())+1
            end=tok.count("\n",0,m.end())+1
            line=tok.splitlines()[m.start().count("\n")]
            if "yarn.lock" in f:
                line=tok.splitlines()[len(tok[:m.start()].splitlines())]
            print(f"{f}:{start}: {line.strip()}")
PY

Repository: sourcebot-dev/sourcebot

Length of output: 8146


🌐 Web query:

ioredis v5 socketTimeout default ping interval default maxRetriesPerRequest null behavior when ping hangs

💡 Result:

In ioredis v5, the following behaviors apply regarding the requested configurations: socketTimeout: This option does not have a default value (it is undefined by default) [1][2][3]. When configured, if the socket fails to receive data within the specified number of milliseconds, the socket is considered dead and is destroyed, any running commands are rejected, and the client triggers its reconnect strategy [1][4]. maxRetriesPerRequest: The default value for this option is 20 [5]. When set to null, this behavior is disabled, meaning every command will wait indefinitely for the connection to become alive again [6][5]. Ping Interval: ioredis does not have a configurable "ping interval" option in the standard RedisOptions interface [1][4]. Connection health is primarily maintained by the underlying Node.js socket keep-alive mechanisms [7]. If a PING command (or any other command) hangs, it will be subject to the standard command processing logic. If a socketTimeout is set, that timeout will govern the maximum duration the socket waits for data; if the socket exceeds this limit without receiving data (which would occur if a PING response never arrives), it will be destroyed as described above [1][4]. Top results: [1][4][6][5]

Citations:


🌐 Web query:

Prisma Client v6 default raw query timeout or how setPrismaClient timeout $queryRaw no timeout

💡 Result:

In Prisma Client v6, there is no global configurable timeout specifically for individual raw queries ($queryRaw, $executeRaw) executed outside of a transaction [1][2]. Consequently, there is no "default" timeout value for these operations because they do not have a built-in time limit imposed by Prisma Client [3]. To manage timeouts for raw queries, you must use one of the following approaches: 1. Use Database-Level Timeouts: Since Prisma does not natively limit the duration of a raw SQL statement, you can rely on database-specific settings. For example, in PostgreSQL, you can set statement_timeout at the session level by executing SET statement_timeout TO '10s'; within the same transaction or connection pool session as your raw query [4][5]. 2. Wrap in an Interactive Transaction: If you require a managed timeout, you can execute your raw query within an interactive $transaction [6]. You can configure the timeout option for the transaction, which will roll back the transaction if it exceeds the specified duration [6]. await prisma.$transaction( async (tx) => { return await tx.$queryRawSELECT * FROM users;; }, { timeout: 10000, // 10 seconds timeout for the transaction }); 3. Differentiate from Pool Timeout: Be aware that Prisma does have a pool_timeout (default 10s), which is the maximum time a query will wait in the queue to acquire a connection from the pool before throwing an error [7][8]. This is separate from the time taken to execute the query once a connection is acquired [7][8]. You can adjust this in your database connection URL (e.g., ?pool_timeout=5 for 5 seconds) [8]. If you are using a driver adapter (e.g., node-postgres), you may be able to configure timeouts directly through the driver's configuration object when initializing the adapter, rather than relying on Prisma's internal handling [7][9].

Citations:


Cancel timed-out readiness dependency operations.

withTimeout only rejects the readiness request after two seconds; the Postgres raw query, ioredis PING, and Zoekt List callback can continue running afterward because the clients/operations here are not being cancelled. Add socket/driver-level timeouts or native cancellation before calling withTimeout to avoid accumulating hung readiness probes during outages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/api/`(server)/health/ready/route.ts around lines 29 -
50, Update the readiness dependency checks that use withTimeout so the
underlying Postgres raw query, ioredis PING, and Zoekt List operations have
socket/driver-level timeouts or native cancellation configured before
invocation. Ensure each operation terminates when its per-check timeout expires,
rather than merely allowing withTimeout to reject while the work continues in
the background.

Comment on lines +91 to +108
const client = await loadZoektClient();
await withTimeout('zoekt', async () => {
await new Promise<void>((resolve, reject) => {
// An empty List with a 1s wall-time cap is the smallest request
// that exercises the gRPC channel end-to-end. It returns an
// empty result, not an error, even when no repos are indexed.
client.List(
{ opts: { max_wall_time: { seconds: 1, nanos: 0 } } },
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
},
);
});
}, READINESS_TIMEOUT_MS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Include lazy Zoekt initialization in the timeout.

Line 91 awaits loadZoektClient() before the timeout starts, so the first Zoekt readiness check can exceed the documented two-second bound.

Proposed fix
-        const client = await loadZoektClient();
         await withTimeout('zoekt', async () => {
+            const client = await loadZoektClient();
             await new Promise<void>((resolve, reject) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const client = await loadZoektClient();
await withTimeout('zoekt', async () => {
await new Promise<void>((resolve, reject) => {
// An empty List with a 1s wall-time cap is the smallest request
// that exercises the gRPC channel end-to-end. It returns an
// empty result, not an error, even when no repos are indexed.
client.List(
{ opts: { max_wall_time: { seconds: 1, nanos: 0 } } },
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
},
);
});
}, READINESS_TIMEOUT_MS);
await withTimeout('zoekt', async () => {
const client = await loadZoektClient();
await new Promise<void>((resolve, reject) => {
// An empty List with a 1s wall-time cap is the smallest request
// that exercises the gRPC channel end-to-end. It returns an
// empty result, not an error, even when no repos are indexed.
client.List(
{ opts: { max_wall_time: { seconds: 1, nanos: 0 } } },
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
},
);
});
}, READINESS_TIMEOUT_MS);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/api/`(server)/health/ready/route.ts around lines 91 -
108, Move the loadZoektClient() call into the callback passed to
withTimeout('zoekt', ...) so both lazy client initialization and the subsequent
client.List readiness probe are covered by READINESS_TIMEOUT_MS. Preserve the
existing List request and error propagation behavior.

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.

[FR] Add /api/health/ready endpoint with dependency health checks (Postgres, Redis, Zoekt)

1 participant