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:
- Database connection checks to fail
- App health checks that depend on the database to fail
- Upstream proxy checks to return 502
You don't expect:
- Disk space alerts on any server
- SSL certificate expiry warnings
- CPU or memory alerts on unrelated services
- Health checks for services that don't depend on the database
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:
- Go to Settings → Maintenance
- Create a maintenance window with start/end time
- 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:
- Go to Alerting → Silences → New Silence
- Add matchers:
service=database,dependency=database - Set duration to your maintenance window plus buffer
- 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:
- Identify which services are directly affected
- Identify which alerts depend on those services
- Mute only those specific alerts
- Set the mute duration to planned-work-time plus a buffer (not hours longer)
- Verify unrelated alerts are still active
- After the work: unmute immediately, don't wait for the window to expire
- Post-maintenance watch: stay for 5-10 minutes with full monitoring active
- Next morning: check for any anomalies in the affected services
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.
