On-Call Health
On-call only works long-term when rotations are fair, alerts are actionable, and recovery time is protected. Python fleets drown in noise when every Celery retry and Gunicorn worker restart pages someone.
Search across all documentation pages
On-call only works long-term when rotations are fair, alerts are actionable, and recovery time is protected. Python fleets drown in noise when every Celery retry and Gunicorn worker restart pages someone.
Quick-reference recipe card - copy-paste ready.
## Alert hygiene checklist (monthly)
- [ ] Each firing alert has runbook URL in annotation
- [ ] No alert fired >3x last month without ticket
- [ ] SLO-based burn alerts replace static thresholds where possible
- [ ] Business-hours vs wake-up severity separated
- [ ] Pages per on-call shift tracked in dashboard# Prefer SLO burn over raw 5xx
- alert: CheckoutErrorBudgetBurn
expr: slo_error_budget_burn_rate{service="orders-api"} > 1
for: 10m
labels:
severity: page
annotations:
runbook: https://github.com/acme/runbooks/orders-api.mdWhen to reach for this:
"""on_call_metrics.py - simple pager load tracker for retros."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import date
@dataclass
class ShiftStats:
engineer: str
week: date
pages: int
actionable: int # led to customer-impacting work
false_positive: int
def health_score(stats: ShiftStats) -> str:
if stats.pages == 0:
return "quiet"
action_rate = stats.actionable / stats.pages
if stats.pages > 10 and action_rate < 0.3:
return "noisy_rotation"
if stats.pages > 15:
return "overloaded"
return "healthy"
def main() -> None:
sample = ShiftStats("ana", date(2026, 7, 7), pages=12, actionable=4, false_positive=8)
print(sample.engineer, health_score(sample))
if __name__ == "__main__":
main()What this demonstrates:
| Rule | Guideline |
|---|---|
| Shift length | 1 week primary, 1 week secondary |
| Handoff | 15-min overlap, open incidents list |
| Max pages/shift | Investigate if >10 actionable or >20 total |
| Shadow | New hire shadows 2 shifts before solo |
| Escalation | Secondary after 15m SEV1 no IC |
| Source | Fix |
|---|---|
| Celery retry storm | Alert on queue age, not each task failure |
| Worker OOM restart | Memory leak guardrail + restart rate alert |
| Gunicorn worker timeout | Separate alert for sustained p99 |
| Deprecation warnings | Log metric, do not page |
for: duration.| Alternative | Use When | Don't Use When |
|---|---|---|
| Dedicated SRE on-call | Large multi-service fleet | Small product team |
| Follow-the-sun | Global customer base | Single-region MVP |
| Managed on-call vendor | Need 24/7 immediately | Strong in-house ops culture |
| No on-call (business hours) | Internal tools only | Revenue-critical API |
Investigate when total pages exceed 10 per shift or actionable rate drops below 30%.
Ticket only unless staging mirrors prod SLO drill. Never wake for non-prod by default.
Shadow shifts, read runbooks, simulate incident in game day, pair first real page.
Batch failures ticket; page only if blocking production inference or SLA dataset.
Tech leads should periodically to feel pain and prioritize alert hygiene.
Trade shifts in calendar tool; never leave gap. Secondary auto-escalates.
Yes for log digests; human still owns decision record in ticket.
Pages per engineer, MTTR, false-positive rate, open guardrails from post-mortems.
Tune Gunicorn timeout alerts, Celery queue SLOs, and separate infra from app burn rates.
No - shared ownership. Seniors mentor juniors during shadow shifts.
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.
Reviewed by Chris St. John·Last updated Jul 16, 2026