Filter concepts by levelShowing all levels.

Python · Section 42

Monitoring and Production Operations

Level
advanced
Read
170 min
Concepts
8

The operations layer built on top of metrics, logs, traces, and health checks (covered in depth in Logging and Observability): alerts and dashboards, the SLO/SLI/error-budget vocabulary for deciding when reliability is actually a problem, the incident-response lifecycle from detection through a blameless postmortem, and the two ways to recover — graceful degradation and rollbacks.

Python overview

What is true here

  1. An SLI is what you measure, an SLO is the target for it, and the error budget is how much the SLO can be missed before it is treated as a problem.
  2. Alert on symptoms a user would notice — elevated error rate, high latency — not on every internal metric crossing a threshold.
  3. Incident response follows a lifecycle: detect, mitigate, resolve, then a blameless postmortem whose goal is a corrected timeline and a fix.
  4. Graceful degradation serves a reduced but working experience when a dependency fails; a rollback reverts a bad deploy rather than fixing forward under pressure.
  5. A blameless postmortem exists because blame makes the next incident report less honest, not because nobody made a mistake.

What you will be able to do

  • Distinguish an SLI from an SLO from an error budget, and explain what spending the budget should trigger
  • Design an alert that fires on user-facing symptoms rather than noisy internal metrics
  • Build a dashboard that answers "is this service healthy right now" at a glance
  • Run an incident through detection, mitigation, and resolution, and write a blameless postmortem
  • Implement a graceful-degradation fallback for a failing dependency
  • Decide when to roll back a deploy versus fixing forward, and explain the tradeoff

The ops layer on top of observability

How metrics, logs, traces, and health checks — taught in full in Logging and Observability — feed alerts and dashboards.

Metrics, logs, and traces (the ops layer)

referenceintermediate

Metrics, logs, and traces are the three raw signals every other tool in this section is built on — alerts fire on metrics, dashboards chart metrics, and an incident responder reads logs and traces to find the broken component.

Think of it as

Think of this section as what a team builds ON TOP of metrics, logs, and traces, not a replacement for them — an alert is a rule watching a metric, a dashboard is metrics made visible, and an incident is worked by reading the logs and traces those three already produce.

Remember: This section builds alerts, dashboards, SLOs, and incident response ON TOP of metrics/logs/traces — see Logging and Observability for how to actually produce them in Python.

See also: logging levels and handlers · structured and json logging · metrics and distributed tracing · alerts and dashboards · slos slis and error budgets

Health checks (in the ops loop)

referenceintermediate

A health check is usually the first thing an on-call engineer opens when a dashboard turns red — GET /healthz or /readyz answering "is it up" before anyone reads a log line.

Think of it as

A health check is the ops equivalent of a pulse check at the start of triage — cheap, fast, and answered before anything deeper is investigated. See Logging and Observability for the liveness-vs-readiness distinction and the Python code behind it.

Remember: Health checks are the fastest first signal in this section's workflow — see Logging and Observability for the liveness/readiness mechanics.

See also: health readiness and liveness checks · slos slis and error budgets · incident response and root cause analysis

Alerts and dashboards

standardintermediate

An alert is a rule that pages a human when a metric crosses a threshold for long enough; a dashboard is the same metrics charted continuously so a human can look without waiting to be paged.

Think of it as

A dashboard is a window a human chooses to look through; an alert is a tripwire that taps them on the shoulder. A system with only dashboards relies on someone staring at them; a system with only alerts has no way to investigate once paged — production monitoring needs both.

text
# The shape shared by Prometheus Alertmanager, CloudWatch Alarms,
# Grafana alerting, and similar tools:
IF <metric> <comparison> <threshold> FOR <duration>
  THEN notify(<channel>)  # page, Slack, email — by severity

# Example rule, in words:
IF rate(http_requests_failed) / rate(http_requests_total) > 0.05
   FOR 5m
   THEN page(on-call)

What we're doing: Show the shape of an alert rule as executable Python — a threshold-over-a-window check a monitoring system runs on every new metric sample, independent of which vendor evaluates it.

alert_rule.pypython
def should_page(error_rate_samples: list[float], threshold: float = 0.05) -> bool:
    """True only if EVERY recent sample breached the threshold — avoids
    paging on one noisy data point, the same 'FOR 5m' idea alerting
    tools express as a rule."""
    if not error_rate_samples:
        return False
    return all(sample > threshold for sample in error_rate_samples)


noisy_spike = [0.01, 0.09, 0.02, 0.01]        # one bad sample among good ones
sustained_breach = [0.08, 0.07, 0.09, 0.11]   # every recent sample is bad

print("noisy spike pages:     ", should_page(noisy_spike))
print("sustained breach pages:", should_page(sustained_breach))
1
error_rate_samples is a sliding window of recent metric values — the "FOR 5m" duration an alert rule requires before it fires.
6
all(...) means one good sample in the window keeps the alert quiet — this is what filters a single blip from a real, sustained breach.
Output
noisy spike pages:      False
sustained breach pages: True

Why this works: A single elevated sample does not page — all() requires every sample in the window to have crossed the threshold, mirroring the "FOR 5m" duration clause every real alerting tool (Alertmanager, CloudWatch, Grafana) attaches to a threshold so a one-off blip cannot trigger a page.

Remember: Alert on symptoms over a time window, not single data points; a dashboard is for looking, an alert is for being interrupted — a healthy setup needs both, and too many low-value alerts trains people to ignore all of them.

See also: metrics logs and traces · slos slis and error budgets · incident response and root cause analysis

Advertisement

Reliability targets

The vocabulary for deciding, in numbers, when reliability is actually a problem worth stopping feature work for.

SLOs, SLIs, and error budgets

coreintermediate

An SLI is a measured number (like the fraction of requests that succeeded); an SLO is the target you set for that number (like 99.9%); the error budget is the allowed amount of failure left before the SLO is breached.

Think of it as

An SLO is a speed limit you set for yourself, the SLI is your speedometer reading right now, and the error budget is how many minutes you can still spend over the limit this month before it becomes a real problem. Spend the whole budget, and the team's own rule says stop shipping risky changes until it recovers.

python
# SLI: measure it
success_rate = successful_requests / total_requests

# SLO: the target you set for that SLI, over a window
slo_target = 0.999   # 99.9% over 30 days

# Error budget: what's left before the SLO is breached
error_budget = 1 - slo_target
budget_remaining = error_budget - (1 - success_rate)

What we're doing: Check a real SLI against an SLO and report how much error budget is left — the exact question "how will you know the service is broken" reduces to.

slo_check.pypython
def check_slo(successful: int, total: int, slo_target: float = 0.999) -> dict:
    sli = successful / total
    error_budget = 1 - slo_target        # e.g. 0.1% allowed to fail
    actual_failure = 1 - sli
    budget_remaining = error_budget - actual_failure
    return {
        "sli": round(sli, 5),
        "slo_target": slo_target,
        "budget_remaining_pct": round(budget_remaining * 100, 4),
        "breached": budget_remaining < 0,
    }


healthy = check_slo(successful=999_600, total=1_000_000)
breached = check_slo(successful=996_000, total=1_000_000)
print("healthy: ", healthy)
print("breached:", breached)
2
sli is the measured, real number — successes divided by total requests over the window.
3
error_budget is fixed by the SLO target alone (1 − 0.999 = 0.001), not by what actually happened.
5
budget_remaining goes negative exactly when actual failure exceeds what the error budget allowed — that IS the SLO breach.
Output
healthy:  {'sli': 0.9996, 'slo_target': 0.999, 'budget_remaining_pct': 0.06, 'breached': False}
breached: {'sli': 0.996, 'slo_target': 0.999, 'budget_remaining_pct': -0.3, 'breached': True}

Why this works: budget_remaining is exactly error_budget minus how much actually failed — positive means the team has room left at the current SLO target, negative means the SLO has already been breached for the window. This is the concrete form of 'how will you know the service is broken': not a single alert firing, but the error budget going negative.

How the three terms relate

SLI

Measured now

99.95% success, last 30 days

SLO

Target committed to

99.9% success, 30-day window

Error budget

100% − SLO

0.1% allowed to fail before breach

  • SLI
    • Measured now — 99.95% success, last 30 days
  • SLO
    • Target committed to — 99.9% success, 30-day window
  • Error budget
    • 100% − SLO — 0.1% allowed to fail before breach

Treating 100% as the reliability target instead of setting an SLO

Wrong

python
# "Our goal is zero errors."
# No SLO, so there's no error budget, and no way to decide
# "is this incident bad enough to halt feature work" objectively.

Better

python
slo_target = 0.999   # 99.9% over 30 days — deliberately not 100%
error_budget = 1 - slo_target
# Budget exhausted -> freeze risky changes until it recovers.
# Budget healthy -> normal feature velocity is fine.

What you see: Every outage becomes an equally urgent all-hands fire, because there is no pre-agreed threshold for 'this is within normal variance' vs. 'this must stop feature work' — the team argues about severity from scratch every time.

Why: 100% reliability is not achievable for a networked system and chasing it stalls feature work for no measurable benefit past the target users actually need. Google's SRE book frames the error budget as the tool that turns 'is this bad?' into an objective, pre-agreed number both engineering and product already signed off on.

SLI vs. SLO vs. error budget

SLI vs. SLO vs. error budget
TermWhat it isWorked example
SLIa measured number, right now99.95% of requests succeeded, last 30 days
SLOthe target you committed to for that SLI99.9% of requests succeed, over 30 days
Error budget100% − SLO — allowed failure before breach0.1% of requests may fail — roughly 43 minutes of full downtime/month

Together

python
def error_budget_minutes(slo_percent: float, window_days: int = 30) -> float:
    """Minutes of full downtime an SLO allows over the window — the
    textbook conversion from a percentage target to a budget in minutes."""
    window_minutes = window_days * 24 * 60
    allowed_failure_fraction = 1 - (slo_percent / 100)
    return window_minutes * allowed_failure_fraction

print(f"{error_budget_minutes(99.9):.1f} minutes")   # 99.9% SLO
print(f"{error_budget_minutes(99.99):.2f} minutes")  # 99.99% SLO

Remember: SLI = what you measured; SLO = the target you promised; error budget = 100% − SLO, and a negative budget is the objective definition of an SLO breach.

See also: alerts and dashboards · health checks · incident response and root cause analysis

Advertisement

Incidents and recovery

The incident-response lifecycle, the blameless postmortem that follows it, and the two ways to recover from a bad deploy or a failing dependency.

Incident response and root cause analysis

coreintermediate

Incident response is the structured process a team follows while a service is broken right now — declare, mitigate, resolve; root cause analysis happens after, asking why it broke so it does not happen the same way twice.

Think of it as

Incident response is a fire drill in progress — stop the fire, get everyone out, worry about arson investigation later. Root cause analysis is that later investigation: not "who left the stove on" but every contributing condition that let one mistake become an outage.

text
Incident lifecycle:  detect -> declare -> mitigate -> resolve -> analyze
Root cause analysis: "Five Whys" — ask why repeatedly past the first answer
                      until you reach a systemic, fixable condition

What we're doing: Show mitigation strictly ordered before full root-cause investigation, and root cause analysis modeled as contributing factors rather than one cause.

incident.pypython
def next_incident_action(mitigated: bool, root_cause_known: bool) -> str:
    if not mitigated:
        return "mitigate now (rollback / flag off / shed traffic)"
    if not root_cause_known:
        return "investigate root cause (Five Whys / contributing factors)"
    return "write the postmortem"


def five_whys(symptom: str, answers: list[str]) -> dict:
    """Models root cause analysis as a chain of contributing factors,
    not a single root cause — the SRE book's own framing."""
    chain = [symptom, *answers]
    return {"chain": chain, "contributing_factors": answers, "depth": len(answers)}


print(next_incident_action(mitigated=False, root_cause_known=False))
print(next_incident_action(mitigated=True, root_cause_known=False))
print(next_incident_action(mitigated=True, root_cause_known=True))
print(five_whys(
    "Checkout returned 500s",
    ["DB connection pool exhausted",
     "a slow query held connections open",
     "an index was dropped in last week's migration",
     "the migration review process has no query-plan check"],
))
2
Mitigation is checked FIRST — the function refuses to suggest root-cause work while users are still impacted.
9
five_whys returns the whole chain of contributing_factors, not a single "root_cause" field — matching the SRE book's framing.
Output
mitigate now (rollback / flag off / shed traffic)
investigate root cause (Five Whys / contributing factors)
write the postmortem
{'chain': ['Checkout returned 500s', 'DB connection pool exhausted', 'a slow query held connections open', "an index was dropped in last week's migration", 'the migration review process has no query-plan check'], 'contributing_factors': ['DB connection pool exhausted', 'a slow query held connections open', "an index was dropped in last week's migration", 'the migration review process has no query-plan check'], 'depth': 4}

Why this works: next_incident_action never recommends root-cause work before mitigation is done — that ordering is the incident-response discipline itself. five_whys stops at the last answer given (here, a process gap: no query-plan check) rather than a person, which is what keeps root cause analysis about fixable systemic conditions instead of blame.

Incident response, in order

Detect

Alert fires or someone reports impact

Declare

Named an incident; Incident Commander assigned

Mitigate

Stop user impact — rollback, flag off, shed load

Resolve

Confirm SLIs are back inside the SLO

Analyze

Root cause analysis feeds the postmortem

  1. Detect — Alert fires or someone reports impact
  2. Declare — Named an incident; Incident Commander assigned
  3. Mitigate — Stop user impact — rollback, flag off, shed load
  4. Resolve — Confirm SLIs are back inside the SLO
  5. Analyze — Root cause analysis feeds the postmortem

Blocking mitigation on fully understanding the root cause first

Wrong

python
# "We won't roll back until we know exactly what broke."
# Users stay impacted for the entire investigation window —
# minutes to hours longer than necessary.

Better

python
# Roll back / flip the flag / shed load immediately.
# Investigate root cause AFTER user impact has already stopped —
# diagnosis does not require the outage to still be happening.

What you see: The incident duration (and the error-budget burn) is exactly as long as the full investigation, when a rollback could have stopped user impact in minutes.

Why: Mitigation and diagnosis are separable — a rollback (next concept) reverses symptoms without needing to know why they happened. The SRE book treats restoring service as the first priority precisely because the fastest fix and the full understanding rarely arrive at the same time, and users do not benefit from the team's understanding, only from the outage ending.

Incident lifecycle

Incident lifecycle
PhaseGoalTypical action
DetectNotice something is wrongAlert fires, or a user/engineer reports it
DeclareName it an incident, assign an Incident CommanderOpen an incident channel, page responders
MitigateStop user impact — not yet full understandingRollback, feature flag off, traffic shed
ResolveConfirm the service is actually healthy againMetrics/SLI back inside the SLO
AnalyzeFind contributing factors, not blameRoot cause analysis, feeds the postmortem

Together

python
def next_incident_action(mitigated: bool, root_cause_known: bool) -> str:
    """The SRE-book ordering: mitigate before you fully understand why —
    diagnosis can continue after user impact has already stopped."""
    if not mitigated:
        return "mitigate now (rollback / flag off / shed traffic)"
    if not root_cause_known:
        return "investigate root cause (Five Whys / contributing factors)"
    return "write the postmortem"

print(next_incident_action(mitigated=False, root_cause_known=False))
print(next_incident_action(mitigated=True, root_cause_known=False))
print(next_incident_action(mitigated=True, root_cause_known=True))

Remember: Mitigate first, understand second — root cause analysis is a chain of contributing factors ("Five Whys"), not one person's mistake, and it happens after impact has already stopped.

See also: postmortems · graceful degradation · rollbacks · slos slis and error budgets

Postmortems

standardintermediate

A postmortem is a written document produced after an incident is resolved — what happened, its impact, the contributing factors, and the concrete follow-up actions that reduce the chance it repeats.

Think of it as

A postmortem is the incident's permanent record, written for someone who was not there — future engineers, not just today's responders. Google's SRE book calls the practice "blameless": it names contributing factors and systems, never a person, because blame makes the next engineer hide a near-miss instead of reporting it.

text
Postmortem sections (SRE book shape):
  Summary        — one paragraph, what happened
  Impact         — duration, users/requests affected, SLO/error-budget impact
  Timeline       — detect -> declare -> mitigate -> resolve, with timestamps
  Root cause     — contributing factors (see Incident response / RCA)
  What went well — genuinely, not just criticism
  Action items   — each with an owner and a status

What we're doing: Model the postmortem's action-item follow-through as data — the part most likely to silently not happen without an owner and a status.

postmortem_actions.pypython
def open_action_items(actions: list[dict]) -> list[dict]:
    """Filters a postmortem's action items down to what's still open —
    the concrete signal a postmortem produced real follow-through."""
    return [a for a in actions if a["status"] != "done"]


actions = [
    {"item": "Add a query-plan check to migration review", "owner": "dana", "status": "done"},
    {"item": "Alert on connection-pool saturation, not just errors", "owner": "priya", "status": "open"},
    {"item": "Document the rollback runbook for checkout", "owner": "priya", "status": "open"},
]

print(open_action_items(actions))
4
status != "done" is the whole check — a postmortem with items stuck at "open" for months is a documented outage that has not actually been prevented from repeating.
Output
[{'item': 'Alert on connection-pool saturation, not just errors', 'owner': 'priya', 'status': 'open'}, {'item': 'Document the rollback runbook for checkout', 'owner': 'priya', 'status': 'open'}]

Why this works: The filter surfaces exactly the two items that still need work — a postmortem that never gets checked back against this list produces a document, not a fix, and the same incident is free to happen again.

Remember: Blameless: name the contributing factors, never the person; a postmortem without owned, tracked action items is a summary, not a fix.

See also: incident response and root cause analysis · rollbacks

Graceful degradation

coreintermediate

Graceful degradation means a service keeps working with reduced functionality when a dependency fails, instead of failing the whole request — a try/except around the dependency call, returning a fallback value.

Think of it as

A plane losing one engine does not fall out of the sky — it flies degraded, on the engines it has left, and lands. Graceful degradation is the same idea in code: catch the SPECIFIC failure of one dependency and return something useful instead of letting it take down the whole response.

python
def get_data():
    try:
        return call_dependency()
    except SpecificDependencyError:   # not a bare except
        return fallback_value

What we're doing: Build a small circuit breaker that stops calling a failing dependency after 3 consecutive failures, instead of retrying (and timing out) forever.

circuit_breaker.pypython
class CircuitBreaker:
    def __init__(self, failure_threshold: int = 3):
        self.failure_threshold = failure_threshold
        self.failure_count = 0
        self.open = False

    def call(self, func, *args):
        if self.open:
            raise RuntimeError("circuit open - skipping call, using fallback")
        try:
            result = func(*args)
        except Exception:
            self.failure_count += 1
            if self.failure_count >= self.failure_threshold:
                self.open = True
            raise
        else:
            self.failure_count = 0
            return result


def flaky_payment_check(_order_id: str) -> str:
    raise TimeoutError("payment gateway unreachable")


breaker = CircuitBreaker(failure_threshold=3)
for attempt in range(1, 5):
    try:
        breaker.call(flaky_payment_check, "order_1")
    except TimeoutError:
        print(f"attempt {attempt}: TimeoutError, failure_count={breaker.failure_count}, open={breaker.open}")
    except RuntimeError as e:
        print(f"attempt {attempt}: {e}")
8
Once open is True, call() refuses to even try the dependency — this is what stops piling up slow, timing-out calls.
13
failure_count only increments on a real failure, and open flips True once it reaches the threshold — 3 consecutive failures here.
26
The 4th attempt never reaches flaky_payment_check at all — the breaker fails fast with RuntimeError instead of another slow TimeoutError.
Output
attempt 1: TimeoutError, failure_count=1, open=False
attempt 2: TimeoutError, failure_count=2, open=False
attempt 3: TimeoutError, failure_count=3, open=True
attempt 4: circuit open - skipping call, using fallback

Why this works: The first three attempts genuinely call flaky_payment_check and pay its full timeout cost each time; failure_count climbs to the threshold on attempt 3 and flips open to True. The 4th attempt never touches the dependency — call() raises immediately, which is the entire point: stop paying the cost of calling something that is already known to be down.

Fallback path vs. failure path
succeedsfailsexcept

Call dependency

Response used

Dependency raises

Fallback returned

  • Call dependency
    • leads to Response used (succeeds)
    • on error, leads to Dependency raises (fails)
  • Response used
  • Dependency raises
    • on error, leads to Fallback returned (except)
  • Fallback returned

Catching Exception broadly instead of the specific dependency error

Wrong

python
def format_recommendations(user_id: str) -> list[str]:
    try:
        items = call_recommendation_service_up(user_id)
        return [item.uppercase() for item in items]   # typo: no such method
    except Exception:
        return ["bestseller-1", "bestseller-2", "bestseller-3"]  # masks the typo too

Better

python
def format_recommendations(user_id: str) -> list[str]:
    try:
        items = call_recommendation_service_up(user_id)
        return [item.uppercase() for item in items]   # same typo
    except RecommendationServiceDown:
        return ["bestseller-1", "bestseller-2", "bestseller-3"]  # only catches the real outage

What you see: The AttributeError from the .uppercase() typo (str has no such method — the real method is .upper()) never surfaces. Every call silently 'succeeds' with the fallback list, even when the dependency is healthy and the bug is in your own code.

Why: except Exception: catches every kind of failure — the dependency being down AND a bug in the code that runs after a successful call. Catching the specific exception the dependency actually raises lets a genuine outage degrade gracefully, while a real bug still raises and gets noticed, instead of being silently hidden behind the same fallback.

Degradation strategies

Degradation strategies
StrategyWhat happens on failureWhen to use
Static fallbackReturn a fixed, cached, or default valueNon-critical, cacheable data (recommendations, "related items")
Circuit breakerStop calling the dependency after N failures; fail fastA slow/timing-out dependency, to avoid piling up retries
Feature flag offDisable the feature entirely for all usersA whole feature is unsafe, not just one call
Retry with backoffRetry the SAME call after a delay, not a fallbackTransient failures expected to self-resolve quickly

Together

python
def get_recommendations(user_id: str) -> list[str]:
    try:
        return call_recommendation_service(user_id)
    except RecommendationServiceDown:
        return ["bestseller-1", "bestseller-2", "bestseller-3"]  # static fallback

Remember: Catch the specific dependency exception, not Exception — a broad catch degrades gracefully on real outages but also hides real bugs behind the same fallback.

See also: rollbacks · incident response and root cause analysis · avoiding swallowed exceptions

Rollbacks

standardintermediate

A rollback reverts a service to the last known-good version — usually the fastest way to stop user impact during an incident, since it does not require knowing why the new version broke.

Think of it as

A rollback is the "undo" button for a deployment: you do not need to understand what went wrong to press it, only that the previous version was known to work. That is exactly why it is the incident-response section's default first move, before root cause analysis even starts.

python
def choose_active_version(current: str, previous: str, errors: int, total: int) -> str:
    if error_rate_breached(errors, total):
        return previous   # roll back
    return current

What we're doing: Show an automated rollback trigger: the deployed version reverts on its own once the error rate crosses a threshold, without anyone deciding it by hand.

rollback_trigger.pypython
def error_rate_breached(errors: int, total: int, threshold: float = 0.05) -> bool:
    return total > 0 and (errors / total) > threshold


def choose_active_version(current: str, previous: str, errors: int, total: int) -> str:
    if error_rate_breached(errors, total):
        return previous   # roll back
    return current


print("healthy   :", choose_active_version("v2.4.0", "v2.3.1", errors=2, total=1000))
print("unhealthy :", choose_active_version("v2.4.0", "v2.3.1", errors=80, total=1000))
1
error_rate_breached is the same threshold-check shape as an alert rule (previous section concept) — rollback triggers and alerts often share logic.
6
Breaching the threshold returns previous, not current — the deploy system would read this as "route traffic back to the last known-good version".
Output
healthy   : v2.4.0
unhealthy : v2.3.1

Why this works: With a 2/1000 (0.2%) error rate, choose_active_version keeps the new version — well under the 5% threshold. With 80/1000 (8%), it returns the previous version instead, the same decision an automated canary-rollback system makes without a human in the loop.

Remember: A rollback reverts code, not data — it is usually the fastest mitigation because it needs no understanding of what broke, only that the previous version worked.

See also: graceful degradation · incident response and root cause analysis · alerts and dashboards

Advertisement