GUIDE, WITHOUT THE GUESSWORK

Maintenance windows done right: mute the noise, not the real outages

Planned upgrades shouldn't spam your alerts. But broad muting hides real breakage. Here's how to set maintenance windows that suppress expected noise while keeping genuine incidents visible.

Maintenance windows done right: mute the noise, not the real outages

You schedule a database upgrade for Saturday at 2 AM. You know it'll take the app down for three minutes. You also know your monitoring will fire every alert it has: database unreachable, app health check failed, SSL endpoint timeout, upstream 502.

So you mute everything. The upgrade finishes, you unmute, and you go back to bed.

Except this time, the upgrade finished but the connection pool didn't recover. The app came back with a degraded state that your health check doesn't catch. By the time someone notices on Monday morning, you've had 30 hours of silent partial outage.

This is the maintenance window trap: muting too broadly hides real problems that happen during or right after planned work.

The two mistakes

Mistake 1: Not muting at all. Your phone goes off twelve times during a planned three-minute restart. You start ignoring alerts. Alert fatigue sets in. Two weeks later, a real alert fires at 3 AM and you sleep through it because you've been conditioned to ignore nighttime notifications.

Mistake 2: Muting everything. You suppress all alerts for a two-hour window. The planned work takes twenty minutes. For the remaining hundred minutes, your infrastructure is unmonitored. If something unrelated breaks during that window — a different service runs out of disk, a certificate expires, a deploy on another app fails — you won't know until the window closes.

Both mistakes have the same root cause: treating maintenance windows as binary. Either everything is monitored or nothing is.

What to mute and what to keep

The principle: mute the alerts you expect to fire, keep everything else active.

For a database restart, you expect:

You don't expect:

The maintenance window should silence the first group and leave the second group untouched.

Implementation patterns

Pattern 1: Tag-based muting

Label your alerts with the service they depend on:

# In your monitoring config
alerts:
  - name: app-health
    tags: [app, database-dependent]
  - name: db-connection
    tags: [database]
  - name: disk-space
    tags: [infrastructure]
  - name: ssl-expiry
    tags: [infrastructure]

When you schedule database maintenance, mute alerts tagged database and database-dependent. Everything else stays active.

This works in Uptime Kuma (using tags), Grafana (using silence matchers), Prometheus Alertmanager (using matchers), and most monitoring tools.

Pattern 2: Dependency-aware muting (Alertmanager)

# alertmanager.yml silence via amtool
amtool silence add \
  --alertname="DatabaseDown|AppHealth" \
  --comment="Planned DB upgrade" \
  --duration="30m" \
  --author="ops-team"

The key: use --duration that's slightly longer than your expected maintenance, not a huge blanket window. If the upgrade should take 10 minutes, set a 30-minute silence. If it's still muted after 30 minutes, something went wrong and you want to know about it.

Pattern 3: Two-phase window

Split your maintenance into two phases:

Phase 1: Active maintenance (muted) Silence expected alerts. Do the work. Verify the service is back.

Phase 2: Post-maintenance watch (fully monitored) Immediately unmute everything. Watch for five to ten minutes. Confirm all health checks pass. Only then walk away.

The post-maintenance watch is what most teams skip. They finish the work, see the service respond to one request, declare victory, and go to bed. The connection pool exhaustion, the slow memory leak from a config change, the replication lag — those show up ten minutes later when nobody is watching.

Pattern 4: Maintenance health check override

Instead of muting alerts, change what the health check accepts during maintenance:

# Before maintenance
healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:3000/health"]

# During maintenance - accept 503 (service unavailable) as "expected"
healthcheck:
  test: ["CMD-SHELL", "curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/health | grep -E '200|503'"]

This way the container stays "healthy" during planned downtime but would still catch unexpected errors (500, connection refused, timeout). This is more surgical than muting the entire alert.

Uptime Kuma specifics

Uptime Kuma has built-in maintenance window support:

  1. Go to Settings → Maintenance
  2. Create a maintenance window with start/end time
  3. Assign specific monitors to the window

The monitors you assign will be paused during the window. Unassigned monitors keep running. This is the tag-based pattern built into the UI.

The trap: don't assign "all monitors" to the maintenance window. Pick only the ones directly affected by the planned work.

Grafana / Prometheus specifics

In Grafana, use Silences in Alertmanager:

  1. Go to Alerting → Silences → New Silence
  2. Add matchers: service=database, dependency=database
  3. Set duration to your maintenance window plus buffer
  4. Add a comment explaining what's being maintained

The comment matters. When someone sees a silence and wonders why alerts aren't firing, the comment tells them it's planned, not a misconfiguration.

The checklist

Before every maintenance window:

The mindset shift

Maintenance windows aren't about turning off monitoring. They're about telling your monitoring system what to expect.

A well-configured maintenance window is a contract: "I expect these specific things to break for this specific duration. Alert me about everything else." That's the difference between controlled downtime and flying blind.