Documentation

Maintenance Windows

Version v2.3.1 · Updated
Interactive docs View on GitHub

Maintenance Windows#

Maintenance Windows define planned periods when IncidentRelay should change how matching alerts are handled. A window can target a group, team, service, or route and can either suppress notifications, suppress incident creation, create a maintenance incident, or pause escalations.

Concepts#

A maintenance window is matched against incoming alerts after routing and service resolution. This means the route, team, and service are already known before IncidentRelay decides whether a maintenance window applies.

Maintenance windows are stored separately from incidents, but matching information is attached to the created alert group and child alerts through maintenance fields. In the IncidentRelay data model, an AlertGroup is the incident storage model.

Scope#

Each maintenance window must have at least one scope.

Supported scope types:

ScopeDescription
groupApplies to all teams, services, routes, and alerts under the group.
teamApplies to the selected team.
serviceApplies to the selected service.
routeApplies to the selected alert route.

If multiple active windows match the same alert, the repository returns the newest matching active window first, ordered by starts_at and then by id.

Behaviors#

Maintenance behavior controls what happens when a matching alert arrives during an active window.

BehaviorIncident createdIncident statusNotificationsEscalationmaintenance_suppressed
suppress_notificationsYesIncoming alert statusSuppressedPausedtrue
suppress_incidentNoNot applicableNot applicableNot applicableNot applicable
create_maintenance_incidentYesmaintenanceNot suppressed by the maintenance flagNormal unless otherwise configuredfalse
pause_escalation_onlyYesIncoming alert statusNormalPausedfalse

suppress_notifications#

Creates the alert group and child alert, attaches the maintenance window, and pauses the complete notification lifecycle while the window is active. This includes:

  • the initial notification;
  • queued group updates;
  • reminders and repeats;
  • user-level delayed deliveries;
  • future escalation steps and escalation notifications.

Already delivered notifications remain in history and are not retracted. Missed reminders and escalation steps are not replayed after maintenance.

When the maintenance occurrence ends, a still-firing alert group schedules one normal notification or update. The reminder counter is reset, and policy escalation timing resumes from the end of maintenance using the current escalation rule.

Use this when alerts should still be visible in IncidentRelay, but responders should not be paged during planned work.

suppress_incident#

Does not create a new alert group for a new matching alert.

If an existing alert or group already exists, the dedup/update flow can still continue. This avoids breaking existing incident lifecycle behavior while preventing new incidents from being created during planned maintenance.

Use this when matching alerts are expected and should not appear as incidents.

create_maintenance_incident#

Creates an incident with status maintenance and attaches the maintenance window.

This does not set maintenance_suppressed=true. The maintenance_suppressed flag only means notification suppression caused by suppress_notifications.

Use this when maintenance alerts should be tracked explicitly as incidents.

pause_escalation_only#

Creates the alert group and child alert, attaches the maintenance window, and sets the next escalation time to None.

Use this when the incident should be visible and notifications can still be sent, but escalation should not advance while the maintenance window is active.

Timezone behavior#

starts_at and ends_at are wall-clock datetimes in the selected maintenance window timezone. They must not be converted to UTC by the frontend before sending them to the API.

Example payload for a Moscow maintenance window:

{
  "starts_at": "2026-06-07T07:52:00",
  "ends_at": "2026-06-07T11:37:00",
  "timezone": "Europe/Moscow"
}

IncidentRelay interprets this as 07:52 to 11:37 in Europe/Moscow, not UTC.

The effective status is calculated dynamically from:

  • starts_at
  • ends_at
  • timezone
  • rrule
  • status
  • enabled
  • deleted

Stored status can remain scheduled, while the API returns effective status as active or finished depending on the current time.

Effective status#

The API returns both stored and effective status:

{
  "status": "active",
  "stored_status": "scheduled"
}

status is the effective status and should be used by the UI.

stored_status is the database value and is useful for debugging or auditing.

Status calculation rules:

ConditionEffective status
status = cancelledcancelled
enabled = falseNot considered active for matching
Current local time is before starts_atscheduled
Current local time is between starts_at and ends_atactive
Current local time is after ends_atfinished
RRULE has a current occurrenceactive
RRULE has a future occurrencescheduled
RRULE has no future occurrencefinished

Recurring windows#

Maintenance windows support optional RFC5545 RRULE strings in the rrule field.

The RRULE is stored without the RRULE: prefix. If the API receives the prefix, it normalizes it before storing.

Examples:

RepeatRRULE
Daily, 3 occurrencesFREQ=DAILY;COUNT=3
Weekly, 5 occurrencesFREQ=WEEKLY;COUNT=5
Monthly, 2 occurrencesFREQ=MONTHLY;COUNT=2
Every MondayFREQ=WEEKLY;BYDAY=MO

The first occurrence starts at starts_at. Each occurrence uses the same duration calculated from ends_at - starts_at.

For recurring windows, the API can return the current or next occurrence:

{
  "occurrence": {
    "status": "active",
    "starts_at": "2026-06-07T07:00:00",
    "ends_at": "2026-06-07T08:00:00",
    "timezone": "Europe/Moscow",
    "recurring": true
  }
}

The UI should display occurrence times for recurring windows instead of only the base starts_at and ends_at.

Alert and incident matching#

Maintenance matching happens in upsert_alert() after route, team, service, and rotation are resolved.

The matching decision is based on:

  • group id
  • team id
  • service id
  • route id
  • current time in the maintenance window timezone
  • enabled/deleted/status fields

For services and routes, active_maintenance is dynamic. If a service or route is currently under maintenance, the serializer can return active maintenance even if no alert was created during the window.

For alerts and incidents, active_maintenance is attached state. Existing alert groups must not be marked as maintenance retroactively just because a new team/service/route maintenance window became active later.

Correct behavior:

Objectactive_maintenance source
ServiceDynamic lookup by current active maintenance window
RouteDynamic lookup by current active maintenance window
Alert group / incidentAttached maintenance_window_id on the alert group
Child alertAttached maintenance_window_id on the alert

API#

List maintenance windows#

GET /api/maintenance-windows?include_finished=1

Response:

{
  "items": [
    {
      "id": 12,
      "group_id": 1,
      "group_name": "Platform",
      "team_id": 2,
      "team_name": "SRE",
      "name": "Payments deploy",
      "description": "Planned deployment",
      "status": "active",
      "stored_status": "scheduled",
      "behavior": "suppress_notifications",
      "timezone": "Europe/Moscow",
      "rrule": null,
      "starts_at": "2026-06-07T07:52:00",
      "ends_at": "2026-06-07T11:37:00",
      "occurrence": {
        "status": "active",
        "starts_at": "2026-06-07T07:52:00",
        "ends_at": "2026-06-07T11:37:00",
        "timezone": "Europe/Moscow",
        "recurring": false
      },
      "enabled": true,
      "deleted": false,
      "scopes": [
        {
          "id": 33,
          "scope_type": "service",
          "service_id": 10,
          "service_name": "payments-api"
        }
      ]
    }
  ]
}

Create maintenance window#

POST /api/maintenance-windows
Content-Type: application/json
{
  "name": "Payments deploy",
  "description": "Planned deployment for payments-api",
  "behavior": "suppress_notifications",
  "timezone": "Europe/Moscow",
  "starts_at": "2026-06-07T07:52:00",
  "ends_at": "2026-06-07T11:37:00",
  "enabled": true,
  "rrule": null,
  "scopes": [
    {
      "scope_type": "service",
      "service_id": 10
    }
  ]
}

Update maintenance window#

PUT /api/maintenance-windows/{window_id}
Content-Type: application/json

Partial updates are supported. If starts_at, ends_at, timezone, behavior, or rrule are omitted, the current values are preserved.

When scopes is omitted, existing scopes are preserved.

{
  "name": "Updated deploy",
  "description": "Deployment postponed"
}

To replace scopes, send scopes explicitly:

{
  "scopes": [
    {
      "scope_type": "team",
      "team_id": 2
    }
  ]
}

Cancel maintenance window#

POST /api/maintenance-windows/{window_id}/cancel
Content-Type: application/json
{
  "reason": "Deployment postponed"
}

Cancel sets:

status = cancelled
enabled = false
deleted = false

Delete maintenance window#

DELETE /api/maintenance-windows/{window_id}

Delete is a soft delete and sets:

deleted = true
enabled = false

RBAC#

Maintenance windows use write permissions for the selected scope.

ScopeRequired permission
groupWrite access to the group
teamWrite access to the team
serviceWrite access to the service team
routeWrite access to the route team

When updating a window:

  • If scopes is provided, permissions are checked against the new scopes.
  • If scopes is omitted, permissions are checked against the existing window owner or existing scopes.

Audit log#

Successful write actions create audit records:

ActionAudit action
Createmaintenance_window.create
Updatemaintenance_window.update
Cancelmaintenance_window.cancel
Deletemaintenance_window.delete

Audit data includes the final maintenance window state and the request payload.

UI behavior#

The Maintenance Windows page should show:

  • summary tiles
  • list/table view
  • status badge
  • behavior
  • repeat value
  • current or next occurrence
  • starts/ends in the selected timezone
  • details panel
  • create/edit modal

Timezone selection should reuse the shared AppTimezones helper used by rotation layers.

The frontend must send datetime-local values as plain wall-clock strings through AppTimezones.normalizeDatetimeLocal(). It must not call new Date(value).toISOString() for maintenance windows.

Maintenance badges#

Services and routes can show active maintenance badges dynamically.

Alert groups and incidents should show badges only when maintenance was attached at alert processing time.

Badge text should include:

  • maintenance window name
  • behavior label
  • current or next occurrence time
  • timezone

Example tooltip:

Payments deploy · Suppress notifications · 07/06 07:52 — 07/06 11:37 Europe/Moscow

Test coverage#

Maintenance window tests should cover:

  • create/update/cancel/delete
  • audit records
  • RBAC for service/team/route/group scopes
  • timezone-aware active status
  • recurring RRULE windows
  • occurrence calculation
  • service and route dynamic active maintenance badges
  • alert group attached maintenance badges
  • no retroactive marking of old alert groups
  • all maintenance behaviors

Behavior matrix to keep covered:

suppress_notifications:
  incident is created
  maintenance_suppressed = true
  notification queue is cleared
  reminders and user-level delayed deliveries are suppressed
  escalation is paused
  after maintenance, one normal notification/update is scheduled
  missed reminders and escalation steps are not replayed

suppress_incident:
  new incident is not created
  upsert_alert returns (None, False)

create_maintenance_incident:
  incident is created
  status = maintenance
  maintenance_suppressed = false

pause_escalation_only:
  incident is created
  next_escalation_at = None
  maintenance_suppressed = false

Retroactive re-evaluation#

Each maintenance window has two independent lifecycle options:

  • Apply to existing unresolved alerts is disabled by default. When enabled, the active window immediately affects matching unresolved alert groups that existed before the occurrence started. It is unavailable for Suppress incident, because an already-created group cannot be retroactively prevented from existing.
  • Reactivate affected alerts when this maintenance ends is enabled by default. When disabled, the applied effect remains after the window ends, is disabled, or is cancelled. Enabling the option later and saving the window releases retained effects. Deleting the window, removing a group from its scope, or changing to an incompatible behavior always releases effects that no longer belong to the window.

Explicit API changes are reconciled immediately. A scheduler job handles scheduled starts, expiration and recurring occurrences. Processing is idempotent and overlapping windows are tracked independently. A group resumes only after the final active or retained effect no longer applies. Missed reminders and escalation steps are not replayed.