# Health checks

Four addresses that say whether GoRunner is up, and whether it can do its work.

| Address | Asked on | Answers |
| --- | --- | --- |
| `GET /api/health` | Your installation’s address | `200` while the app runs. |
| `GET /api/ready` | Your installation’s address | `200` when the app reaches its database and Redis. `503` when it can’t. |
| `GET /healthz` | Port `9464` of each app and worker | `200` while the process runs. |
| `GET /readyz` | Port `9464` of each app and worker | `200` when the process can work. A worker also needs its heartbeat from the last 90 seconds. |

Point a load balancer, an ingress or an uptime monitor at `/api/ready`. It needs no token.

`/healthz` and `/readyz` answer only inside the Docker network, and only with `METRICS_ENABLED=true`. The app’s second process, `api-twin`, uses port `9465`.

## What the answer shows

Anyone who asks gets the status alone. `/api/ready` answers `{"status":"ok"}`, or `{"status":"unavailable"}` with `503`. `/api/health` answers in the API’s usual shape, `{"data":{"status":"ok"}}`.

A request made on the server itself gets the version and each check. That means from inside the app, with no proxy in between. The setup and the update ask this way.

```bash
docker compose exec api wget -qO- http://127.0.0.1:3001/api/ready
```

```text
{"status":"unavailable","version":"v2026.10.24","checks":{"database":{"ok":true,"ms":3},"redis":{"ok":false,"ms":2001,"error":"did not answer within 2s"}}}
```

Each check has 2 seconds. A failed check names its cause in a fixed phrase, such as `connection refused` or `authentication failed`, never the address or the user. The full message is in the app’s log.

The answer is reused for one second, so a load balancer and a burst of callers cost the database one ping.

**Why does my proxy only get the short answer?**

A request that carries `X-Forwarded-For`, `X-Real-IP`, `Forwarded` or `True-Client-IP` gets the short answer, even from the server itself. So a proxy that passes a caller’s header through can’t make a caller look local.

**Can everyone see the details?**

Set `HEALTH_DETAILS=public` in `.env`, then apply it with `./deploy.sh "$(cat VERSION)"`. On Kubernetes it is `api.healthDetails: public`. The probes work the same either way.

Related: [Connect a monitoring tool](https://docs.gorunner.io/self-host/monitoring-tools) · [Metrics](https://docs.gorunner.io/self-host/metrics) · [Behind your own proxy](https://docs.gorunner.io/self-host/own-proxy) · [Troubleshooting](https://docs.gorunner.io/self-host/troubleshooting)

Source: https://docs.gorunner.io/self-host/health-checks
