oto_jobs_failed_total
| Type | counter |
| Labels | kind, queue, class — the §G.6 taxonomy: retryable, rate_limited, permanent, config_invalid, auth_expired |
| Registered in | internal/platform/jobs/metrics.go |
| Alertable | yes, on a sustained rate |
| Rule | OtoJobsFailing in deploy/prometheus/oto-rules.yaml |
What it counts
Section titled “What it counts”Job executions that returned an error. One job can increment this many times — once per failed
attempt — so this is a rate, not a population. A snooze is not a failure and is not counted here
(see oto_jobs_snoozed_total); the whole point is that a busy thread
must not look like a broken one.
class is the same closed set stored in notification_deliveries.error_class, so an operator
reading a dead delivery and an operator reading a dead job are reading the same word.
What a sustained rate means
Section titled “What a sustained rate means”Retries are currently absorbing a real problem. It is a leading indicator: at the ceiling — 13
attempts for retryable, 21 for rate_limited — these become entries on
oto_jobs_dead_total, and that is unrecoverable work.
The terminal classes (permanent, config_invalid, auth_expired) never retry, so for those this
counter and the dead counter move together, once each.
What to check
Section titled “What to check”- Split by
classfirst.retryableclimbing is a dependency;auth_expiredis a credential;config_invalidis a configuration — or, for a render error, an oto bug. - Split by
kind.deliver.dispatchis Slack or a webhook;ingest.process_batchis the database;source.reconcileis Alertmanager. - The per-attempt error in the job logs, keyed by
job_id. - For delivery kinds: the channel’s health, and its banner in the UI.
What to do
Section titled “What to do”retryable— fix the dependency. Watch the ratiofailed / started: below the ceiling nothing is lost yet.rate_limited— oto already honoursRetry-Afteras a snooze rather than an attempt, so seeing this class fail rather than snooze means the provider is refusing beyond its own advice. Reduce fan-out (fewer channels per policy) before raising limits.config_invalid/auth_expired— go tooto_jobs_dead_total; the response is the same and it is already terminal.