Testing alerts: what every Monitoring developer should know
An alert that doesn't fire when it should is worse than no alert at all—it gives your team false confidence while real incidents slip through undetected. We've all seen the moment when a critical threshold breach gets missed because a rule had a syntax error, miscalculated state duration, or was never actually tested against real data. In this article, we'll walk through the practical tools and patterns that ensure your Prometheus alerts behave exactly as intended before they reach production.
Why alert testing matters
Alert logic sits at the intersection of two challenging domains: metrics collection and on-call incident response. A single typo in a PromQL expression can silently break an alert. A misconfigured for clause might wait too long to fire, letting an outage cascade. A threshold that works in staging might be wildly inappropriate for your production traffic patterns. Testing alerts isn't an afterthought—it's a core responsibility of monitoring developers.
The consequence of untested alerts is high-stakes debugging during an actual incident. You'll be woken at 2 AM, check your monitoring system, and find that the alert that should have fired hours ago simply didn't. By then, customer impact is already significant. We prevent this by validating alert rules the same way we validate application code: with automated tests, syntax checks, and simulation exercises.
Unit testing with promtool
Prometheus includes a built-in command-line tool called promtool that gives us a powerful testing framework. Unlike integration tests that require a running Prometheus server, promtool evaluates alert rules against synthetic time-series data in isolation. This is fast, deterministic, and ideal for CI/CD pipelines.
The basic workflow is straightforward: you define test cases that specify input metrics, expected alert output, and timing. promtool then loads your Prometheus rule files and evaluates them against those inputs, comparing the results to your expectations.
Here's a practical example. Suppose we have a simple alert rule that fires when CPU usage exceeds 80% for more than 5 minutes:
groups:
- name: cpu_alerts
interval: 1m
rules:
- alert: HighCPUUsage
expr: node_cpu_seconds_total > 0.8
for: 5m
annotations:
summary: "CPU usage is high on {{ $labels.instance }}"We'd write a test file (typically cpu_alerts_test.yml) that looks like this:
rule_files:
- 'alerts.yml'
evaluation_interval: 1m
tests:
- interval: 1m
input_series:
- series: 'node_cpu_seconds_total{instance="server1"}'
values: '0.5 0.5 0.5 0.5 0.5 0.85 0.85 0.85 0.85 0.85 0.85'
alert_rule_test:
- eval_time: 5m
alerting: []
- eval_time: 10m
alerting:
- exp_labels:
alertname: HighCPUUsage
instance: server1
exp_annotations:
summary: 'CPU usage is high on server1'What's happening here: we're simulating a metric that starts at 0.5, stays there for 5 minutes, then jumps to 0.85 and stays elevated. At 5 minutes (eval_time: 5m), the rule hasn't fired yet—we expect an empty alerting array. At 10 minutes, we expect the alert to have fired because the threshold violation has now persisted for at least 5 minutes (the for duration).
Running this test is simple:
promtool test rules cpu_alerts_test.ymlIf your rule logic matches the test expectations, you'll see a success message. If not, you'll get a clear diff showing what fired versus what you expected. This immediate feedback loop catches logic errors before they reach production.
Simulating realistic time series in tests
The power of promtool becomes clear when we model complex scenarios. Real metrics are rarely linear—they spike, plateau, and recover. Testing against realistic patterns matters because alerts can behave differently under different data shapes.
Consider a memory leak scenario. Memory usage climbs gradually but consistently. We want an alert that fires if memory stays above 90% for 15 minutes, but we don't want false positives from temporary spikes. Here's how we'd test that:
tests:
- interval: 1m
input_series:
- series: 'node_memory_usage_percent{instance="app-server"}'
values: '60 62 64 66 68 70 72 74 76 78 80 82 84 86 88 90 91 92 93'
alert_rule_test:
- eval_time: 14m
alerting: []
- eval_time: 16m
alerting:
- exp_labels:
alertname: MemoryLeak
instance: app-serverNotice the gradual climb: each minute increases by roughly 2%. The alert fires at 16 minutes but not at 14 minutes. This catches a common mistake: when developers write rules that fire too early or too late.
We can also simulate recovery scenarios to ensure alerts clear properly:
alert_rule_test:
- eval_time: 20m
alerting:
- exp_labels:
alertname: MemoryLeak
- eval_time: 25m
alerting: []At 25 minutes, memory drops back to safe levels, and the alert clears. This tests both the firing and resolution logic, which is equally important.
Validating rule syntax in CI/CD
Before integration tests even run, we need to catch syntax errors. A malformed PromQL expression or a rule missing required fields should fail immediately in CI. This is separate from behavioral testing—it's about ensuring your rules are structurally valid.
The promtool check rules command does exactly this:
promtool check rules /path/to/rules.ymlThis validates:
- YAML structure is well-formed
- Required alert fields (expr, alert name) are present
- PromQL expressions are syntactically correct
- Label and annotation templates are valid Go templates
Add this to your CI pipeline before any unit tests run:
stages:
- validate
- test
validate_rules:
stage: validate
script:
- promtool check rules alerts/*.yml
only:
- merge_requests
- main
test_rules:
stage: test
script:
- promtool test rules **/*_test.yml
only:
- merge_requests
- mainThis two-stage approach catches syntactic issues in seconds, then runs behavioral tests to ensure logic correctness. By the time your rules merge to main, you've eliminated entire classes of errors.
Game days: testing alerts in production-like environments
Unit tests catch obvious mistakes, but they can't simulate the chaotic reality of production. This is where game days come in. A game day is a scheduled exercise where your team deliberately triggers scenarios that should fire alerts, observing whether they actually do and whether the team responds correctly.
Start small. Pick one critical alert—perhaps your "database down" alert. Schedule 30 minutes where you'll:
- Coordinate with your team in a meeting or Slack channel
- Simulate the condition that should trigger the alert (or trigger it for real in a staging environment)
- Wait and watch Prometheus and your alerting system
- Verify the alert fired and check that notification channels received it
- Confirm team members got paged or notified appropriately
- Discuss what went well and what needs improvement
A real example: you alert on Kafka consumer lag exceeding 100,000 messages. Instead of assuming the alert works, create a test topic with a fast producer and pause the consumer. Watch the lag climb. Does your alert fire within 2 minutes? Did Slack get notified? Did the on-call engineer's phone ring? If any part of that chain is broken, you've learned it now—not during a real incident.
Game days also reveal issues that unit tests can't catch: inconsistent alert severity levels, misleading alert messages, slow notification delivery, or missing runbooks. You might discover that your alert fires correctly but contains no guidance for the on-call engineer about how to respond.
Document the results of each game day. Over time, you'll build a mental map of which alerts are reliable and which ones need work. This is organizational knowledge that makes you more resilient.
Common mistakes
- Forgetting the for duration in tests: Many developers write unit tests that verify an alert fires on a single metric value, but forget to simulate the minimum duration required by the rule's for clause. Your test passes, but in production, transient spikes don't trigger the alert because they don't persist long enough. Always model the full time window in your test values.
- Using hardcoded absolute timestamps: Some teams write tests that depend on specific unix timestamps or wall-clock times. This breaks during daylight saving time transitions or when tests run at different times. Use relative eval_time instead, which is timezone-agnostic.
- Testing in isolation without related alerts: If you have an alert that suppresses another alert under certain conditions (using alert routing or record rules), test them together. Unit testing a single alert in isolation might pass, but the full system might behave differently.
- Never actually verifying alerts fire: Some teams write test files but never run them in CI. The tests sit dormant, gradually rotting as rules change. Integrate rule testing into your CI/CD and make it a blocking step for pull requests.
- Ignoring label cardinality in tests: If your alert depends on specific labels (instance, service, environment), verify that your test series include those labels and that your rule correctly references them. A rule like
group_by(instance)will behave differently if the instance label is missing.
Key takeaways
promtool test ruleslets you validate alert logic against synthetic time series before production, catching logic errors quickly- Simulate realistic metric patterns in tests—gradual climbs, temporary spikes, and recovery scenarios—so your alerts behave correctly under real-world conditions
- Run
promtool check rulesin CI as a first-pass validation gate to catch syntax errors before behavioral tests - Use game days to test alerts in production-like environments and verify the entire incident response chain works (alerting, notifications, team response)
- Testing alerts is not optional for production systems; untested alerts create false confidence and allow incidents to slip through






Comments
Loading comments…