Skip to content
Menu

Documentation

Service level objectives

Sandloom exports the metrics an SLO needs; it does not choose your objectives. This page defines SLIs on the shipped metrics, gives example SLOs with starting targets measured on a small stand, and a template for the error budget and its policy. The SLIs are Prometheus recording rules in deploy/observability/prometheus/rules/sandloom-slo.yml; the Sandloom / SLOs Grafana dashboard plots their budgets and burn rates.

Examples, not promises

Every target below is an example. It comes from one qualification run of a 2 vCPU monolith (see Where the numbers come from), not from your hardware, traffic mix or users' expectations. Start with them, measure for a few weeks, then set your own.

SLIs

An SLI is the share of good events among valid events. The recording rules store the bad share (1 - good/valid) per job, over 5m, 30m, 1h and 6h windows, as sandloom:sli_<name>:ratio_rate<window>.

SLI Valid events Bad events Recording rule
API read availability sandloom_http_requests_total, method GET/HEAD status 5xx sandloom:sli_read_errors:ratio_rate*
API write availability method POST/PUT/PATCH/DELETE status 5xx sandloom:sli_write_errors:ratio_rate*
API read latency sandloom_http_request_duration_seconds, reads slower than 250 ms sandloom:sli_read_slow:ratio_rate*
API write latency writes slower than 500 ms sandloom:sli_write_slow:ratio_rate*
Login availability POST /auth/login status 5xx sandloom:sli_login_errors:ratio_rate*
Login latency POST /auth/login slower than 2.5 s sandloom:sli_login_slow:ratio_rate*
Event delivery sandloom_event_end_to_end_latency_seconds outbox commit → consumer commit slower than 5 s sandloom:sli_event_slow:ratio_rate*
Outbox freshness every rule evaluation (30 s) oldest pending outbox row ≥ 10 s sandloom:sli_outbox_fresh:bool (1 = good)

Definitions and their limits:

  • Probes are not traffic. /health*, /ready, /metrics and unmatched paths (<unmatched>, i.e. 404 scans) are excluded.
  • 4xx are good events. 401, 403, 404, 409, 422 and 429 are the API working as designed. A deployment that wants "rate limited" to count against it adds status="429" to the bad set.
  • Reads vs writes is the HTTP method. Routes carry no class label (route policy is derived from the method, the authenticator and the declared risk; none of it is a metric label). For an SLO on one route (or a group of routes), add a path=~"…" matcher to the rules.
  • The latency histogram has no status label: a fast 500 counts as fast. Read latency and availability together.
  • Thresholds must be bucket boundaries. The HTTP histogram uses prometheus_client's default buckets (… 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5 … seconds); the event histogram … 1, 2.5, 5, 10, 30, 60 …. Pick an SLO threshold from those, or the SLI is interpolated guesswork.
  • Server-side only. The app measures from its own socket: TLS, proxies, the gateway and the network are outside the SLI. An edge SLO needs the balancer's or a synthetic probe's metrics.
  • Event latency includes clock skew between the producer's database (emitted_at) and the consumer host. Keep NTP on both.
  • Consumer lag has no ratio SLI. Track it as a threshold instead: sandloom_streams_group_oldest_unacked_age_seconds (alert SandloomConsumerGroupBehind).

Example SLOs

For a 2 vCPU monolith (2 Gunicorn workers, PostgreSQL on 2 vCPU), measured over a rolling 30 days. "Load envelope" is the load under which the target was met on the stand — above it, expect to miss the latency SLO first.

SLO Target Load envelope (2 vCPU) Evidence on the stand
API read availability 99.9 % non-5xx — 0 errors in every valid read window
API read latency 99 % < 250 ms ≤ 500 reads/s fixed 500/s: p50 5 ms, p99 193–232 ms; 650/s: p99 621–854 ms
API write availability 99.9 % non-5xx — one baseline-build 200/s window answered 20 × 503 in 12 000 (0.17 %) — the budget is not theoretical
API write latency 99 % < 500 ms ≤ 200 writes/s fixed 200/s: p50 14–20 ms, p99 126–211 ms; 290/s: p99 409–634 ms
Login availability 99.9 % non-5xx — generic 401 for every wrong credential
Login latency 99 % < 2.5 s ≤ 5 logins/s 8 concurrent: ≈ 9.2 logins/s, p99 1.08–1.36 s (argon2id m=64 MiB, t=3, p=4)
Outbox freshness 99.9 % of evaluations < 10 s ≤ 200 events/s pending outbox rows peak ≈ 100 and return to 0 in every write-capacity window
Event delivery 99 % < 5 s end-to-end ≤ 200 events/s 200/s: p99 0.67–0.77 s (one baseline round 4.5 s); 300/s: p99 30–39 s — not sustainable
Consumer lag oldest unacked < 60 s, 99.9 % of the time ≤ 200 events/s lag returns to 0 after every window at 200/s

Why these numbers and not tighter ones:

  • Reads: p99 at 500/s sits at 190–230 ms, so 250 ms leaves little headroom; it is the tightest bucket boundary the stand met. Planned single-route reads and /auth/me saturate near 1 200/s, ordinary reads near 690/s — the envelope is ~70 % of the ordinary-read saturation point.
  • Writes: at 200/s the p99 is ≤ 211 ms; 500 ms absorbs bursts towards 290/s (where p99 already reaches 409–634 ms). Financial (F) writes saturate around 310–360/s, ordinary (W) around 470/s in closed loop.
  • Login is dominated by the password hash on purpose. Its capacity (≈ 9/s on 2 vCPU) is a planning number: see PASSWORD_HASH_PROFILE in docs/operate/operator-reference.md → Password hashing. Do not "fix" a login SLO by weakening the hash.
  • Events: the durable consumer's whole callback costs 13–15 ms under HTTP load; a pure drain without HTTP traffic reaches ≈ 510–520 events/s, but that is burst-drain capacity, not a sustained mixed rate. Between 200/s and 300/s the consumer group starts to fall behind — set the envelope below the rate where SandloomConsumerGroupBehind would fire, and scale consumers (more processes) before raising it.

Where the numbers come from

Oracle qualification of 2026-10-22 (f898857a vs b609b5ee), private evidence benchmarks/qualification/.state/oracle-main-20261022-f898857a/: four ARM Neoverse N1 vCPUs host, app limited to 2 vCPU / 1 GiB with two Gunicorn workers, PostgreSQL 16 on 2 vCPU / 1 GiB, separate cache, security and broker Redis (AOF appendfsync=always), pool 5 + 5 per worker, strict production RLS. The load generator ran on the same host without TLS, proxy or network in the path, so production latencies are higher by your edge's cost. Windows were 30–60 s and small in number; they establish orders of magnitude, not confidence intervals.

Error budget

The budget is what the SLO allows to go wrong: 1 − target of the valid events in the window.

SLO Target Window Budget In time (full outage) In events (at 100 req/s)
API read availability 99.9 % 30 d 0.1 % 43 min 259 200 requests
API write availability 99.9 % 30 d 0.1 % 43 min 259 200 requests
API read latency 99 % 30 d 1 % 7 h 12 min 2 592 000 requests
Event delivery 99 % 30 d 1 % — 1 % of events
your SLO

Burn rate = observed bad ratio ÷ budget. A burn rate of 1 spends exactly the budget over the window; 14.4 spends 2 % of a 30-day budget per hour (gone in ~2 days).

Burn-rate alerts

sandloom-slo.yml ships multi-window, multi-burn-rate alerts (Google SRE workbook) for read and write availability at the example 99.9 % target. A long window proves the burn is real; the short one makes the alert resolve quickly once it stops.

Alert Severity Burn rate Long / short window Budget spent when it fires
SandloomApi{Read,Write}AvailabilityBurnFast page > 14.4 1h / 5m 2 %
SandloomApi{Read,Write}AvailabilityBurnSlow warning > 6 6h / 30m 5 %

The latency, login and event SLIs are recorded but not alerted on by burn rate: their thresholds depend more on your hardware than availability does. Copy the availability pair, change the record name and budget (0.01 for a 99 % SLO), and add a minimum-traffic guard (and sum by (job) (rate(sandloom_http_requests_total[1h])) > 1) for low-traffic services, where a handful of errors is a large ratio.

Changing a target touches three places together: the alert expressions in sandloom-slo.yml, slo() in deploy/observability/build_dashboards.py (regenerate the dashboards) and the table above. CI checks the SLO board and the burn alerts agree on the budget.

Error-budget policy (template)

Agree on it before the budget runs out; adapt the thresholds.

Budget remaining (30 d) Response
> 50 % Ship normally. Spend budget on planned risk (migrations, upgrades).
25–50 % Ship, but no risky changes without a rollback plan; review the top error sources weekly.
0–25 % Freeze feature releases for the affected service; reliability work only.
exhausted Freeze until the 30-day window recovers above 0; postmortem for every incident that spent > 20 % of the budget.

Record for each SLO: the owner, the target and window, the dashboard link, the alerts that protect it, and the date it was last reviewed (quarterly, or after any capacity change).