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 (a rule that mutes alerts), it pages the on-call engineer four times. A silence clicked in the UI might still mute critical alerts in three years. Here the window is a small YAML file in git with a hard end time. Everything else follows from that file.

Starts fromA planned change that will trip alerts. Examples are a migration, an upgrade hop or a failover test.
Looks atThe declared silence file. The live Alertmanager silences. The loaded alert rules. Open PagerDuty incidents.
You getA reviewed silence that applies on merge. It snoozes the pager and warns you before it expires. The assistant checks it afterwards.
01 · Why clicked silences rot

"Ok", ending in 2137.

A real audit of one company's infrastructure found silences created years earlier. They had an end date more than a century away and a two-word comment. Their matchers pointed at alert rules that no longer existed anywhere. They muted nothing useful. They also hid whatever came next under the same name.

A better UI is not the fix. The fix is to make 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 repository under silences/<tenant>/<id>.yaml. Each file has matchers, a start, an end at most 30 days later, a comment and a link to the change that justifies the silence.

A genuinely long-lived silence is rare. It has a loud escape hatch, permanent:. This needs a reason, an approver and a review_by date.

03 · Checked before merge

A lint that needs no credentials at all.

The merge request (MR) pipeline runs a small, self-contained validator. It refuses unknown keys, duplicate keys and multi-document files. The tenant must match its directory. The id must match the file name. The window must have an end. 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 assistant uses the same validator as its loader. 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 it creates the silence. Each silence carries a sentinel comment with its id and generation. So the job never re-creates a past window. If you edit an end time, the next pass reconciles it.

The lint ships with the assistant. The applier is a small, tested script that runs in your pipeline. If the job cannot read a tenant, it fails and does not guess.

05 · PagerDuty follows the silence

An incident opened before the window stops paging too.

An Alertmanager silence does nothing for a PagerDuty incident that is already open. The escalation policy keeps sending notifications. So every 15 minutes the same pipeline snoozes open incidents that a live declared silence covers. The snooze lasts until the silence ends.

It also resolves incidents whose alert group has stopped firing. A silenced alert would never do this 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 the alert, with the silence attached as context. The assistant annotates silenced alerts and does not drop them. 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 before the end, and again thirty minutes before, a check runs. If alerts that the silence covers are still firing, a Slack message says so. At that point you can extend the silence on purpose (edit ends_at in a new MR) or accept the page.

At ends_at the silence expires. Nobody has to remember it or clean it up.

08 · Audited afterwards

Every live silence, checked against the rules that exist.

A read-only audit checks every live silence of every tenant against the loaded rule set. Rule-based checks flag a silence with no end, or with an end more than 30 days away. They also flag a silence that matches no loaded rule, and an empty or broken matcher. A permanent grant whose review date has passed is a finding too.

A second report lists the other ways to mute an alert. These are inhibit rules, mute intervals and severity demotions. The report also shows their expiry debt (mutes that should have ended). If the assistant cannot read a source, 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. The silence fits the window the hop needs. We also plan recurring maintenance windows, such as a weekly patch slot, as a route-level mute interval.

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. It is 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. This is an 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. PostgresDown and PostgresReplicationLag will fire for a few minutes each. This MR 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 to fail at checkout, the pager still wakes you.

+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. It checked the schema, tenant, id and 4h window, and all matchers compile. It used no credentials.
  • Merge-request pipelines skip apply:silences and reconcile:silences. They run only on the default branch.
  • The default-branch pipeline plans CREATE orders-db-pg16-2026-09-26@g1 and applies it.
  • From 06:00Z, Alertmanager suppresses matching alerts. The pipeline snoozes any matching PagerDuty incident until 10:00Z.
  • At 08:00Z and 09:30Z, a Slack message says if covered alerts are still firing.
  • At 10:00Z the silence expires. To extend it, edit ends_at in a new MR. The reconcile job picks it up.
GA
silences audit scheduled run, not an MR comment

The silence has an end (4h) and matches 3 loaded rules in tenant acme. The next audit has no findings.

What keeps it honest.

A silence stops people from hearing about a problem. It gets stricter handling than any other config.

No write verb in the assistantThe assistant only reads Alertmanager. Your default-branch CI applies silences after a person merges them.
Every silence endsOne-shot silences have a 30-day cap. The permanent hatch needs a reason, an approver and a review date. Your CI applies it only until that date.
One schema, one interpretationThe pre-merge lint and the runtime loader are byte-identical copies. A checksum self-check means they cannot drift apart.
Events, not desired stateA later pipeline never re-creates a past window, even after Alertmanager has forgotten it.
Blind is not cleanIf the assistant cannot read the silences or rules of a tenant, the audit exits "blind" and the applier refuses to act.
Context, not filteringAlert triage keeps seeing silenced alerts, labelled with the silence. So it still notices an unexpected failure during a window.

Where this stands.

Live
  • Declared one-shot silences with a mandatory end, and a credential-free MR lint
  • CI applier on the default branch. It checks generation and content
  • 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