An Error-Budget Policy for Tile Pipelines
Name a consequence that happens automatically, an owner who can enact it without escalating, and the exemptions — all before the budget is spent, because an exemption invented during an incident is indistinguishable from ignoring the policy.
This guide sits under SLOs & Alerting for Spatial Webhook Pipelines, within Monitoring & Observability for Spatial Pipelines. It is the step after the slow-burn ticket from Alert Rules That Survive a Bursty Spatial Stream has been filed and ignored twice.
When to use this pattern
- An objective exists and is missed regularly with no consequence, which is the state that makes objectives decorative.
- The tile pipeline competes for capacity with feature work, and the competition is settled by whoever asks most recently.
- Overload currently produces an undirected backlog, so which tiles go stale is decided by queue order rather than by anyone.
A policy is a decision made early
Complete runnable implementation
The policy is a document, and the code is what makes it self-enforcing rather than aspirational.
from dataclasses import dataclass
from enum import IntEnum
class BudgetState(IntEnum):
HEALTHY = 0 # > 50% remaining
WATCH = 1 # 25-50% — reliability work is prioritised
FROZEN = 2 # < 25% — non-urgent pipeline changes blocked
EXHAUSTED = 3 # 0% — freeze plus mandatory review
# Written in advance. An exemption invented during an incident is
# indistinguishable from ignoring the policy.
EXEMPT_CHANGE_TYPES = frozenset({
"security-fix",
"data-loss-fix",
"reliability", # the work that would restore the budget
"rollback",
})
FREEZE_ESCALATION_DAYS = 10
@dataclass(frozen=True, slots=True)
class Policy:
service: str
owner_role: str # who can declare it, without escalating
escalates_to: str # who decides after FREEZE_ESCALATION_DAYS
recover_to: float = 0.25 # freeze lifts here, NOT at 0% — otherwise
# the pipeline oscillates in and out of freeze
TILE_POLICY = Policy(
service="tile-pipeline",
owner_role="tile-pipeline on-call",
escalates_to="platform engineering manager",
)
def state_for(budget_remaining: float) -> BudgetState:
if budget_remaining > 0.50:
return BudgetState.HEALTHY
if budget_remaining > 0.25:
return BudgetState.WATCH
if budget_remaining > 0.0:
return BudgetState.FROZEN
return BudgetState.EXHAUSTED
def may_deploy(change_type: str, budget_remaining: float,
frozen_for_days: float = 0.0) -> tuple[bool, str]:
"""Called by CI. A policy nothing enforces is a preference."""
state = state_for(budget_remaining)
if state <= BudgetState.WATCH:
return True, "budget healthy"
if change_type in EXEMPT_CHANGE_TYPES:
return True, f"{change_type} is exempt under the policy"
if frozen_for_days > FREEZE_ESCALATION_DAYS:
return False, (
f"frozen {frozen_for_days:.0f} days — {TILE_POLICY.escalates_to} must "
"either extend the freeze or accept the risk explicitly"
)
return False, (
f"tile-pipeline budget at {budget_remaining:.0%}; non-urgent changes are "
f"frozen until it recovers above {TILE_POLICY.recover_to:.0%}"
)
Lifting the freeze at 25% rather than at 0% is the same hysteresis that keeps a paused partition from oscillating: recovering to exactly the threshold means the next bad hour re-freezes, and a freeze that toggles daily stops being taken seriously.
The degradation ladder
The other half of the policy decides what the pipeline does under overload, rather than leaving it to queue order.
# Rebuild priority under pressure. Lower value = rebuilt first.
ZOOM_PRIORITY = {z: (0 if z <= 9 else 1 if z <= 14 else 2) for z in range(0, 23)}
# Maximum acceptable lag per band while the pipeline is under pressure.
DEGRADED_LAG_SECONDS = {0: 60, 1: 900, 2: 14_400}
def rebuild_order(tiles):
"""Shed from the bottom of the ladder upwards.
Sorting by (band, age) means a shallow tile is always rebuilt before a
deep one, and within a band the oldest goes first.
"""
return sorted(tiles, key=lambda t: (ZOOM_PRIORITY[t.z], -t.age_seconds))
Parameter reference
| Name | Type | Spatial constraint | Default |
|---|---|---|---|
recover_to |
float |
Freeze lifts here, not at 0%, or it toggles daily | 0.25 |
EXEMPT_CHANGE_TYPES |
frozenset | Written in advance; additions are a policy change, not a judgement call | 4 types |
FREEZE_ESCALATION_DAYS |
int |
After this, a named person must extend or accept the risk | 10 |
ZOOM_PRIORITY |
dict | Shallow zooms first — the inverse of what queue order produces | 3 bands |
DEGRADED_LAG_SECONDS |
dict | Per band, agreed with whoever consumes the tiles | — |
owner_role |
str |
Someone who can declare the freeze without a meeting | on-call |
Gotchas and spatial edge cases
-
A policy nothing enforces is a preference. The
may_deploycheck has to run in CI and block the pipeline, not print a warning. A policy enforced by everyone remembering it is enforced during calm periods and forgotten during the ones it exists for. -
The exemption list must be short and hard to extend. Every plausible change can be argued into a category, and a list with a “business critical” entry has no entries at all. Adding one is a policy change with the same review as any other, which is what stops it being a judgement call at the moment of maximum pressure.
-
The degradation ladder must match how tiles are actually invalidated. If invalidation is scoped by zoom as Scoping Tile Invalidation to the Zoom Levels That Changed describes, most small edits never enqueue shallow tiles at all, and the ladder mainly governs the large edits — which is the right target.
-
On-demand rendering for deep zooms needs to exist before it is relied on. A ladder that allows z15 to lag four hours assumes something renders it when asked. If nothing does, the policy has quietly promised a capability the pipeline does not have.
-
The budget and the shed rate are the same currency. Load shedding, described in Shedding Spatial Load by Geographic Priority, spends the completeness budget. A pipeline shedding routinely while its freshness budget looks healthy has moved the problem rather than solved it.
-
Review the policy when it does not fire. A budget that has never been exhausted in a year means the objective is too loose, and the policy has been costless because it has never applied. That is worth noticing, because a policy that cannot bite provides the same information as no policy at all.
Verification
import pytest
def test_frozen_budget_blocks_a_feature_change():
allowed, reason = may_deploy("feature", budget_remaining=0.10)
assert not allowed and "frozen" in reason
def test_exempt_changes_pass_during_a_freeze():
for change_type in ("security-fix", "data-loss-fix", "reliability", "rollback"):
allowed, _ = may_deploy(change_type, budget_remaining=0.0)
assert allowed, f"{change_type} must remain deployable"
def test_freeze_lifts_with_hysteresis():
"""Lifting at 0% would re-freeze on the next bad hour."""
assert not may_deploy("feature", budget_remaining=0.20)[0]
assert may_deploy("feature", budget_remaining=0.30)[0]
def test_long_freeze_escalates_to_a_named_person():
_, reason = may_deploy("feature", budget_remaining=0.10, frozen_for_days=12)
assert "platform engineering manager" in reason
def test_shallow_tiles_are_rebuilt_before_deep_ones():
"""The inverse of what queue order produces."""
tiles = [_tile(z=18, age=3600), _tile(z=6, age=30), _tile(z=12, age=600)]
assert [t.z for t in rebuild_order(tiles)] == [6, 12, 18]
The last test is the one that catches a regression in the thing readers actually experience: the deep tile is by far the oldest, so any ordering by age alone puts it first, and that ordering is exactly what the ladder exists to override.
Related
- SLOs & Alerting for Spatial Webhook Pipelines — the topic this guide belongs to
- Defining a Freshness SLO for a Spatial Pipeline — the objective whose budget this policy governs
- Scoping Tile Invalidation to the Zoom Levels That Changed — reducing the rebuild volume the ladder has to ration
- Shedding Spatial Load by Geographic Priority — the same budget, spent at the consumer instead of the renderer