Halton Meter Docs
Docs /Concepts /Error classification
CONCEPTS · 06 Stable

Error classification

How the Halton Meter daemon normalises every provider's error vocabulary into ten local classes that collapse to seven wire buckets, the fields that carry the classification, and where classified errors appear locally.

macOS · Linux · Windows ·5 min read ·Updated Jun 8, 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 ten local classes that collapse to seven wire 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 wire 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 wire bucket set is locked. New providers map into the existing buckets; a new bucket is never added without a recorded decision.

Ten local classes, seven wire buckets

The seven buckets above are the cloud wire contract. The daemon's local enum (ERROR_CLASS_VALUES in adapters/base.py) is richer: ten values. It is the seven wire buckets plus three locally-distinct classes that the serialiser collapses on the way to the cloud (ERROR_CLASS_LOCAL_TO_WIRE):

Local classCollapses to wire bucket
permissionauth
quota_exceededauth
context_length_exceededbad_request

Your local SQLite store keeps the ten-value class. The cloud sees only the collapsed seven.

The classification fields

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

FieldTypeNullableWireNotes
error_classstring(32)yesyesOne of the seven wire buckets, or any future string
provider_error_codestring(64)yesyesNative provider code, e.g. overloaded_error, FAILED_PRECONDITION
http_statusint (smallint)yesyesHTTP status the provider returned, e.g. 200, 429, 529
retryableboolyesyesAdded in v0.3.0 Phase 2. Set independently of the bucket. See the per-provider tables.
error_message_hashstring(64)yesnoLocal-only. Never serialised to the wire.

Five taxonomy columns live on the local requests row. Four ride the wire to the cloud; error_message_hash is local-only and is never sent.

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 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 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