# Health (/api/health)



Two routes an uptime monitor or a load balancer can call with no API key. They answer in their own small shape, never the `{ error: { code, ... } }` envelope every other route uses.

| Endpoint            | Checks                                      | Brake                               |
| ------------------- | ------------------------------------------- | ----------------------------------- |
| `GET /health`       | Nothing. The API process answered           | None                                |
| `GET /health/ready` | One database query, with a 3 second timeout | 60 requests a minute per IP address |

* Both send `Cache-Control: no-store`.
* No key needed. A key sent anyway is ignored.
* No version, commit or region in either body.

## Liveness [#liveness]

<Endpoint method="GET" path="/health" auth="none" />

Touches nothing but the process. Use it to ask "is the API up".

```bash title="Terminal"
curl -s https://app.riffads.com/api/v1/health
```

```json title="Response: 200"
{
  "ok": true,
  "status": "ok",
  "service": "riffads-api"
}
```

This is the only answer it gives. Anything else (a timeout, a 5XX from the platform in front) means the API is down.

## Readiness [#readiness]

<Endpoint method="GET" path="/health/ready" auth="none" />

Runs one query against the database. Use it to ask "can the API serve a real request".

```bash title="Terminal"
curl -s -w "\n%{http_code}\n" https://app.riffads.com/api/v1/health/ready
```

```json title="Response: 200"
{
  "ok": true,
  "status": "ready",
  "checks": { "database": "ok" }
}
```

```json title="Response: 503"
{
  "ok": false,
  "status": "unavailable",
  "checks": { "database": "down" }
}
```

* `503` when the query fails or takes longer than 3 seconds.
* The reason is never in the body. Only `database: "down"`.

### The IP brake [#the-ip-brake]

`/health/ready` does real work, so it is braked at **60 requests a minute per IP address**. Over it:

```json title="Response: 429"
{
  "error": "too_many_requests",
  "error_description": "Too many requests from this address. Please wait and try again."
}
```

* Sent with `Retry-After` in seconds and `Cache-Control: no-store`.
* `/health` is never braked.
* One monitor every 30 seconds is far below it.

## What healthy does not mean [#what-healthy-does-not-mean]

* Readiness checks the database only. Renders run on outside model providers. A provider outage shows up on the generation (`provider_unavailable`), not here.
* A healthy probe does not mean your key works. Test the key with a cheap read such as `GET /capabilities`. [Authentication](/api/authentication).

<Callout type="info" title="Branch on the HTTP status">
  Both probes answer `ok` plus `status`. Branch on the HTTP status first (200, 429 or 503), then on `status`. Never parse them as the agent envelope: there is no `error.code` and no `retryable` here.
</Callout>

## Worked example: a monitor [#worked-example-a-monitor]

Poll liveness often and readiness less often:

```bash title="Terminal"
# Every 30 seconds: is the API up?
curl -sf https://app.riffads.com/api/v1/health > /dev/null || echo "RiffAds API down"

# Every 2 minutes: can it serve a request?
code=$(curl -s -o /dev/null -w "%{http_code}" https://app.riffads.com/api/v1/health/ready)
[ "$code" = "200" ] || echo "RiffAds API not ready ($code)"
```

A `429` from readiness means your monitors share an address and poll too often. Slow them down: it is not an outage.
