Home / Stories / Silences as code
Story · Alerting Live

Planned work that pages nobody, and silences that end when their reason does.

A database migration is booked for Saturday morning. Without a silence, it pages the on-call engineer four times. With a silence clicked in the UI, it might still be muting critical alerts in three years. Here the window is a small YAML file in git with a hard end time, and everything downstream follows from it.

Starts fromA planned change that will trip alerts: a migration, an upgrade hop, a failover test.
Looks atThe declared silence file, live Alertmanager silences, the loaded alert rules, open PagerDuty incidents.
You getA reviewed silence that applies on merge, snoozes the pager, warns before it expires, and is audited afterwards.
01 · Why clicked silences rot

"Ok", ending in 2137.

A real audit of one estate found silences created years earlier with an end date more than a century away, a two-word comment, and matchers for alert rules that no longer existed anywhere. They muted nothing useful and hid whatever came next under the same name.

The fix is not a better UI. It is making every silence a reviewed change with a reason and an end.

02 · Declare the window

One file per silence, with an end time that is mandatory.

Silences live in your alerting-config repo under silences/<tenant>/<id>.yaml: matchers, a start, an end at most 30 days later, a comment and a link to the change that justifies it.

The rare genuinely long-lived silence has a loud escape hatch, permanent:, which requires a reason, an approver and a review_by date.

03 · Checked before merge

A lint that needs no credentials at all.

The merge request pipeline runs a small, self-contained validator: unknown keys, duplicate keys and multi-document files are refused, the tenant must match its directory, the id must match the file name, the window must be bounded, every regex must compile.

It runs with no network and no secrets, because in many Alertmanager setups a "read-only" certificate can also write. The same validator is the loader the assistant uses, so review and runtime never disagree about the schema.

04 · Applied on merge, by your CI

The assistant never holds an Alertmanager write key.

After merge, a job on the default branch prints a plan, then creates the silence. Each silence carries a sentinel comment with its id and generation, so a past window is never re-created and an edited end time is reconciled on the next pass.

The lint ships with the assistant; the applier is a small, tested script that runs in your pipeline. If a tenant cannot be read, the job fails instead of guessing.

05 · PagerDuty follows the silence

An incident opened before the window stops paging too.

An Alertmanager silence does nothing for a PagerDuty incident that was already open: the escalation policy keeps re-notifying. So every 15 minutes the same pipeline snoozes open incidents that a live declared silence covers, until the silence ends.

It also resolves incidents whose alert group has stopped firing, which a silenced alert would otherwise never do on its own.

06 · During the window

The alert fires, nobody is woken, the context is kept.

The migration trips PostgresReplicationLag at 06:40. Alertmanager matches it to the silence and sends nothing.

The assistant's alert triage still sees it, with the silence attached as context: silenced alerts are annotated, not dropped, so if something outside the planned change breaks, the triage knows which alerts were expected.

07 · Before and at the end

A warning if it is still firing. Then alerting comes back.

Two hours and again thirty minutes before the end, if alerts covered by the silence are still firing, a Slack message says so. That is the moment to extend it deliberately (edit ends_at in a new MR) or accept the page.

At ends_at the silence expires. Nothing has to be remembered or cleaned up.

08 · Audited afterwards

Every live silence, checked against the rules that exist.

A read-only audit walks every tenant's live silences and the loaded rule set. Deterministic rules flag an unbounded or over-30-day expiry, a silence that matches no loaded rule, an empty or broken matcher, and a permanent grant whose review date has passed.

A second report inventories the other ways an alert can be muted (inhibit rules, mute intervals, severity demotions) and their expiry debt. If a source cannot be read, the answer is "unknown", never "clean".

09 · Proposed with the change Roadmap

Planned: the silence arrives next to the upgrade MR.

Planned: when the assistant proposes a risky upgrade hop, it also drafts the matching silence file as a companion MR, sized to the window the hop needs. Recurring maintenance windows (a weekly patch slot, as a route-level mute interval) are also planned.

Today your team writes the silence file. The rest of this page runs in production.

Alertmanager · active silences · tenant acme
  • alertname=EtcdInsufficientMembers, severity=critical ends in 2137 · "Ok"
  • alertname=NodeExporterDown, severity=critical ends in 2137 · "fine"
SIL001 unbounded expiry · SIL004 matches no loaded rule
Illustrative, modelled on a real finding.
silences/acme/orders-db-pg16-2026-09-26.yamlnew file
+tenant: acme                        # must match the directory
+doc_id: orders-db-pg16-2026-09-26   # must match the file name
+generation: 1                       # bump only to re-issue
+matchers:
+  - name: namespace
+    value: orders-db
+  - name: alertname
+    value: Postgres(ReplicationLag|Down|TooManyConnections)
+    isRegex: true
+starts_at: 2026-09-26T06:00:00Z
+ends_at:   2026-09-26T10:00:00Z      # ≤ 30 days after start
+comment: "orders-db Postgres 15 → 16 migration, primary failover expected"
+link: https://git.example.com/acme/platform/-/merge_requests/318
MR pipeline · verify:silencesno credentials
$ python3 ci/silences_lint.py --self-check silences/
✓ self-check: validator matches the vendored checksum
✓ acme/orders-db-pg16-2026-09-26.yaml  schema · keys · types
✓   tenant "acme" matches directory
✓   window 4h (cap 30d)
✓   2 matchers · regex compiles
  note: regex checked with Python re; Alertmanager uses RE2
0 failing · 0 skipped
what it refuses
  • ends_at missing, or more than 30 days out use permanent: with review_by
  • unknown or duplicate keys no silent "last one wins"
  • empty matcher list would silence everything
Mergeddefault branch
apply:silencesshort-lived cert from CI
Alertmanagertenant acme
apply:silences · plan, then apply
tenant acme   live sentinels: 3   declared: 4
  CREATE  orders-db-pg16-2026-09-26@g1   06:00Z → 10:00Z
  skip    batch-reindex-2026-09-12@g1    past window, never re-created
  ok      search-rebuild-2026-09-24@g2   content matches
comment: "gitops-silences:orders-db-pg16-2026-09-26@g1: orders-db …"
applied 1 · 0 errors
the assistant: read-only · CI: the only writer
PagerDuty · [FIRING] PostgresDown · orders-db
opened 05:52Z, before the window · escalation: re-notify every 30 min
reconcile:silences · every 15 min
SNOOZE   incident PostgresDown orders-db   until 10:00Z  covered by orders-db-pg16…@g1
RESOLVE  incident CronJobFailed batch      alert group no longer firing in any tenant
note added to both incidents with the silence id
PagerDuty · PostgresDown · orders-db
snoozed until 10:00Z · no further notifications
PostgresReplicationLag · orders-db · 06:40Z
lag 48s · state: suppressed · silenced by orders-db-pg16-2026-09-26@g1
P
pager 06:40
(nothing sent)
GA
gitops-assistant alert triage
Replication lag on orders-db is inside a declared silence for the Postgres 16 migration.
Silence context: 06:00Z → 10:00Z · matchers namespace=orders-db, alertname=~Postgres(…) · "primary failover expected"
silenced alerts are annotated, never filtered out
CI
silences reconcile 08:00
Silence orders-db-pg16-2026-09-26 ends in 2h. 1 covered alert still firing: PostgresReplicationLag.
CI
silences reconcile 09:30
(no message: nothing covered is firing any more)
replication lag · orders-db
silence 06:00–10:00 expired
10:00Z silence expired · alerting for orders-db is back to normal
silences audit · read-only · all tenants
  • orders-db-pg16-2026-09-26 bounded · matches loaded rules
  • SIL001 expiry beyond 30 days / unbounded
  • SIL004 matches no loaded rule · silences nothing real
  • SIL006 declared permanent grant · kept loud
  • SIL007 permanent grant past review_by
suppression ladder · expiry debt
  • Alertmanager silences 1 active · all bounded
  • inhibit rules 2 candidates · static, not proof of firing
  • route mute intervals 0 · schedule only
  • severity demotions 12 alertnames demoted to info · no expiry
Draft MR · upgradekube-prometheus-stack 66.7.1 → 68.x
Companion draft MR · silencealert family for the hop · window sized to the rollout
You merge bothsilence applies first, then Flux rolls the upgrade
Roadmap. Illustrative of the planned companion MR.

The merge request that books the window.

Today your team opens this MR; drafting it alongside an upgrade is on the roadmap. Everything after "merge" runs in production now. Illustrative example.

Open

Silence orders-db Postgres alerts during the PG16 migration (Sat 06:00–10:00 UTC)

from silence/orders-db-pg16 into main · 1 file · +13 · pipeline passed
verify:silences ✓tenant:acmewindow 4h
OverviewChangesPipelineAfter merge
What and why

The orders-db cluster moves from Postgres 15 to 16 on Saturday. The operator performs a switchover, so PostgresDown and PostgresReplicationLag will fire for a few minutes each. This silences that alert family for orders-db only, for four hours.

Not silenced

Any other namespace; any other Postgres alert name; the application's own SLO alerts. If orders-api starts failing checkout, you will still be paged.

+tenant: acme
+doc_id: orders-db-pg16-2026-09-26
+generation: 1
+matchers:
+  - name: namespace
+    value: orders-db
+  - name: alertname
+    value: Postgres(ReplicationLag|Down|TooManyConnections)
+    isRegex: true
+starts_at: 2026-09-26T06:00:00Z
+ends_at:   2026-09-26T10:00:00Z
+comment: "orders-db Postgres 15 → 16 migration, primary failover expected"
+link: https://git.example.com/acme/platform/-/merge_requests/318
  • verify:silences passed: schema, tenant, id, 4h window, matchers compile. No credentials used.
  • apply:silences and reconcile:silences are skipped on merge-request pipelines; they only run on the default branch.
  • The default-branch pipeline plans CREATE orders-db-pg16-2026-09-26@g1 and applies it.
  • From 06:00Z, matching alerts are suppressed; any matching PagerDuty incident is snoozed until 10:00Z.
  • At 08:00Z and 09:30Z, Slack is told if covered alerts are still firing.
  • At 10:00Z the silence expires. To extend it, edit ends_at in a new MR; the reconcile picks it up.
GA
silences audit scheduled run, not an MR comment

Silence is bounded (4h) and matches 3 loaded rules in tenant acme: no findings in the next audit.

What keeps it honest.

A silence is a way to stop people being told something. It gets stricter handling than any other config.

No write verb in the assistantThe assistant only reads Alertmanager. Silences are applied by your default-branch CI after a human merge.
Every silence endsA 30-day cap on one-shot silences; the permanent hatch needs a reason, an approver and a review date, and is applied only until that date.
One schema, one interpretationThe pre-merge lint and the runtime loader are byte-identical copies, with a checksum self-check, so they cannot drift apart.
Events, not desired stateA past window is never re-created by a later pipeline, even after Alertmanager has forgotten it.
Blind is not cleanIf a tenant's silences or rules cannot be read, the audit exits "blind" and the applier refuses to act.
Context, not filteringAlert triage keeps seeing silenced alerts, labelled with the silence, so an unexpected failure during a window is still noticed.

Where this stands.

Live
  • Declared one-shot silences with a mandatory end, credential-free MR lint
  • CI applier on the default branch, generation- and content-aware
  • PagerDuty snooze / resolve and Slack expiry warnings every 15 minutes
  • Read-only silence audit (SIL rules) and silence context in alert triage
In development
  • Suppression-ladder inventory across inhibit rules, mute intervals and severity demotions (built, report-only)
Roadmap
  • The assistant drafting the silence MR alongside a risky upgrade MR
  • Recurring maintenance windows as reviewed route-level mute intervals
  • A silence history store once something consumes it
← Previous storyVault configuration as code