Source: https://help.spamfoo.com/api/health
Last updated: 2026-10-27

***

# Check Health

Check that SpamFoo Client is up, whether it's ready to classify mail, and how busy its classification queues are. Use these endpoints for service monitors, container health checks, load balancers, and tuning queue limits.

- [`GET /health/live`](https://help.spamfoo.com/api/health#live): is the process running?
- [`GET /health/ready`](https://help.spamfoo.com/api/health#ready): can it classify mail right now?
- [`GET /health`](https://help.spamfoo.com/api/health#health): queue and intake counts.

None of them need the proxy secret or check the license.

## Check That It Is Running

`GET /health/live`

Returns `200` with `{"status":"live"}` whenever the process is running and answering. The Docker image's health check uses it. If the request can't connect, SpamFoo Client is down.

## Check That It Is Ready

`GET /health/ready`

Returns `200` when SpamFoo Client can classify mail:

```
{"status": "ready", "contract": 1}
```

Otherwise it returns `503` with a `Retry-After: 5` header and the reasons:

```
{
    "status": "not_ready",
    "code": "not_ready",
    "reasons": ["models_loading", "not_activated"],
    "contract": 1
}
```

| Reason | Meaning |
| --- | --- |
| `not_activated` | No license key is set up. |
| `models_loading` | The models haven't finished downloading. Classification requests wait for them. |
| `shutting_down` | SpamFoo Client is stopping, for example to install an update. `code` is then `shutting_down` instead of `not_ready`. |

Use it to decide whether to send mail, or to take SpamFoo Client out of a load balancer. In standalone mode, when SpamFoo Client is asked to stop, it keeps answering for a few seconds with `shutting_down` before it closes, so callers notice before connections are refused.

## Check Queue Load

`GET /health`

### When to Use It

- Confirm that SpamFoo Client is accepting requests before you send mail to it.
- Watch for `shutting_down` so a load balancer or mail server can stop sending requests while SpamFoo Client restarts for an update.
- Track queue counters to decide whether to raise or lower SpamFoo Client's concurrency and intake limits.

The endpoint needs no proxy secret and has no license check. It returns `200` whenever SpamFoo Client is running, including while it prepares to restart, so check the `status` field rather than the status code. A request that cannot connect, or times out, means the service is down.

> **Note** `healthy` means the service is running and not waiting to restart. It does not check whether the models have finished downloading or loading. Use [`/health/ready`](https://help.spamfoo.com/api/health#ready) for that.

In standalone mode, a caller that isn't on loopback and doesn't send the proxy secret gets only `{"status": "healthy", "service": "SpamFoo Client"}`, without the queue counts.

### Request

No headers, query parameters, or body.

### Response

```
{
    "status": "healthy",
    "service": "SpamFoo Client",
    "shuttingDown": false,
    "activeRequests": 1,
    "modelReloading": false,
    "spoolQueue": {
        "active": 1,
        "waiting": 0,
        "maxConcurrency": 2,
        "totalProcessed": 18234,
        "totalTimedOut": 0
    },
    "backgroundQueue": {
        "active": 0,
        "waiting": 0,
        "maxConcurrency": 1,
        "totalProcessed": 912,
        "totalTimedOut": 0
    },
    "classifyIntake": {
        "inFlight": 1,
        "maxInFlight": 1024,
        "peakInFlight": 9,
        "totalRejected": 0
    },
    "bulkIntake": {
        "inFlight": 0,
        "maxInFlight": 1,
        "peakInFlight": 1,
        "totalRejected": 0
    },
    "authHeaderRaises": {
        "live_compared": 18234,
        "header_only": 0,
        ...
    }
}
```

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | `healthy`, or `shutting_down` after SpamFoo Client has started to restart for an update. It finishes the requests in progress before it restarts. |
| `service` | string | `SpamFoo Client`. |
| `shuttingDown` | boolean | `true` when `status` is `shutting_down`. |
| `activeRequests` | integer | Classification and embedding requests in progress right now. |
| `modelReloading` | boolean | `true` while SpamFoo Client pauses its queues to swap in updated models. New requests wait until the swap finishes. |
| `spoolQueue` | object | The real-time queue used by `/classify-stream`. |
| `backgroundQueue` | object | The queue used by `/tabs/classify-stream` and SpamFoo Client's own background work. |
| `classifyIntake` | object | The limit on how many classification requests can hold a message in memory at once. |
| `bulkIntake` | object | The same counts for requests sent with `X-SpamFoo-Priority: bulk`, which have their own limit. See [Mark Bulk Work](https://help.spamfoo.com/api/classify#priority). |
| `authHeaderRaises` | object | Counters SpamFoo uses to study how email authentication results are checked. You can ignore them. |

#### Queue Fields

`spoolQueue` and `backgroundQueue` have the same fields. Counters start at zero when SpamFoo Client starts.

| Field | Type | Description |
| --- | --- | --- |
| `active` | integer | Jobs running now. |
| `waiting` | integer | Jobs waiting for a free slot. |
| `maxConcurrency` | integer | How many jobs can run at once. |
| `totalProcessed` | integer | Jobs finished since start. |
| `totalTimedOut` | integer | Jobs that waited too long and returned `503` with `Classification queue timeout`. |

#### Intake Fields

`classifyIntake` and `bulkIntake` have the same fields.

| Field | Type | Description |
| --- | --- | --- |
| `inFlight` | integer | Requests to `/classify-stream` and `/tabs/classify-stream` in progress now. |
| `maxInFlight` | integer | The limit. For `classifyIntake` it's set by `SPAMFOO_MAX_CLASSIFY_INFLIGHT`. For `bulkIntake` it's one less than Max Concurrency, and at least 1. Requests over it return `503` with `server_busy`. |
| `peakInFlight` | integer | The highest `inFlight` value since start. Use it to pick a realistic limit for your traffic. |
| `totalRejected` | integer | Requests turned away with `server_busy` since start. |

## Errors

`/health` and `/health/live` have no error responses of their own. `/health/ready` returns `503` when SpamFoo Client isn't ready. If SpamFoo Client is not running, the connection is refused. In standalone mode, an address blocked for wrong secrets gets `429` on these endpoints too. See [Repeated Wrong Secrets](https://help.spamfoo.com/api#blocked).

## Example

### curl

```
curl -s http://localhost:16253/health
```

### C#

```
using System.Net.Http.Json;
using System.Text.Json;

using var http = new HttpClient
{
    BaseAddress = new Uri("http://localhost:16253"),
    Timeout = TimeSpan.FromSeconds(5)
};

try
{
    var health = await http.GetFromJsonAsync<JsonElement>("/health");
    var status = health.GetProperty("status").GetString();
    var waiting = health.GetProperty("spoolQueue").GetProperty("waiting").GetInt32();
    Console.WriteLine($"status={status} waiting={waiting}");
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
{
    Console.WriteLine("SpamFoo Client is not reachable.");
}
```

### Python

```
import requests

try:
    health = requests.get("http://localhost:16253/health", timeout=5).json()
    print(f"status={health['status']} waiting={health['spoolQueue']['waiting']}")
except requests.RequestException:
    print("SpamFoo Client is not reachable.")
```
