Defining SLAs for Spatial Data Products
A spatial data product without a published service-level agreement is a best-effort dataset wearing a product label. This page shows how to write measurable SLAs and SLOs for a spatial product — availability, freshness, query latency, positional accuracy, and an error budget — and where that SLA table lives. It operationalizes Product Thinking for GIS Datasets within Geospatial Data Mesh Fundamentals, and the numbers you set here become the runtime targets enforced by SLA Monitoring for Spatial Data Products. The SLA is not a wiki page: it is a machine-readable section of the Data Contracts for Spatial Products that the domain publishes, versions, and is paged against.
An SLA that lists five aspirational numbers with no error budget is unenforceable. The discipline is to pair every objective with a measurement window and a budget for how much it may be missed, so an alert fires on burn rate rather than on a single unlucky sample.
Figure — Spatial SLOs feed a shared 30-day error budget whose burn rate drives alerting.
Prerequisites
| Requirement | Version / assumption | Notes |
|---|---|---|
promtool |
>= 2.45 | Validates recording/alerting rules that measure each SLO |
| Data contract store | Git-backed, versioned | The SLA table is a section of the product’s contract, not a separate doc |
| Metrics backend | Prometheus-compatible | Exposes up, freshness lag, and request-latency histograms per product |
| CRS assumption | Positional accuracy stated at native resolution in EPSG:4326; UTM sources measured in EPSG:32633 metres |
Accuracy SLO must name the CRS it is measured in |
| Role | product-owner (edits SLA) + platform-sre (owns alert routing) |
Owner sets targets; SRE wires enforcement |
| Env | PRODUCT_ID, SLO_WINDOW=30d |
Window must match the error-budget arithmetic |
Step-by-Step Implementation
1. Write the SLA/SLO table into the data contract
Each objective needs an indicator (what is measured), an objective (the threshold), a window, and an error budget derived from the objective. Put it in the contract so it versions with the product:
# contract.yaml — sla section (lives inside the data contract)
product_id: flood-risk-tiles
version: v1.2.0-crs:EPSG:4326-res:10m
sla:
window: 30d
objectives:
- indicator: availability # successful tile reads / total
slo: 0.999 # 99.9% -> error budget 43m 12s / 30d
measured_by: probe_success_ratio
- indicator: freshness # now - source_commit_time
slo_max_staleness: 15m
measured_by: data_freshness_seconds
- indicator: query_p95_latency
slo_max: 400ms
measured_by: request_duration_seconds
- indicator: positional_accuracy
slo_max_error_m: 0.5 # metres at native res, EPSG:4326
measured_by: control_point_rmse
Validate that the objectives are internally consistent (an availability SLO must translate to a finite budget):
python3 -c "w=30*24*60; b=w*(1-0.999); print(f'error budget = {b:.1f} min / 30d')"
# expect: error budget = 43.2 min / 30d
2. Derive the error budget and wire a burn-rate check
Turn the availability SLO into an alerting rule that fires on budget burn, not on a single failed probe. This is the check that SLA Monitoring for Spatial Data Products will run in production:
# sla_rules.yaml
groups:
- name: flood-risk-tiles-slo
rules:
- record: job:probe_success:ratio_rate1h
expr: sum(rate(probe_success{product="flood-risk-tiles"}[1h]))
/ sum(rate(probe_total{product="flood-risk-tiles"}[1h]))
- alert: SpatialErrorBudgetBurn
# 14.4x burn = 2% of a 30d budget in 1h -> fast-burn page
expr: (1 - job:probe_success:ratio_rate1h) > (14.4 * (1 - 0.999))
for: 5m
labels: {severity: page, product: flood-risk-tiles}
annotations:
summary: "Availability budget burning >14.4x for flood-risk-tiles"
Verify the rules parse before committing — a malformed rule silently disables the SLO:
promtool check rules sla_rules.yaml
# SUCCESS: 2 rules found
3. Add a freshness and accuracy gate at publish
Availability and latency are measured at runtime, but freshness and positional accuracy are also gated at publish so a stale or drifted product never reaches consumers. Fail the publish when the staleness or RMSE budget is exceeded:
STALE=$(promtool query instant http://prometheus:9090 \
'time() - data_source_commit_timestamp{product="flood-risk-tiles"}' | awk '{print $2}')
test "${STALE%.*}" -le 900 && echo "FRESHNESS_OK" || { echo "STALE_BLOCK"; exit 1; }
# 900s == 15m budget; exceeding it blocks the publish, not just alerts
Route a breach to the owning tier: a freshness breach pages the domain owner, an accuracy regression blocks publish and quarantines the artifact, and an availability burn pages platform SRE. The versioning of these thresholds follows Versioning Spatial Data Contracts with SemVer — tightening an SLO is a minor contract change, loosening one is a major, consumer-breaking change.
Configuration Reference
| Metric | SLO | Window | Error budget |
|---|---|---|---|
| Availability (tile read success) | ≥ 99.9% | 30 d | 43m 12s / 30 d |
| Freshness (max staleness) | ≤ 15 min | rolling | 0 — hard gate at publish |
| Query p95 latency | ≤ 400 ms | 30 d rolling | ≤ 0.1% requests may exceed |
| Positional accuracy (RMSE) | ≤ 0.5 m at native res | per publish | 0 — hard gate at publish |
| CRS-contract conformance | 100% of publishes | per publish | 0 — reject at catalog ingest |
Common Failure Modes & Fixes
Symptom: the SLA looks healthy on the dashboard but consumers still complain about outages.
Root cause: availability is measured from inside the Kubernetes cluster, not from the consumer’s ingress path, so gateway and DNS failures are invisible.
Fix: run the availability probe from the consumer edge (through the API gateway) and record probe_success there, so the SLO reflects what consumers actually experience.
Symptom: the burn-rate alert flaps on and off every few minutes.
Root cause: the alert is evaluated on a raw ratio with no for: clause, so a single failed scrape trips it.
Fix: keep the for: 5m dwell and use a multi-window burn rate (fast 1h + slow 6h) so only a sustained burn pages, as shown in step 2.
Symptom: the positional-accuracy SLO passes but downstream analysis is misaligned.
Root cause: RMSE was measured in the source UTM CRS while the product is served in EPSG:4326, so the metre error does not correspond to the delivered geometry.
Fix: measure control-point RMSE in the CRS the product is published in, and state that CRS explicitly in the SLO row.
Symptom: freshness “passes” yet the data is hours old.
Root cause: the freshness metric measures pipeline completion time, not the age of the underlying source observation.
Fix: compute staleness as now - source_commit_timestamp, not now - last_pipeline_run, so latency in the source is counted against the budget.
FAQ
Where should the SLA table actually live?
Inside the product’s data contract, version-controlled alongside the schema and CRS declaration. A separate wiki drifts from reality; a contract-embedded SLA versions with the product and can be validated in CI, so a consumer that pins a contract version also pins the guarantees it was promised.
How do I choose between a 99.9% and a 99.95% availability SLO?
Work backwards from the error budget the owning team can realistically defend: 99.9% permits 43m 12s of downtime per 30 days, 99.95% only 21m 36s. Pick the loosest SLO consumers will accept, because a tighter number you cannot meet burns trust faster than an honest one.
Should freshness have an error budget like availability does?
Usually not — freshness is better modelled as a hard publish gate than a burnable budget, because serving stale spatial data is a correctness problem, not a reliability one. Block the publish when staleness exceeds the objective rather than spending a budget against it.
What makes positional accuracy an SLA concern rather than a QA step?
Because consumers make decisions with the coordinates, an accuracy regression is a broken promise, not an internal defect. Stating RMSE at a named resolution and CRS in the SLA lets a consumer decide whether 0.5 m is fit for their use, and lets the owner block a publish that regresses against the baseline.
Related
- Product Thinking for GIS Datasets — the parent topic where a dataset becomes a contracted product.
- SLA Monitoring for Spatial Data Products — the runtime layer that enforces the objectives defined here.
- Data Contracts for Spatial Products — the document the SLA table is embedded in.
- Versioning Spatial Data Contracts with SemVer — how tightening or loosening an SLO maps to a version bump.
- Data Mesh vs Traditional GIS Architecture — where the SLA baselines replace unmeasured best effort.