On-Call That Doesn't Burn People Out
Most on-call rotations are designed for the system, not the humans. They optimize for coverage without considering what it costs the person holding the pager. I've been on rotations that were fine and rotations that made me dread Sundays. The difference isn't alert volume alone -- it's rotation design, escalation quality, and whether management treats on-call as real work or a side obligation.
Rotation Structures That Work
The standard one-week rotation is popular because it's simple. Also terrible for well-being if alert volume is anything above minimal. Seven consecutive nights of disrupted sleep potential. Even with only two pages per week, you sleep worse all seven nights because your brain won't fully relax knowing a phone call could come at any moment.
The Split Rotation
Split the 24-hour day into shifts. We use 12/12: daytime (8 AM to 8 PM) and overnight (8 PM to 8 AM). Different people hold each shift.
{
"schedule_layers": [
{
"name": "Day Shift",
"rotation_turn_length_seconds": 604800,
"restrictions": [{
"type": "daily_restriction",
"start_time_of_day": "08:00:00",
"duration_seconds": 43200
}],
"users": ["engineer_a", "engineer_b", "engineer_c", "engineer_d"]
},
{
"name": "Night Shift",
"rotation_turn_length_seconds": 604800,
"restrictions": [{
"type": "daily_restriction",
"start_time_of_day": "20:00:00",
"duration_seconds": 43200
}],
"users": ["engineer_e", "engineer_f", "engineer_g", "engineer_h"]
}
]
}
The overnight shift is shorter -- three or four nights, not seven. The person on overnight gets a compensating day off the next day. This isn't generosity; it's retention. Engineers who burn out on on-call leave. Replacing them costs six months of salary in recruiting and ramp-up. A day off costs nothing by comparison.
Follow-the-Sun for Distributed Teams
If your team spans time zones, follow-the-sun eliminates overnight pages entirely. The challenge is handoff -- each transition risks information loss about active incidents and flaky services.
We formalize handoffs with automated summaries that post to the team channel at every rotation boundary:
def generate_handoff(outgoing_region, incoming_region):
active = pagerduty.list_incidents(
statuses=["triggered", "acknowledged"]
)
recent = pagerduty.list_incidents(
since=datetime.now() - timedelta(hours=12),
statuses=["resolved"]
)
flaky = get_services_with_repeated_alerts(hours=24)
message = f"Handoff: {outgoing_region} -> {incoming_region}\n\n"
message += f"Active incidents: {len(active)}\n"
for inc in active:
message += f" [{inc['urgency']}] {inc['title']}\n"
message += f" Status: {inc['status']}, Owner: {inc['assignee']}\n"
message += f"\nResolved in last 12h: {len(recent)}\n"
if flaky:
message += f"\nFlaky services (multiple alerts in 24h):\n"
for svc in flaky:
message += f" {svc['name']}: {svc['alert_count']} alerts\n"
return message
The flaky services section is critical. Without it, the incoming engineer has no context about recurring issues and wastes time investigating known problems.
Escalation Policy Design
Escalation policies need different timings based on urgency. A customer-facing outage needs fast escalation because every minute costs money and trust. An internal tooling issue that doesn't affect customers can wait longer because the blast radius is contained.
High Urgency Escalation
Customer-facing outage: escalate after 5 minutes to secondary, 10 minutes to engineering lead, 15 minutes to VP. The VP coordinates communication and stakeholder management, not debugging.
Low Urgency Escalation
Internal tooling degradation: escalate after 30 minutes to secondary, 60 minutes to team lead. No further escalation -- if it hasn't been acknowledged in an hour, it probably can wait until morning.
{
"escalation_policy": {
"name": "API Team - High Urgency",
"escalation_rules": [
{
"escalation_delay_in_minutes": 5,
"targets": [{"type": "schedule_reference", "id": "PRIMARY_SCHEDULE"}]
},
{
"escalation_delay_in_minutes": 5,
"targets": [{"type": "schedule_reference", "id": "SECONDARY_SCHEDULE"}]
},
{
"escalation_delay_in_minutes": 5,
"targets": [{"type": "user_reference", "id": "ENG_LEAD"}]
}
],
"num_loops": 2
}
}
Alert Quality: The Most Impactful Factor
The single most impactful improvement for on-call quality isn't rotation design -- it's reducing alert noise. A well-designed rotation with 50 false positive alerts weekly is worse than a mediocre rotation with 5 real alerts. Alert fatigue doesn't just reduce response quality; it drives attrition.
Metrics to Track
Pages per shift: above 2 per overnight shift, burnout is inevitable. Above 5 per day shift, the engineer can't do meaningful project work. Track this weekly and hold service teams accountable for their alert volume.
Actionable rate: what percentage of alerts require human intervention? Below 70%, you have a noise problem. Below 50%, the on-call rotation is actively harmful because people stop trusting alerts.
Time to acknowledge: healthy teams acknowledge high-urgency alerts in under 5 minutes. Consistently over 15 minutes means either broken notification routing or alert fatigue from too much noise.
def generate_oncall_report(team, period_start, period_end):
incidents = pagerduty.list_incidents(
service_ids=team.service_ids,
since=period_start, until=period_end
)
overnight = [i for i in incidents
if parse_time(i["created_at"]).hour >= 20
or parse_time(i["created_at"]).hour < 8]
actionable = [i for i in incidents
if i.get("resolution_summary") != "auto-resolved"
and i.get("resolution_summary") != "false-positive"]
ack_times = []
for i in incidents:
if i.get("acknowledged_at"):
ack_delta = (parse_time(i["acknowledged_at"]) -
parse_time(i["created_at"])).total_seconds()
ack_times.append(ack_delta)
days = (period_end - period_start).days
return {
"total_pages": len(incidents),
"overnight_pages": len(overnight),
"overnight_per_night": round(len(overnight) / max(days, 1), 1),
"actionable_rate": f"{len(actionable)/max(len(incidents),1):.0%}",
"median_ack_seconds": sorted(ack_times)[len(ack_times)//2] if ack_times else None,
"p90_ack_seconds": sorted(ack_times)[int(len(ack_times)*0.9)] if len(ack_times) > 1 else None,
"pages_per_shift": round(len(incidents) / max(days * 2, 1), 1),
"repeat_offenders": get_top_alerting_services(incidents, top_n=5)
}
The Noise Budget
Set an explicit noise budget. Each service team is allowed a maximum number of false positive alerts per month. Exceeding the budget means the team's next sprint includes alert tuning as a priority item, not a backlog wish. We set ours at 10 false positives per service per month. Teams that exceed it three months running get their alert configurations reviewed by the platform team.
Compensation and Recognition
On-call work is real work. It constrains your personal life, disrupts sleep patterns, and creates ongoing cognitive load even when no alerts fire. The anxiety of knowing your phone might ring doesn't clock out when you do.
Our compensation model: each overnight shift earns a half-day of comp time. Each page between 10 PM and 6 AM earns an additional hour of comp time. Weekend days on-call earn a full comp day. Most engineers take Monday off after an on-call week, converting a five-day recovery into a three-day recovery.
Some companies pay a flat on-call stipend instead. Either approach works as long as it's explicit, consistent, and actually reflects the burden. What doesn't work is treating on-call as an unpaid obligation that comes with the job title. That breeds resentment, and resentful on-call engineers produce slower response times and worse incident outcomes.
The cost of comp time is trivial compared to replacing an experienced SRE who burned out because on-call was treated as free labor. Track your on-call attrition separately from general attrition. If people leave citing on-call as a factor, your rotation design needs work regardless of what the metrics say.