5563 by default, referenced as the named container port http in the chart). Each is deliberately scoped, /readyz flipping to 503 must mean “this replica should not serve customer traffic right now”, and nothing more.
Endpoint summary
The three probes are in-cluster signals.
/health is the one meant to be reachable from outside; the chart publishes it through the ingress and leaves the rest internal.
The three probes return the same JSON shape:
checks is omitted when there are no registered checks AND the status is ok. The gateway registers MarkStarted (at boot) and MarkDraining (on SIGTERM), but no per-dependency liveness or readiness checks. The probes are intentionally lightweight signals about process state and lifecycle, not external-dependency health. Dependency health is reported on /health, per-request error codes, and the OTel trace surface, never on the probes.
/healthz (liveness)
charts/gateway/templates/deployment.yaml):
/readyz (readiness)
SIGTERM:
SIGTERM or via an explicit administrative call.
Chart default:
What /readyz does NOT check
- Per-provider live health. If OpenAI is rate-limiting, the gateway still serves and falls back per the VK’s configured chain. Reporting
not_readyfor one upstream would amplify the incident. - The control plane. A control-plane blip is a degradation the auth cache is built to absorb: warm keys keep serving from the in-process cache. Failing readiness on it would drop every replica from the load balancer at once and turn that degradation into a total outage. Control-plane reachability is reported on
/healthinstead, where it informs a status page rather than gating traffic. - Redis L2. The current gateway has no Redis client; the auth cache is in-process LRU.
- PostgreSQL. The gateway never talks to Postgres directly; the control plane mediates persistence.
MarkDraining.
/startupz (startup)
MarkStarted is called while dependencies are being constructed, which finishes before the HTTP listener opens, so /startupz is already 200 on the first request it can possibly receive and the starting state is unobservable over HTTP. The auth cache warms organically on the first request per virtual key; an unwarmed gateway costs a cold-cache request one extra control-plane round trip, which is the correct behavior for a fresh deployment. The chart’s generous failure threshold is headroom kept for a future bootstrap-pull, not a window the gateway currently uses.
/startupz first returns 200, Kubernetes stops calling it; readiness + liveness take over.
/health (public status endpoint)
This is the endpoint to point a status page or uptime monitor at. Plain HTTP monitor semantics: 200 healthy, 503 not. HEAD returns the same status and headers with no body, as HTTP requires, so a monitor that probes with HEAD reads the same verdict. Responses carry Cache-Control: no-store so a CDN cannot serve a stale verdict. Any other method is 405: the ingress publishes the path, the gateway bounds the methods.
version, no uptime_s. This endpoint is polled by the public internet, and restart cadence and build identity are not something to publish.
What it covers, and what it never will
The verdict covers the gateway process and the dependencies you own: the control plane. It is structurally independent of model providers.- A provider outage cannot turn it red. Nothing on the verdict path reads dispatch state, so if OpenAI and Anthropic are both down, completions fail while
/healthstays 200. That is the providers’ status page’s news, not yours, and a gateway that goes red on someone else’s outage trains people to ignore the page. - A poll never triggers an upstream call. A background monitor probes the control plane every 15 s and
/healthserves the cached verdict, so poll rate and control-plane load are unrelated. On an unauthenticated public endpoint, fanning out per poll would be an amplification and cost bug.
Control-plane semantics
The monitor probes the control plane’s/api/internal/gateway/health over the same HMAC-signed internal channel every request uses. That is the point of the design: a 200 proves DNS, TCP/TLS, the app being up, and the shared LW_GATEWAY_INTERNAL_SECRET matching. A mismatched secret is the misconfiguration where every pod looks green while every virtual-key resolve is refused, and this is the only signal that catches it.
Unreachability shorter than 60 s stays 200: warm keys keep serving from the auth cache through a blip, and a page that flaps on what customers cannot feel is worse than no page. Past 60 s, cold-cache requests are failing with auth_upstream_unavailable and the page should say so. A booting pod is given one full 60 s window before it can report red, so a rolling deploy never blinks the page.
Because the tolerance is 60 s and the probe interval is 15 s, a monitor polling every 30 to 60 s sees a sustained outage within about two minutes of onset.
Exposure
The chart publishes exactly this one path through the ingress, as anExact match, so /healthz, /readyz, /startupz, /metrics and /internal stay in-cluster:
https://gateway.your-corp.com/health. Set ingress.healthPath.enabled: false if you would rather not publish it at all; the endpoint still answers in-cluster.
Graceful shutdown
SIGTERM triggers (the chart’s shutdown.preDrainWait and shutdown.timeout knobs are not currently wired into the gateway code; the gateway uses Server.GracefulSeconds from pkg/config/server.go, default 5, bump via SERVER_GRACEFUL_SECONDS if you need a longer window):
- Immediately:
MarkDraining()flips/readyzto 503 withstatus:"draining". The Service’s endpoint controller and the LB observe and stop routing new traffic. - Drain window: existing in-flight requests continue. SSE streams continue until the upstream provider closes them or the request finishes naturally.
- End of
GracefulSeconds: the HTTP server shuts down; remaining sockets close cleanly. - Process exits 0.
terminationGracePeriodSeconds to the drain window plus a few seconds of slack:
/readyz=503 before the listener closes, a small fraction of in-flight requests during rolling deploys hits a replica that has already shut down, the LB returns 502. If you observe this on rollout, add a preStop sleep so the pod stays around long enough for the LB to remove it:
End-to-end synthetic check
Neither the probes nor/health exercise the request path, and /health will not tell you a provider is down, on purpose. For real-traffic confidence, run a synthetic completion every 30 s. Keep it on an internal alert rather than on the public status page, so a provider incident does not read as a LangWatch incident:
- Capture the
X-LangWatch-Request-Idheader from the response. - Look up the request’s trace in the LangWatch UI under Origin =
gateway. - If the gateway returned 5xx, pull the gateway pod logs filtered by pod name (
GATEWAY_NODE_IDis unused; the gateway derivesnode_idfromos.Hostname()which inside a Kubernetes pod is the pod name). - If the gateway returned the upstream provider’s error verbatim, the gateway is healthy, the provider is degraded.