Concepts · 06

Error classification

How the Halton Meter daemon normalises every provider's error vocabulary into seven operator-action buckets, the four wire fields that carry the classification, and where classified errors appear locally.

macOS 12+ · Python 3.11+ Reading time 2 min Updated May 11, 2026

Every LLM provider has its own error vocabulary. Anthropic returns overloaded_error on HTTP 529. Gemini returns gRPC RESOURCE_EXHAUSTED mapped to HTTP 429. OpenAI returns insufficient_quota on HTTP 429. The HTTP status alone does not tell an operator what to do — a 429 might mean “back off and retry” or “your billing is exhausted, no amount of retry will fix it”.

Error classification normalises every provider’s error vocabulary into seven canonical buckets. The bucket maps directly to the operator action to take — back off, fix billing, wait it out, fix the request shape. The bucket is provider-agnostic, so your local logs and the bundled dashboard read the same regardless of which provider was called.

The daemon classifies errors locally; this ships in v0.3.0. No network round-trip is involved — the bucket is computed on the machine, written to local SQLite, and read straight back by the bundled dashboard.

The seven canonical buckets

error_classMeaningOperator action
rate_limitProvider throttled the request (RPM / TPM)Back off; retry with exponential delay
server_errorProvider-side fault or availability eventWait it out; retry; check the provider status page
bad_requestRequest shape was rejected (schema, model, size, region)Fix the caller; do not retry as-is
authCredentials, permissions, or billing exhaustedFix the API key, org access, or billing balance
timeoutRequest exceeded the provider’s deadlineReduce payload; shorten prompt; retry
networkCould not reach the providerCheck egress; retry
unknownProvider returned an error the classifier did not matchInspect provider_error_code and http_status

The bucket set is locked. New providers map into the existing buckets; a new bucket is never added without a recorded decision.

The four wire fields

Classification rides on four fields attached to every log record. All four are nullable so that older daemons (pre-v0.3.0) and any not-yet-classified provider continue to work without changes.

FieldTypeNullableNotes
error_classstring(32)yesOne of the seven buckets above, or any future string
provider_error_codestring(64)yesNative provider code, e.g. overloaded_error, FAILED_PRECONDITION
http_statusint (smallint)yesHTTP status the provider returned, e.g. 200, 429, 529
retryableboolyesSet independently of the bucket. See the per-provider tables.

Forward-compatibility

  • error_class has no CHECK constraint and no enum at the storage layer. Any string is accepted.
  • Consumers tolerate unknown error_class strings — any unrecognised bucket is treated as unknown and rendered generically. A new bucket can be introduced by the daemon without a schema migration.
  • The wire schema ignores unknown top-level fields, so a future daemon field never causes rejection on sync. A missing required field or a type mismatch still rejects (HTTP 422) when syncing to the cloud; the four classification fields never cause rejection.

Two judgement calls worth understanding

Bucketing is not a mechanical HTTP-status lookup. Two cases are bucketed by what an operator should do, not by the HTTP code the provider returned.

Anthropic HTTP 529 → server_error, not rate_limit

HTTP 529 (overloaded_error) is Anthropic’s non-standard signal that the provider is currently overloaded. The instinct is to treat it like 429 (rate limit), but a 529 is not a per-key throttle — it is a provider-availability event. The right operator action is “wait it out and retry”, not “investigate your caller’s request rate”. Bucketing 529 as server_error puts it alongside 500 / 503 in the provider-health view, where it belongs, rather than the developer-behaviour view. retryable=true is set so it stays distinguishable from a hard 5xx.

OpenAI HTTP 429 insufficient_quotaauth, not rate_limit

OpenAI overloads HTTP 429 with two semantically different conditions:

  • rate_limit_error — an RPM / TPM throttle. Back off and retry. Bucket rate_limit, retryable.
  • insufficient_quota — billing balance exhausted. No amount of retry fixes it. Bucket auth, not retryable.

These share an HTTP status and look identical to a naive classifier, but the operator action is completely different. insufficient_quota belongs in the same bucket as a missing or invalid API key: someone needs to log in to the provider console and fix something, not change the retry strategy.

Where classified errors appear

Classified errors land in your local SQLite store (~/.halton-meter/db.sqlite) on the four fields above, and surface in the bundled dashboard alongside captured-cost rows. halton-meter report slices the same store from the terminal.

Compatibility with older daemons

Per-provider mapping

The exact status → bucket → retryable table for each provider lives on its page:

What’s next

  • Proxy model — where in the request path the classifier sits
  • Fail-open behaviour — a network-level daemon outage is not a provider error; it never produces an error_class row