Part of: Monitoring Idempotency Metrics & Dashboards
Raw metrics tell you a number changed; a service-level objective (SLO) tells you whether that change matters enough to wake someone up. This runbook defines a dedup-correctness SLI, sets an error budget, and writes multi-window burn-rate alerting rules so pages fire fast for severe outages and stay quiet during brief, self-resolving blips. It builds directly on the metrics taxonomy and the operational context in Observability & Operations for Idempotent Systems — read those first if the metric names below are unfamiliar.
Problem statement and prerequisites
What you are building: a formal SLI/SLO pair for deduplication correctness, an error-budget calculation, and a set of Prometheus alerting rules that page on fast budget burn and file a lower-urgency ticket on slow burn.
Prerequisites:
- The counters from monitoring idempotency metrics and dashboards — specifically
idempotency_requests_totalanddedup_conflict_total— are already emitting. - You understand fencing tokens and lease expiry from lock timeout and lease management, since a stale-lock incident is one of the concrete events this SLI must catch.
- A Prometheus Alertmanager instance with a PagerDuty receiver configured, or an equivalent routing layer.
Step-by-step implementation
Step 1 — Define the dedup-correctness SLI
The SLI is the proportion of requests that received a correct idempotent outcome: either a genuine cache hit returning the original recorded response, or a genuine miss processed exactly once. It excludes legitimate, correctly-resolved conflicts (two concurrent retries where one correctly waits and receives the other’s result) but counts as bad events: duplicate processing, a stale-fencing-token write that was not rejected, or a reconciliation record left stuck past its deadline.
SLI = 1 - (duplicate_processing_events + unrejected_stale_writes) / total_requests
Instrument the numerator as its own counter rather than deriving it after the fact from logs — deriving SLIs retroactively from unstructured logs is unreliable during an incident when you need the number immediately:
# Python: increment the bad-event counter at the one place duplicate processing can be detected
from prometheus_client import Counter
dedup_correctness_bad_events_total = Counter(
"dedup_correctness_bad_events_total",
"Requests that violated the deduplication correctness guarantee",
["reason", "route"], # reason: "duplicate_processed" | "stale_write_accepted" | "reconciliation_timeout"
)
def record_bad_event(reason: str, route: str):
dedup_correctness_bad_events_total.labels(reason=reason, route=route).inc()
Step 2 — Set the SLO target and compute the error budget
For a payment or fintech write path, a 99.95% correctness target is a reasonable starting point — tighter than a general availability SLO because a single bad event has outsized cost (a double charge, a duplicate shipment). Compute the budget explicitly rather than reasoning about the percentage alone:
SLO target: 99.95%
Error budget: 0.05%
Window: 30 days
Total requests/mo: ~130,000,000 (assume 50 req/s average)
Budget in requests: 130,000,000 × 0.0005 = 65,000 bad events allowed per month
Budget in time (if fully unavailable): 30 days × 0.0005 = 21.6 minutes/month
Track budget consumption as a Prometheus recording rule so it is a first-class queryable signal, not a spreadsheet calculation redone by hand during an incident:
# recording_rules.yml
groups:
- name: dedup-slo
interval: 30s
rules:
- record: dedup:correctness:sli_ratio
expr: >
1 - (
sum(rate(dedup_correctness_bad_events_total[5m]))
/
sum(rate(idempotency_requests_total[5m]))
)
- record: dedup:correctness:error_budget_remaining_ratio
expr: >
1 - (
(1 - dedup:correctness:sli_ratio) / 0.0005
)
Step 3 — Write multi-window, multi-burn-rate alerting rules
A single fixed threshold either pages too late on a severe outage or too often on noise. The standard pattern (as used for Google SRE-style SLOs) requires the burn rate to exceed a multiple of the sustainable rate across two windows simultaneously — a short window for speed, a long window to avoid paging on transient spikes:
# alert_rules.yml — multi-window burn-rate alerts for dedup correctness SLO (99.95% / 30d)
groups:
- name: dedup-slo-burn-rate
rules:
# Fast burn: would exhaust the 30-day budget in ~2 hours if sustained — page immediately
- alert: DedupCorrectnessFastBurn
expr: >
(
dedup:correctness:sli_ratio5m < (1 - 14.4 * 0.0005)
and
dedup:correctness:sli_ratio1h < (1 - 14.4 * 0.0005)
)
for: 2m
labels:
severity: page
team: payments-platform
annotations:
summary: "Dedup correctness SLO burning at 14.4x — budget exhausted in ~2h if sustained"
runbook: "https://distributedrequest.com/observability-operations-for-idempotent-systems/monitoring-idempotency-metrics-and-dashboards/defining-slos-and-alerts-for-deduplication-failures/"
# Slow burn: would exhaust the budget in ~5 days if sustained — ticket, not a page
- alert: DedupCorrectnessSlowBurn
expr: >
(
dedup:correctness:sli_ratio1h < (1 - 6 * 0.0005)
and
dedup:correctness:sli_ratio6h < (1 - 6 * 0.0005)
)
for: 15m
labels:
severity: ticket
team: payments-platform
annotations:
summary: "Dedup correctness SLO burning at 6x — budget exhausted in ~5d if sustained"
The 5m/1h/6h ratio series referenced above are additional recording rules using the same dedup:correctness:sli_ratio expression with different rate() windows — add sli_ratio5m, sli_ratio1h, and sli_ratio6h as separate recorded names so the alert expressions can reference each window independently:
- record: dedup:correctness:sli_ratio5m
expr: 1 - (sum(rate(dedup_correctness_bad_events_total[5m])) / sum(rate(idempotency_requests_total[5m])))
- record: dedup:correctness:sli_ratio1h
expr: 1 - (sum(rate(dedup_correctness_bad_events_total[1h])) / sum(rate(idempotency_requests_total[1h])))
- record: dedup:correctness:sli_ratio6h
expr: 1 - (sum(rate(dedup_correctness_bad_events_total[6h])) / sum(rate(idempotency_requests_total[6h])))
Burn-rate multiplier reference
The 14.4 and 6 multipliers used above aren’t arbitrary — they come from how much of the monthly budget a sustained burn at that rate would consume within a given detection window. Use this table when tuning thresholds for a different SLO target or window:
| Burn Rate Multiplier | Budget Consumed in 1 Hour | Time to Exhaust 30-Day Budget | Typical Severity |
|---|---|---|---|
14.4x |
~2% | ~2 hours | Page immediately (for: 2m) |
6x |
~0.8% | ~5 days | Ticket, review within a business day (for: 15m) |
3x |
~0.4% | ~10 days | Ticket, review within the week |
1x (sustainable rate) |
~0.14% | 30 days (exactly the budget) | No alert — this is the target burn rate |
Higher multipliers require shorter detection windows because they represent genuinely severe degradations that must page fast; lower multipliers can tolerate longer windows because a slow, sustained burn is still recoverable if caught within days rather than minutes. Recalculate this table whenever the SLO target or the measurement window changes — a 99.9% target over a 90-day window produces different absolute budgets even though the multipliers stay the same.
Step 4 — Route fast-burn pages to PagerDuty
Alertmanager routes on the severity label set above, sending page straight to PagerDuty with no delay and ticket to a lower-urgency channel:
# alertmanager.yml
route:
receiver: default-ticket
group_by: ["alertname", "team"]
routes:
- match:
severity: page
receiver: pagerduty-payments
group_wait: 10s
repeat_interval: 15m
- match:
severity: ticket
receiver: jira-payments-backlog
repeat_interval: 24h
receivers:
- name: pagerduty-payments
pagerduty_configs:
- service_key: "$PAGERDUTY_INTEGRATION_KEY"
severity: critical
description: '{{ .CommonAnnotations.summary }}'
- name: jira-payments-backlog
webhook_configs:
- url: "https://jira-webhook-bridge.internal/dedup-slo"
Step 5 — Codify the runbook link in the alert
Every page must carry a direct link to remediation steps so the responding engineer doesn’t start from zero. The runbook annotation above points back to this page; extend it in your real alert to also link the stale lock handling runbook when the bad-event reason label is stale_write_accepted.
Verification and testing
Confirm the SLI recording rules evaluate correctly
curl -s 'localhost:9090/api/v1/query?query=dedup:correctness:sli_ratio' | jq '.data.result'
# Expect a value very close to 1 (e.g. 0.9998) under normal operation
Inject synthetic bad events and confirm the fast-burn alert fires
# Simulate 200 duplicate-processing events in a short window to trigger a 14.4x burn
for i in $(seq 1 200); do
curl -s -X POST localhost:9091/metrics/job/synthetic \
--data-binary 'dedup_correctness_bad_events_total{reason="duplicate_processed",route="/v1/payments"} 1'
done
# Wait for the 2m "for" duration, then check Alertmanager
curl -s localhost:9093/api/v2/alerts | jq '.[] | select(.labels.alertname=="DedupCorrectnessFastBurn")'
Confirm error budget consumption is queryable
curl -s 'localhost:9090/api/v1/query?query=dedup:correctness:error_budget_remaining_ratio' | jq '.data.result[0].value[1]'
# A value near 1.0 means most of the monthly budget remains; near 0 means it is nearly exhausted
Confirm PagerDuty receives the fast-burn page
curl -s -H "Authorization: Token token=$PD_API_TOKEN" \
"https://api.pagerduty.com/incidents?service_ids[]=$PD_SERVICE_ID" | jq '.incidents[0].title'
Failure scenarios and debugging
| Failure Scenario | Remediation Steps | Observability Hooks |
|---|---|---|
| Fast-burn alert never fires despite a real outage | Confirm both the 5m and 1h windows are wired into the and expression — a single-window rule pages too early on noise, but a misconfigured dual-window rule can also silently require both to breach simultaneously when only one has enough data yet |
dedup:correctness:sli_ratio5m and sli_ratio1h both queried independently in Prometheus to confirm they carry data |
| Alert flaps between firing and resolved every few minutes | The for duration is too short relative to the natural noise in the ratio at low traffic volumes; increase for or widen the rate window so small sample sizes don’t cross the threshold repeatedly |
ALERTS metric transition count; Alertmanager notification log showing repeated fire/resolve pairs |
| Error budget appears exhausted but no user-facing incident occurred | The bad-event counter is being incremented by a false positive in the detection logic itself (e.g. a reconciliation job flagging records that actually completed) — audit the reason label breakdown before assuming a real correctness violation |
dedup_correctness_bad_events_total broken down by (reason); cross-reference with the reconciliation backlog gauge |
| PagerDuty receives duplicate pages for the same underlying incident | group_by in Alertmanager doesn’t include a dimension that differs across replicas firing the same rule; add the relevant label (e.g. route) to group_by or confirm repeat_interval is set high enough to avoid re-notification storms |
Alertmanager /api/v2/alerts/groups endpoint; PagerDuty incident dedup key matching the Alertmanager group_key |
| Slow-burn ticket never files even during a genuine multi-day degradation | Confirm the 6h window recording rule has sufficient retention and isn’t being dropped by a retention policy shorter than 6 hours; verify the ticket receiver webhook is reachable and not silently failing |
alertmanager_notifications_failed_total counter; webhook endpoint health check separate from the alerting pipeline |
SRE / observability checklist
dedup_correctness_bad_events_total{reason,route}— Counter. The single most important metric on this page; confirm it increments at every place duplicate processing, stale-write acceptance, or reconciliation timeout can occur, not just one of them.dedup:correctness:sli_ratio5m/sli_ratio1h/sli_ratio6h— recording-rule Gauges powering the burn-rate windows; alert on any of them reportingNoDataas if it were a budget breach.dedup:correctness:error_budget_remaining_ratio— Gauge surfaced on a leadership-visible dashboard; treat a value below0.2(20% of monthly budget remaining) as a trigger to freeze risky deploys to the dedup layer.ALERTS{alertname="DedupCorrectnessFastBurn"}— track fire/resolve transitions over time to catch flapping before it causes alert fatigue.- PagerDuty incident annotations — confirm every fired alert carries the
runbooklink and areason-specific remediation pointer, not a generic “investigate dashboards” message. - Alertmanager
alertmanager_notifications_failed_total— a failure in the paging pipeline itself is worse than a missed threshold; alert on this independently of the SLO burn-rate alerts.
Related
- Monitoring Idempotency Metrics & Dashboards — parent page defining the underlying metrics taxonomy this SLI is built from
- Building an Idempotency Hit-Rate Dashboard in Grafana — the companion runbook for the hit-rate panel referenced alongside this SLO
- Handling Stale Locks in Distributed Systems — the remediation runbook linked from the
stale_write_acceptedbad-event reason - Lock Timeout & Lease Management — the fencing-token mechanism this SLI’s
stale_write_acceptedreason is designed to catch - Observability & Operations for Idempotent Systems — the broader operational section covering tracing and chaos engineering alongside SLO alerting