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: is the process running?GET /health/ready: can it classify mail right now?GET /health: queue and intake counts.
None of them need the proxy secret or check the license.
Check That It Is Running
/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
/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
/healthWhen to Use It
- Confirm that SpamFoo Client is accepting requests before you send mail to it.
- Watch for
shutting_downso 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.
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 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. |
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.
Example
curl -s http://localhost:16253/health
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.");
}
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.")