Prometheus Alertmanager Silences and Inhibition Rules: Reducing Alert Noise in Production Systems

Monitoring tutorial - IT technology blog
Monitoring tutorial - IT technology blog

Context: When Alerts Outnumber Actual Incidents

Alert fatigue is a real problem I ran into when first setting up monitoring. I tuned thresholds more than ten times before realizing the issue wasn’t the thresholds at all — it was how Alertmanager was configured. Every time an incident occurred, dozens of alerts would fire simultaneously: CPU high, memory high, response time spiking, health checks failing — all from a single root cause. After a few weeks, the whole team started ignoring the monitoring Slack channel.

As it turns out, Alertmanager has two built-in features that address this exactly:

  • Silences: Temporarily mute a group of alerts for a defined time window — useful when you know downtime is coming or you’re in a maintenance period
  • Inhibition Rules: Automatically suppress secondary alerts when a primary alert has already fired — reduces symptom alerts when a root cause alert is already active

This article goes straight into both features. It assumes you already have Alertmanager running with a basic receiver configured.

Installing amtool — The Alertmanager CLI

amtool ships with the Alertmanager distribution and is the primary tool for interacting with it from the terminal. If you don’t have it yet:

# Download Alertmanager (amtool is included in the package)
wget https://github.com/prometheus/alertmanager/releases/download/v0.27.0/alertmanager-0.27.0.linux-amd64.tar.gz
tar xvf alertmanager-0.27.0.linux-amd64.tar.gz
sudo mv alertmanager-0.27.0.linux-amd64/amtool /usr/local/bin/

# Create default amtool config (avoids having to type the URL every time)
mkdir -p ~/.config/amtool
cat > ~/.config/amtool/config.yml <<EOF
alerting.url: http://localhost:9093
EOF

# Verify the connection
amtool alert query

Detailed Configuration

Silences — Intentionally Muting Alerts

The easiest way is through the UI at http://your-server:9093, under the Silences tab → New Silence. In practice though, I use amtool more often since it integrates cleanly into scripts.

# Silence all alerts for the web service for 2 hours
amtool silence add \
  --duration=2h \
  --comment="Deploy v2.1.0 — expected restart" \
  alertname=~".+" \
  service="web"

# View the list of active silences
amtool silence query

# Expire the silence immediately if the deploy finishes earlier than expected
amtool silence expire <silence-id>

I currently integrate amtool into my CI/CD pipeline. Before each deploy, the script automatically creates a 30-minute silence. Once the health check passes, it expires immediately — avoiding dozens of false positive alerts on every release:

#!/bin/bash
# deploy-with-silence.sh

SILENCE_ID=$(amtool silence add \
  --duration=30m \
  --comment="CI/CD deploy $(git rev-parse --short HEAD)" \
  environment="production" \
  service="api" \
  --output=json | jq -r '.silenceID')

echo "Silence created: $SILENCE_ID"

# Run the deploy
./deploy.sh

# Expire the silence after the health check passes
amtool silence expire "$SILENCE_ID"
echo "Silence expired"

Important note: Always fill in the comment field when creating a silence. Two weeks later, you won’t remember why that silence exists — and your teammates definitely won’t either.

Inhibition Rules — Automatic Logic for Suppressing Secondary Alerts

This feature is more overlooked than I’d expect, even though it’s the one that actually tackles alert noise at the root. The concept is simple: if alert A is active, automatically suppress all alert B instances that share specified labels.

The classic example: when a node goes down, every service on that node is obviously down too. There’s no need to receive 20 separate alerts — a single NodeDown alert is enough to know what needs to be done.

Add the following to /etc/alertmanager/alertmanager.yml:

inhibit_rules:
  # When a node is down, suppress all warning/critical severity alerts from that node
  - source_matchers:
      - alertname = "NodeDown"
    target_matchers:
      - severity =~ "warning|critical"
    equal:
      - instance

  # Critical alerts suppress warning alerts from the same service (avoids warning spam when critical has already fired)
  - source_matchers:
      - severity = "critical"
    target_matchers:
      - severity = "warning"
    equal:
      - alertname
      - namespace
      - service

  # Database down suppresses application error alerts (blocks symptom alerts when root cause is clear)
  - source_matchers:
      - alertname = "DatabaseDown"
      - severity = "critical"
    target_matchers:
      - alertname =~ "HighErrorRate|SlowResponse|ServiceUnavailable"
    equal:
      - environment

Field explanations:

  • source_matchers: Conditions for the “parent” alert — the alert that triggers the inhibition
  • target_matchers: Conditions for the alert to be suppressed
  • equal: Labels that must have identical values in both source and target — this is how Alertmanager determines that two alerts belong to the same instance or service

Production Example for a Kubernetes Cluster

The configuration below is what I’m currently running in a real Kubernetes multi-cluster environment:

inhibit_rules:
  # Cluster network partition suppresses node-level alerts
  - source_matchers:
      - alertname = "ClusterNetworkPartition"
    target_matchers:
      - alertname =~ "NodeDown|NodeUnreachable"
    equal:
      - cluster

  # Node not ready suppresses pod alerts on that node
  - source_matchers:
      - alertname = "KubeNodeNotReady"
    target_matchers:
      - alertname =~ "KubePodCrashLooping|KubePodNotReady|KubeDeploymentReplicasMismatch"
    equal:
      - node
      - cluster

  # PVC full suppresses application write errors (blocks symptom alerts when storage is the root cause)
  - source_matchers:
      - alertname = "KubePersistentVolumeFillingUp"
      - severity = "critical"
    target_matchers:
      - alertname = "AppWriteError"
    equal:
      - namespace
      - persistentvolumeclaim

Another trick I use: create a “maintenance window alert” to trigger inhibitions automatically rather than creating silences manually. Specifically, define a Prometheus recording rule named MaintenanceWindow with the label environment="production", active only during maintenance hours. The inhibition rule then automatically suppresses all warning/info alerts during that window — no manual intervention needed.

Testing and Monitoring

Verifying Inhibition Rules Work Correctly

# View all active alerts
amtool alert query

# View alerts including inhibited and silenced ones
amtool alert query --silenced --inhibited

# Filter by specific label
amtool alert query severity=critical environment=production

# Validate the config file before reloading
amtool check-config /etc/alertmanager/alertmanager.yml

When an alert is being suppressed unexpectedly, use the API directly to debug:

# View alerts along with the reason they are inhibited
curl -s 'http://localhost:9093/api/v2/alerts?inhibited=true&silenced=true' | \
  jq '.[] | {alertname: .labels.alertname, state: .status.state, inhibitedBy: .status.inhibitedBy}'

Alertmanager Metrics for Monitoring Effectiveness

Scrape Alertmanager’s own metrics — the fastest way to have Prometheus monitor itself:

# prometheus.yml
scrape_configs:
  - job_name: 'alertmanager'
    static_configs:
      - targets: ['localhost:9093']

The most useful metrics:

  • alertmanager_alerts{state="active"} — number of currently active alerts
  • alertmanager_alerts{state="suppressed"} — number of suppressed alerts (inhibit + silence combined)
  • alertmanager_silences{state="active"} — number of currently active silences
  • alertmanager_notifications_total — total notifications sent
  • alertmanager_notifications_failed_total — number of failed notifications

A query to track the suppression ratio in Grafana:

# Suppression ratio vs. total alerts
alertmanager_alerts{state="suppressed"} / ignoring(state) sum without(state)(alertmanager_alerts)

When this ratio increases after adding inhibition rules, that’s a good sign. The team receives fewer notifications, but each one is genuinely worth looking at.

Practical Lessons from the Field

After many rounds of tweaking the config, here’s what I’ve learned:

  • The equal field must exist in both source and target alerts — if either alert is missing that label, the rule won’t match and inhibition won’t work. This was a bug that cost me a lot of debugging time the first time around.
  • Avoid overly broad inhibitions — only suppress alerts that are genuinely symptoms of the source alert, not every alert from the same environment.
  • Reloading config doesn’t require a restart: curl -X POST http://localhost:9093/-/reload is enough.
  • Silences are not a long-term solution — if you find yourself creating a silence repeatedly for the same alert, it’s a sign the threshold is wrong or an inhibition rule needs to be added.

If you already have a basic Alertmanager running, adding inhibition rules takes about 30 minutes. Daily notifications can drop by 50–70% depending on your system. More importantly: the team will start paying attention again, because every alert will have a real reason to fire.

Share: