---
title: Configure routing health probes
description: "Configure the alpha end-to-end probe that sets a workload cluster's TrafficMap weight to zero after repeated failures, for every InferenceService or for one."
status: preview
since: v1.3
---
A routing health probe takes traffic away from a workload cluster when requests stop getting through to it, even while its pods are Ready. The control plane sends a request to an [InferenceService](../../concepts/serving/inference-services.md)'s endpoint on each of its workload clusters. After enough failures in a row, it sets that cluster's weight in the service's [TrafficMap](../../concepts/rollouts-and-traffic/traffic-map.md) to zero, and the other clusters take its traffic. That catches what readiness misses: a broken ingress, DNS record, certificate or route. You set the probe for every routed InferenceService in the `ome-resources` chart, or for one service in its `spec.routing.probe`.
Multi-cluster routing is alpha, still in development, and off by default. Its API, the probe settings included, can change without notice. [Step 1](#step-1-set-a-probe-for-every-service) turns routing on along with the probe.
- A control-plane cluster running OME v1.3 or later, installed with the `ome-resources` chart as the release `ome` in the namespace `ome`, with `ome.multicluster.enabled: true` and `ome.multicluster.role: control-plane`. You need Helm and `kubectl` access to it.
- Workload clusters registered on the control plane as [WorkloadClusters](../../reference/api/ome.v1beta1.md#ome-io-v1beta1-WorkloadCluster). The examples call them `worker-a` and `worker-b`.
- The InferenceService `qwen3-0-6b` in the namespace `qwen3-0-6b` on the control plane, placed on both workload clusters. Step 2's manifest shows its `placement` block. `kubectl get inferenceservice qwen3-0-6b -n qwen3-0-6b -o jsonpath='{.status.placement.phase}'` prints `Placed`.
- Network access from the controller's pods on the control plane to each workload cluster's endpoint for the InferenceService.
## How the probe works
Every `period`, OME sends one request, with the probe's `method`, to the `endpoint` in each cluster's TrafficMap entry, followed by the probe's `path`. The endpoint is the `status.url` of the service's copy on that cluster, so the probe URL depends on the cluster's ingress, like the capacity poll URLs in [The report format](tune-routing-capacity-polling.md#the-report-format). On `worker-a`, with Gateway API and the default shared host, `/v1/models` gives `http://llm.worker-a.example.com/qwen3-0-6b/qwen3-0-6b/v1/models`. OME doesn't follow redirects. An attempt gets one of three results:
| Result | When |
| --- | --- |
| `Passing` | The status is in `acceptStatuses`. |
| `Failing` | The status is in `gateStatuses`, or the request fails: a DNS, connection or TLS error, or no full response within `timeout`. |
| `Unknown` | Any other status, such as a redirect. It leaves the counts and the gate as they are, so it never moves traffic. |
After `failureThreshold` `Failing` results in a row, OME gates the cluster: its weight drops to zero, and its entry's `healthy` turns `false`. After `successThreshold` `Passing` results in a row, OME lifts the gate and restores the weight. Until a cluster is gated, its weight follows its ready replicas, as it would without a probe.
A gated cluster stays gated when the controller restarts. Changing any probe setting lifts the gates and starts the counts over. A failing cluster then gets traffic until it fails `failureThreshold` times in a row again.
Probes share [the observer limits](tune-routing-capacity-polling.md#the-observer-limits) with the capacity polls: the chart's `routing.observer` block caps the requests underway at once, across all routed InferenceServices, and how much of an answer OME reads. An attempt that waits for a request slot can't time out, since `timeout` starts with the request, but on a large fleet the waits stretch the time between a cluster's attempts past `period`: its entry's `lastProbeTime` falls behind. Raise `routing.observer.maxConcurrentRequests` when it does.
## Step 1: Set a probe for every service
Set a probe for all routed InferenceServices under `ome.multicluster.config.routing.probe` in your `ome-resources` values file. This example turns routing on and probes `/v1/models` every 10 seconds:
```yaml title="values.yaml"
ome:
multicluster:
enabled: true
role: control-plane
config:
routing:
enabled: true
probe:
path: /v1/models
method: GET
acceptStatuses: [200]
gateStatuses: [404, 500, 502, 503, 504]
period: 10s
timeout: 3s
failureThreshold: 3
successThreshold: 2
allFailedPolicy: PreserveTraffic
```
Keep your other values in the same file: `helm upgrade` resets the values that aren't in it to the chart's defaults.
| Field | What it sets |
| --- | --- |
| `path` | The path that OME appends to the cluster's endpoint, starting with `/`. Pick one that answers without a body or credentials, such as `/v1/models`. |
| `method` | `GET`, `HEAD` or `POST`. |
| `acceptStatuses` | The statuses that count as `Passing`. |
| `gateStatuses` | The statuses that count as `Failing`. It can't hold a status from `acceptStatuses`, or 401, 403 or 429, which mean that the endpoint is up but busy or wants credentials. |
| `period` | How often OME probes a cluster, such as `10s`. At least the chart's [`routing.observer.minPeriod`](tune-routing-capacity-polling.md#the-observer-limits), `1s` by default. |
| `timeout` | How long an attempt can take. Shorter than `period`. |
| `failureThreshold` | How many `Failing` results in a row gate a cluster. |
| `successThreshold` | How many `Passing` results in a row lift the gate. |
| `allFailedPolicy` | When all the clusters are gated, `PreserveTraffic` weights them as if there were no probe, and `Drain` leaves them at zero. See [When every cluster fails](#when-every-cluster-fails). |
With these values, a failing cluster is gated about 20 seconds after its first failure, and gets its weight back after two passes in a row.
The probe has no defaults, so set all nine fields. By default, the chart sets no probe. A missing or invalid field stops the controller from starting, as [The controller rollout doesn't finish](#the-controller-rollout-doesnt-finish) shows.
Upgrade the release with the chart version that you run. The probe needs 1.3.0 or later:
```bash
helm upgrade --install ome oci://ghcr.io/moirai-internal/charts/ome-resources \
--namespace ome --version 1.3.0 -f values.yaml
```
Until v1.3 is released, upgrade from [a checkout of `main`](../../getting-started/install.md#install-from-source) instead.
Helm reports that the release `ome` has been upgraded. It can also warn that it's ignoring the non-table value `[]` for `ome-resources.ome.multicluster.config.routing.probe`: that's the chart's empty default, so ignore it.
The controller restarts with the probe. Check that it's back up:
```bash
kubectl rollout status deployment ome-controller-manager -n ome
```
When the rollout finishes, kubectl prints:
```output
deployment "ome-controller-manager" successfully rolled out
```
## Step 2: Set a probe for one service
Give an InferenceService its own probe in `spec.routing.probe` when its runtime answers on another path or needs another `allFailedPolicy`, or when the chart sets no probe. This manifest probes `qwen3-0-6b` every 30 seconds, and drains it when all its clusters fail:
```yaml title="isvc.yaml"
apiVersion: ome.io/v1beta1
kind: InferenceService
metadata:
name: qwen3-0-6b
namespace: qwen3-0-6b
spec:
model:
name: qwen3-0-6b
runtime:
name: srt-qwen3-0-6b
engine:
minReplicas: 1
maxReplicas: 1
placement:
policy: ClusterAffinity
mode: All
clusterAffinity:
- matchFields:
- key: metadata.name
operator: In
values: [worker-a, worker-b]
routing:
probe:
path: /v1/models
method: GET
acceptStatuses: [200]
gateStatuses: [404, 500, 502, 503, 504]
period: 30s
timeout: 5s
failureThreshold: 3
successThreshold: 2
allFailedPolicy: Drain
```
`spec.routing.probe` replaces the chart's probe as a whole, so set all nine fields: the API server rejects a probe that's missing one.
```bash
kubectl apply -f isvc.yaml
```
```output
inferenceservice.ome.io/qwen3-0-6b configured
```
## Step 3: Check the probe state
OME records the probe results on the InferenceService's TrafficMap, which has the same name and namespace. List each cluster's weight, health and last probe result:
```bash
kubectl get trafficmap qwen3-0-6b -n qwen3-0-6b \
-o jsonpath='{range .spec.entries[*]}{.cluster}{" "}{.weight}{" "}{.healthy}{" "}{.probe.result}{" "}{.probe.message}{"\n"}{end}'
```
When `worker-b` has answered 503 three times in a row, and `worker-a` passes:
```output
worker-a 1 true Passing HTTP 200
worker-b 0 false Failing HTTP 503
```
With both clusters passing, both lines end in `true Passing HTTP 200`.
`weight` is the cluster's share of the traffic, and `healthy` is `true` when the cluster has ready replicas and isn't gated. The entry's `probe` block holds:
| Field | What it holds |
| --- | --- |
| `result` | The last attempt's result: `Passing`, `Failing` or `Unknown`. |
| `message` | What the last attempt got: the status, as in `HTTP 503`, or the error. |
| `gated` | `true` while the cluster is gated, even after `result` turns `Passing`, until `successThreshold` passes in a row. |
| `consecutiveFailures` | The cluster's `Failing` results in a row. |
| `lastProbeTime` | When the last attempt finished. If it stops moving, OME isn't probing the cluster. |
`gated` and `consecutiveFailures` appear only while they're set. An entry gets a `probe` block once OME has probed its cluster. See [`TrafficMapProbe`](../../reference/api/ome.v1beta1.md#ome-io-v1beta1-TrafficMapProbe) for all its fields.
[`kubectl ome placement endpoint`](../../reference/kubectl-ome/placement.md#endpoint), which reads the alpha multi-cluster features, also shows the clusters' weights, recorded probe results and gates.
## When every cluster fails
When all the service's clusters are gated, OME follows the probe's `allFailedPolicy`:
- `PreserveTraffic`: OME ignores the gates, and weights the clusters as it would without a probe. The entries still show `healthy: false`. The `Routable` condition stays `True`, with the reason `AllHomesProbeFailed`.
- `Drain`: all weights stay at zero. The `Routable` condition turns `False`, with the reason `AllHomesProbeFailed`.
A failure on the control plane's side, such as its egress or DNS, fails all the clusters at once, while they may be serving normally. `PreserveTraffic` keeps traffic flowing through such a failure. Choose `Drain` only when sending traffic to clusters that all fail their probe is worse than sending none.
Read the outcome in the `Routable` condition. With the `Drain` probe from Step 2, when both clusters are gated:
```bash
kubectl get trafficmap qwen3-0-6b -n qwen3-0-6b \
-o 'custom-columns=ROUTABLE:.status.conditions[?(@.type=="Routable")].status,REASON:.status.conditions[?(@.type=="Routable")].reason,MESSAGE:.status.conditions[?(@.type=="Routable")].message'
```
```output
ROUTABLE REASON MESSAGE
False AllHomesProbeFailed 2 home(s) have conclusive probe failures; all route weights are zero
```
With `PreserveTraffic`, the message is `2 home(s) have conclusive probe failures; preserving traffic among homes with ready capacity`.
## Turn the probe off
To stop probing one InferenceService, whatever the chart sets, replace the `probe` block in `isvc.yaml` with `disabled: true`:
```yaml
spec:
routing:
probe:
disabled: true
```
Apply the file again:
```bash
kubectl apply -f isvc.yaml
```
```output
inferenceservice.ome.io/qwen3-0-6b configured
```
OME then stops probing, lifts the gates, and leaves the `probe` block out of the entries. To take the service out of routing instead, and delete its TrafficMap, set `spec.routing.enabled: false`.
To go back to the chart's probe, delete `probe` from `spec.routing`, and apply the file again. To stop the chart's probe, delete its block from your values file, and run the `helm upgrade` command from Step 1 again. InferenceServices with their own `spec.routing.probe` keep it.
## Troubleshooting
### The controller rollout doesn't finish
With an unusable multi-cluster setting, the new controller pods exit when they start and keep restarting, and `kubectl rollout status` ends with `deployment "ome-controller-manager" exceeded its progress deadline`. Find the setting in the controller's logs:
```bash
kubectl logs -n ome -l control-plane=ome-controller-manager --tail=-1 | grep 'multi-cluster configuration'
```
The error starts with `invalid multi-cluster configuration:` and names the setting, as in `routing.probe.timeout (10s) must be shorter than routing.probe.period (10s)`, or `routing.probe.period (500ms) must be at least routing.observer.minPeriod (1s)` for a period under [the observer's floor](tune-routing-capacity-polling.md#the-observer-limits). For a probe block that leaves out a string field, it starts with `load multi-cluster configuration: unable to parse multicluster config json`. Fix the value in your values file, and run `helm upgrade` again.
### The TrafficMap doesn't pick up the probe
`kubectl apply` accepts some mistakes in `spec.routing.probe` that OME refuses later: a status that's in both `acceptStatuses` and `gateStatuses`, or a `period` under the chart's [`routing.observer.minPeriod`](tune-routing-capacity-polling.md#the-observer-limits). Until you fix them, OME stops probing the service's clusters, and [its TrafficMap stops updating](../../concepts/rollouts-and-traffic/traffic-map.md#where-it-comes-from): it keeps its last weights and probe results. A new InferenceService gets no TrafficMap. Find the error in the controller's logs:
```bash
kubectl logs -n ome -l control-plane=ome-controller-manager --tail=-1 | grep -E 'validate routing policy|resolve probe policy'
```
The error reads like `validate routing policy: spec.routing.probe status 503 is in both acceptStatuses and gateStatuses`, or `resolve probe policy: spec.routing.probe.period (500ms) must be at least routing.observer.minPeriod (1s)`. Fix the field, and apply `isvc.yaml` again.
### A cluster is gated
Read the cluster's `probe.message` with the command in [Step 3](#step-3-check-the-probe-state):
| Message | What to check |
| --- | --- |
| `HTTP 404` | The path is wrong, or the cluster's route is missing. Compare `path` with the entry's `endpoint`. |
| Another status in `gateStatuses`, such as `HTTP 503` | The cluster's gateway, route or engine is failing. Fix the cluster. |
| `transport error:` or `response body error:`, then the error | The request failed: a DNS error, a refused connection, an untrusted certificate or a timeout. Check that the controller's pods can reach the endpoint, and raise `timeout` if it's slow. The probe trusts the public certificate authorities in the controller image, and has no setting for a private one. |
When all the clusters fail at once, check the controller's egress first.
A cluster whose result stays `Unknown`, with a message such as `HTTP 301 (not in accept or gate list)`, is never gated. Add the status to one of the lists, or choose a path that answers with a listed status.
## Next steps
- [Traffic map](../../concepts/rollouts-and-traffic/traffic-map.md): how OME builds the TrafficMap and works out the clusters' weights.
- [Tune routing capacity polling](tune-routing-capacity-polling.md): cap a cluster's weight at the replicas that its endpoint reports it can serve.
- [Drain a workload cluster](drain-a-workload-cluster.md): hold a workload cluster at zero weight with `kubectl ome traffic drain`, whatever its probe says.