<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: DevHelm</title>
    <description>The latest articles on DEV Community by DevHelm (@devhelm).</description>
    <link>https://dev.to/devhelm</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3936382%2Fe8a13abc-de71-41f3-a5eb-70eb7efde5e6.png</url>
      <title>DEV Community: DevHelm</title>
      <link>https://dev.to/devhelm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devhelm"/>
    <language>en</language>
    <item>
      <title>Monitoring as Code: Why Your Monitors Should Live in Git</title>
      <dc:creator>DevHelm</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:29:29 +0000</pubDate>
      <link>https://dev.to/devhelm/monitoring-as-code-why-your-monitors-should-live-in-git-4456</link>
      <guid>https://dev.to/devhelm/monitoring-as-code-why-your-monitors-should-live-in-git-4456</guid>
      <description>&lt;p&gt;Your infrastructure is defined in Terraform. Your application deploys through CI/CD. Your database schema migrates through version-controlled files. But your monitors — the thing that tells you everything else is working — live in a vendor's web UI, maintained by whoever clicked the buttons last, with no history, no review process, and no way to recreate them if the vendor loses the configuration.&lt;/p&gt;

&lt;p&gt;Monitoring as code is the practice of defining monitors, alert channels, notification policies, escalation policies, and status pages in version-controlled configuration files that deploy through the same pipeline as your application. When a developer changes the checkout flow, the monitor that watches the checkout flow is updated in the same pull request — not three days later when someone remembers to update the dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why monitors rot in a web UI
&lt;/h2&gt;

&lt;p&gt;Every team that maintains monitors in a vendor dashboard eventually hits the same failure modes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration drift.&lt;/strong&gt; Two engineers edit the same monitor at the same time. One changes the threshold, the other changes the URL. The vendor keeps the last write. Nobody knows what the "correct" state is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orphaned monitors.&lt;/strong&gt; A service is decommissioned. Its monitors keep running, alerting on expected failures, training the team to ignore the alert channel. Six months later a real failure in that channel goes unnoticed because everyone learned to tune it out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Undocumented changes.&lt;/strong&gt; Someone increased the alert threshold from 500ms to 5000ms during an incident "temporarily." There is no record of who changed it, when, or why. The threshold never goes back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unreviewed configuration.&lt;/strong&gt; A new engineer creates a monitor that alerts the entire team on every 404 response. No peer review caught it because there is no review process for monitor changes in a web UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disaster recovery failure.&lt;/strong&gt; The vendor has an outage. Or you switch vendors. Your monitoring configuration — dozens of monitors, alert channels, notification policies — cannot be recreated because it was never captured in a reproducible format.&lt;/p&gt;

&lt;h2&gt;
  
  
  What monitoring as code looks like in practice
&lt;/h2&gt;

&lt;p&gt;Monitoring as code means your repository contains declarative files that define your monitoring setup. The format varies by tooling — YAML, HCL, JSON, TypeScript — but the principle is the same: &lt;strong&gt;the repository is the source of truth, and the running state is derived from it.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# monitors/checkout-api.yaml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout API Health&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.example.com/v1/checkout/health&lt;/span&gt;
&lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
&lt;span class="na"&gt;frequency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
&lt;span class="na"&gt;regions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;us-east&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;eu-west&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ap-southeast&lt;/span&gt;
&lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status_code&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;response_time&lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1000ms&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;json_body&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$.status&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;healthy"&lt;/span&gt;
&lt;span class="na"&gt;alerts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pagerduty-checkout-team&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slack-engineering&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
    &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;degraded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# monitors.tf — Terraform approach&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"devhelm_monitor"&lt;/span&gt; &lt;span class="s2"&gt;"checkout_api"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Checkout API Health"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http"&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://api.example.com/v1/checkout/health"&lt;/span&gt;
  &lt;span class="nx"&gt;frequency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;

  &lt;span class="nx"&gt;regions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"us-east"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"eu-west"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ap-southeast"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;assertions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;status_code&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="nx"&gt;response_time&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="nx"&gt;json_path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"$.status"&lt;/span&gt;
    &lt;span class="nx"&gt;expected_value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"healthy"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;alert_channels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;devhelm_alert_channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pagerduty_checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;devhelm_alert_channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slack_engineering&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both formats express the same intent. The HCL version integrates with Terraform's plan/apply workflow. The YAML version integrates with a CLI (&lt;code&gt;devhelm apply -f monitors/&lt;/code&gt;). Either way, the monitor definition lives in Git.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff: review, history, and reproducibility
&lt;/h2&gt;

&lt;p&gt;Once monitors are code, you get everything version control provides:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pull request review for monitor changes.&lt;/strong&gt; When someone changes an alert threshold, the diff shows up in a PR. A teammate can ask "why did you change the checkout timeout from 1s to 5s?" before it ships. Bad configurations get caught before they reach production — the same way bad code does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git blame for debugging.&lt;/strong&gt; "Who changed this monitor, and when?" is a &lt;code&gt;git log&lt;/code&gt; query, not a support ticket to the vendor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Atomic deploys with the application.&lt;/strong&gt; When a developer renames an endpoint from &lt;code&gt;/v1/users&lt;/code&gt; to &lt;code&gt;/v2/users&lt;/code&gt;, the monitor definition updates in the same commit. The monitor never points at a dead endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment parity.&lt;/strong&gt; The same monitor definitions deploy to staging and production with environment-specific variables. Staging monitors catch issues before production monitors do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disaster recovery.&lt;/strong&gt; If you need to recreate your entire monitoring setup — new vendor, new account, after an outage — you run one command: &lt;code&gt;devhelm apply -f monitors/&lt;/code&gt; or &lt;code&gt;terraform apply&lt;/code&gt;. Everything recreates from the source of truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit trail.&lt;/strong&gt; For SOC 2 and ISO 27001, you need evidence that monitoring changes are reviewed and authorized. Git history provides this automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;A mature monitoring-as-code workflow looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Developer changes application code
2. Developer updates monitor definition in the same branch
3. PR review covers both code and monitoring changes
4. CI validates monitor syntax (lint, dry-run)
5. Merge to main triggers deploy pipeline
6. Application deploys first
7. Monitors deploy/update second (same pipeline)
8. Monitors validate the deploy succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 7 is the key: monitors deploy through CI/CD, not through a web UI. The running state always matches what's in the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should be code — and what should not
&lt;/h2&gt;

&lt;p&gt;Not everything needs to be version-controlled configuration:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should be code:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monitor definitions (URL, frequency, assertions, regions).&lt;/li&gt;
&lt;li&gt;Alert channel configuration (which Slack channel, which PagerDuty service).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devhelm.io/blog/monitoring-alerts" rel="noopener noreferrer"&gt;Notification policy rules&lt;/a&gt; (which severity routes where).&lt;/li&gt;
&lt;li&gt;Status page component mappings (which monitors feed which status page components).&lt;/li&gt;
&lt;li&gt;Escalation policies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Can stay in a UI:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-time ad-hoc debugging checks (temporary monitors during incident investigation).&lt;/li&gt;
&lt;li&gt;Dashboard visualizations (the &lt;em&gt;view&lt;/em&gt; of data, not the data source).&lt;/li&gt;
&lt;li&gt;Historical alert data and incident timelines (these are records, not configuration).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule: if losing it would require manual recreation, it should be code. If it's ephemeral or a view of data, a UI is fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common objections — and responses
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Our team isn't technical enough for config files."&lt;/strong&gt; If your team manages monitors, they're technical enough to edit YAML. A config file with clear field names is less complex than navigating a 15-field form in a web UI. And unlike the form, the config file has documentation, examples, and review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"We'd need to deploy every time we change a monitor."&lt;/strong&gt; Yes — that's the point. An unreviewed, un-deployed change to a production monitor is exactly as risky as an unreviewed change to production code. The deploy pipeline is the safety net.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What about quick changes during an incident?"&lt;/strong&gt; Some tools support both: config-as-code for steady-state, with a UI for temporary overrides that sync back to the repo. If your tool doesn't, keep a documented escape hatch ("during incidents, mute via UI; file a follow-up to update the config post-incident").&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Export your current monitors.&lt;/strong&gt; Most tools have an export or API endpoint. Capture your current state into files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a format.&lt;/strong&gt; YAML for simplicity, HCL for Terraform users, TypeScript/JSON for programmatic generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version control them.&lt;/strong&gt; Commit to your application repository (same repo = atomic changes) or a dedicated infrastructure repo (if you manage many services).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a deploy step.&lt;/strong&gt; Wire your CI/CD to apply monitor definitions on merge to main. Start with a dry-run step that validates syntax.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce the workflow.&lt;/strong&gt; Once monitors deploy from code, disable (or restrict) direct UI edits so drift cannot accumulate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first time a PR catches a bad monitor change before it reaches production, the investment pays for itself. The second time — when you recreate your entire monitoring setup in a new environment in 30 seconds — it pays for itself again.&lt;/p&gt;

&lt;p&gt;Set up monitors as code with a CLI (&lt;code&gt;devhelm apply&lt;/code&gt;), a Terraform provider, or the dashboard — and a status page that updates from the same monitor data — at &lt;a href="https://app.devhelm.io" rel="noopener noreferrer"&gt;app.devhelm.io&lt;/a&gt;. Your first monitor is live in 60 seconds, no credit card.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devhelm.io/blog/monitoring-as-code" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>guides</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>API Testing vs API Monitoring: Different Problems, Different Tools</title>
      <dc:creator>DevHelm</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:28:53 +0000</pubDate>
      <link>https://dev.to/devhelm/api-testing-vs-api-monitoring-different-problems-different-tools-586</link>
      <guid>https://dev.to/devhelm/api-testing-vs-api-monitoring-different-problems-different-tools-586</guid>
      <description>&lt;p&gt;API testing and API monitoring share a surface similarity — both send requests to endpoints and check responses — which is why teams confuse them and assume one covers the other. They do not. They solve different problems, at different times, against different failure modes.&lt;/p&gt;

&lt;p&gt;API testing answers: &lt;strong&gt;"does this code work correctly before we deploy it?"&lt;/strong&gt; API monitoring answers: &lt;strong&gt;"is this endpoint working correctly right now, in production, for real users?"&lt;/strong&gt; The first catches bugs. The second catches incidents. A test suite that passes in CI cannot tell you that your production database connection pool is exhausted, that a third-party API your service depends on is returning 503, or that a deploy rolled out a misconfigured environment variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  When each runs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;API Testing&lt;/th&gt;
&lt;th&gt;API Monitoring&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Before deploy (CI/CD pipeline)&lt;/td&gt;
&lt;td&gt;After deploy (continuous, on a schedule)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Triggered by&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Code change (push, PR, merge)&lt;/td&gt;
&lt;td&gt;Time (every 30s, 1min, 5min)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Environment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Staging, test, local&lt;/td&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Failure means&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bug in the code; block the deploy&lt;/td&gt;
&lt;td&gt;Incident in production; alert on-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Duration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minutes (test suite runtime)&lt;/td&gt;
&lt;td&gt;Forever (runs until you stop it)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This timing difference is fundamental. A test runs once per code change and validates correctness. A monitor runs continuously and validates availability. You need both because production fails in ways that tests cannot predict.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each catches — and what each misses
&lt;/h2&gt;

&lt;h3&gt;
  
  
  API testing catches
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logic errors.&lt;/strong&gt; An endpoint returns the wrong status code for an edge case. A query parameter is parsed incorrectly. A validation rule rejects valid input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regression.&lt;/strong&gt; A refactor broke an endpoint that was working last week. A dependency upgrade changed behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract violations.&lt;/strong&gt; The response schema changed — a field was renamed, a type changed from string to number, a required field became nullable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance regression.&lt;/strong&gt; A new database query added 2 seconds of latency that wasn't there before (if your test suite includes performance assertions).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  API testing misses
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure failures.&lt;/strong&gt; The database is up in CI but the production replica is lagging 30 seconds behind. The Redis cluster lost a node. The connection pool is exhausted under production load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party dependency failures.&lt;/strong&gt; Your payment provider's API is returning 503. The OAuth provider changed their JWKS endpoint. A CDN edge is serving stale certificates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration drift.&lt;/strong&gt; A deploy shipped with the wrong environment variable. The production secret rotated but the app is using the old one. A feature flag was toggled off accidentally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradual degradation.&lt;/strong&gt; Latency creeping up over hours as a memory leak consumes the heap. Disk filling up until writes start failing. Connection pool exhaustion under sustained load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regional failures.&lt;/strong&gt; The endpoint works from your CI runner in us-east-1 but returns timeouts from eu-west-1 because of a routing misconfiguration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  API monitoring catches
&lt;/h3&gt;

&lt;p&gt;Everything testing misses — because it runs in production, continuously, from multiple locations. A monitor does not know what the "correct" behavior is for every edge case (that's testing's job). It knows what "working" looks like: responds within a latency budget, returns expected status codes, response body contains expected fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  API monitoring misses
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logic correctness.&lt;/strong&gt; A monitor can assert that &lt;code&gt;/users/123&lt;/code&gt; returns a 200 with a &lt;code&gt;name&lt;/code&gt; field. It cannot assert that the &lt;em&gt;value&lt;/em&gt; of the name field is correct for that specific user ID — that's a test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge cases.&lt;/strong&gt; A monitor runs one synthetic request on a schedule. It does not cover the 47 edge cases your test suite validates (malformed input, concurrent writes, boundary conditions).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-deploy validation.&lt;/strong&gt; By definition, monitoring runs post-deploy. If you ship a broken endpoint, monitoring tells you &lt;em&gt;after&lt;/em&gt; users are affected.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Same tool, different workflows
&lt;/h2&gt;

&lt;p&gt;The confusion deepens because the same tools appear in both workflows. Postman is used for both ad-hoc API testing and scheduled monitors. Playwright is used for both E2E tests in CI and &lt;a href="https://devhelm.io/blog/playwright-monitoring" rel="noopener noreferrer"&gt;production browser monitors&lt;/a&gt;. The difference is not the tool — it's the workflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                    Development                           │
│                                                         │
│  Write code → Run tests → Push → CI runs test suite     │
│                                    │                    │
│                                    ▼                    │
│                              Tests pass?                │
│                              Yes → Deploy               │
│                              No  → Fix                  │
│                                                         │
└─────────────────────────────────────────────────────────┘
                              │
                              ▼ deploy
┌─────────────────────────────────────────────────────────┐
│                    Production                            │
│                                                         │
│  Monitor runs every 30s from 3 regions                  │
│      │                                                  │
│      ▼                                                  │
│  Response OK?                                           │
│  Yes → Continue                                         │
│  No  → Alert on-call → Incident → [MTTR clock starts]  │
│                                                         │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testing gates the deploy. Monitoring watches the deploy's aftermath. Skipping either leaves a gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between them — and what fills it
&lt;/h2&gt;

&lt;p&gt;Teams with mature API infrastructure have three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unit + integration tests&lt;/strong&gt; — validate logic correctness in isolation and against test databases. Run in CI, block merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract tests&lt;/strong&gt; — validate that the API response schema matches what consumers expect. Catch breaking changes before deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production monitoring&lt;/strong&gt; — validate availability, latency, and response correctness continuously. Catch incidents after deploy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The gap between layers 2 and 3 is where most P0 incidents originate. The code is correct (tests prove it). The contract is intact (schema hasn't changed). But the &lt;em&gt;system&lt;/em&gt; fails because of infrastructure, configuration, or dependency issues that no pre-deploy validation covers.&lt;/p&gt;

&lt;p&gt;Some teams add a fourth layer: &lt;strong&gt;synthetic API tests against production&lt;/strong&gt; — essentially API tests that run post-deploy against real infrastructure, before the traffic shifts fully. Canary deploys and smoke tests fill this gap, but they are time-bounded. Monitoring is continuous.&lt;/p&gt;

&lt;h2&gt;
  
  
  What good API monitoring actually asserts
&lt;/h2&gt;

&lt;p&gt;A common mistake is monitoring only the HTTP status code. A 200 OK from an endpoint that returns an empty JSON body, a 200 that returns an error message in the body, or a 200 that takes 12 seconds to arrive — these are all failures that status-code monitoring misses.&lt;/p&gt;

&lt;p&gt;Good API monitoring asserts on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Status code&lt;/strong&gt; — the baseline, but not sufficient alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response time&lt;/strong&gt; — within your latency SLO (e.g., p95 &amp;lt; 500ms).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response body content&lt;/strong&gt; — the expected fields exist and contain valid data. &lt;code&gt;data.users&lt;/code&gt; is a non-empty array. &lt;code&gt;meta.total&lt;/code&gt; is a number &amp;gt; 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response headers&lt;/strong&gt; — cache headers are present, CORS headers are correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate validity&lt;/strong&gt; — the &lt;a href="https://devhelm.io/blog/what-ssl-error-means-and-how-to-fix-it" rel="noopener noreferrer"&gt;TLS certificate&lt;/a&gt; won't expire within 14 days.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example: a monitor that checks status, latency, and body content&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;%{http_code} %{time_total}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://api.example.com/v1/health | &lt;span class="se"&gt;\&lt;/span&gt;
  jq &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'.status == "healthy" and .database == "connected"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"We have tests, we don't need monitoring."&lt;/strong&gt; Tests validate code correctness. They cannot validate production infrastructure health, third-party availability, or configuration correctness. These are different failure classes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"We have monitoring, we don't need tests."&lt;/strong&gt; Monitoring tells you something broke. It doesn't tell you &lt;em&gt;what&lt;/em&gt; the correct behavior is, doesn't validate edge cases, and doesn't prevent broken code from deploying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Our staging environment is identical to production."&lt;/strong&gt; It is not. Staging has different load, different data, different third-party credentials (often sandbox/test keys), and different infrastructure (usually smaller). Passing in staging does not guarantee passing in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"We monitor the health endpoint, that covers it."&lt;/strong&gt; A health endpoint that returns 200 proves the process is running. It does not prove that the &lt;code&gt;/checkout&lt;/code&gt; endpoint can reach the payment provider, that the &lt;code&gt;/search&lt;/code&gt; endpoint can query Elasticsearch, or that the &lt;code&gt;/upload&lt;/code&gt; endpoint can write to S3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with both
&lt;/h2&gt;

&lt;p&gt;If you have tests but no monitoring: your next incident will be a production failure that tests could never have caught. Add multi-region API monitoring with response body assertions on your 5 most critical endpoints.&lt;/p&gt;

&lt;p&gt;If you have monitoring but no tests: your next bug will ship to production before monitoring detects it, because it will affect an edge case the monitor doesn't cover. Add integration tests for your API's critical paths.&lt;/p&gt;

&lt;p&gt;Set up API monitoring with multi-region checks, response body assertions, latency thresholds, and a status page that updates from the same monitoring data at &lt;a href="https://app.devhelm.io" rel="noopener noreferrer"&gt;app.devhelm.io&lt;/a&gt; — your first monitor is live in 60 seconds, no credit card. For the full tooling landscape, see &lt;a href="https://devhelm.io/blog/best-api-monitoring-tools" rel="noopener noreferrer"&gt;the best API monitoring tools in 2026&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devhelm.io/blog/api-testing-vs-api-monitoring" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>guides</category>
      <category>reliability</category>
    </item>
    <item>
      <title>How to Set Up Browser Monitoring with Playwright</title>
      <dc:creator>DevHelm</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:28:16 +0000</pubDate>
      <link>https://dev.to/devhelm/how-to-set-up-browser-monitoring-with-playwright-1bjh</link>
      <guid>https://dev.to/devhelm/how-to-set-up-browser-monitoring-with-playwright-1bjh</guid>
      <description>&lt;p&gt;You want to know — continuously, automatically — that a user can complete a critical task in your production application. Not that the server returns 200. Not that the homepage loads HTML. That a real browser can navigate to your checkout page, fill in payment details, click "Pay now," and see "Order confirmed."&lt;/p&gt;

&lt;p&gt;This tutorial walks through setting up browser monitoring with Playwright from scratch: writing a check that asserts on user-visible outcomes, capturing forensic evidence on failure, scheduling it against production, and routing alerts to the right channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 18+&lt;/strong&gt; with an existing project (or create one: &lt;code&gt;mkdir browser-monitors &amp;amp;&amp;amp; cd browser-monitors &amp;amp;&amp;amp; npm init -y&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playwright installed&lt;/strong&gt;: &lt;code&gt;npm install -D @playwright/test &amp;amp;&amp;amp; npx playwright install chromium&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A production (or staging) URL&lt;/strong&gt; to monitor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A dedicated synthetic test account&lt;/strong&gt; — credentials stored in environment variables, never in source.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1 — Write a check that asserts on outcomes
&lt;/h2&gt;

&lt;p&gt;A monitoring check is not a test in the QA sense — it does not cover edge cases or validate business logic. It answers one question: &lt;strong&gt;can a user complete this journey right now?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// monitors/checkout.spec.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout completes successfully&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/products&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Add item to cart&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Add to cart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cart&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Begin checkout&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Checkout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Fill payment (test card — non-charging token)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SYNTHETIC_EMAIL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Card number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4242424242424242&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Expiry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;12/28&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CVC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Submit and assert on the user-visible outcome&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pay now&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Order confirmed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assertion is the key: &lt;code&gt;page.getByText("Order confirmed")&lt;/code&gt; proves the entire chain worked — frontend rendering, API call, payment provider round-trip, database write, and confirmation page render. A status code check would miss failures in any of those layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Use stable selectors that survive deploys
&lt;/h2&gt;

&lt;p&gt;Browser checks break when the UI changes. Minimize breakage by using selectors that survive visual redesigns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fragile — breaks when CSS classes change&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.btn-primary.checkout-submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Stable — survives any visual redesign&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pay now&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Also stable — explicit test IDs for elements without semantic roles&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order-total&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Priority order for selectors:&lt;/strong&gt; ARIA roles &amp;gt; &lt;code&gt;getByLabel&lt;/code&gt; / &lt;code&gt;getByText&lt;/code&gt; &amp;gt; &lt;code&gt;data-testid&lt;/code&gt; &amp;gt; CSS selectors. The first three are tied to user-visible semantics that rarely change; CSS classes change on every design sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Wait for conditions, never for time
&lt;/h2&gt;

&lt;p&gt;The number-one cause of flaky monitoring checks is fixed sleeps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Flaky: assumes 3 seconds is enough, or wastes 3 seconds when it's fast&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;balance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Stable: retries automatically until the condition holds or times out&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;balance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;balance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toHaveText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright's web-first assertions (&lt;code&gt;expect(locator).toBeVisible()&lt;/code&gt;, &lt;code&gt;expect(locator).toHaveText()&lt;/code&gt;) retry until the condition is met. They pass in 200ms when the app is fast and only fail when something is genuinely broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Capture evidence on failure
&lt;/h2&gt;

&lt;p&gt;When a production check fails at 3 AM, the on-call engineer needs to diagnose &lt;em&gt;why&lt;/em&gt; without re-running it. Configure Playwright to preserve forensic evidence on failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;testDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./monitors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;only-on-failure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retain-on-failure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;video&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;retain-on-failure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On failure, you get: a &lt;strong&gt;screenshot&lt;/strong&gt; of the page state at the moment of failure, a &lt;strong&gt;trace&lt;/strong&gt; (full DOM snapshot + network log + console, viewable at &lt;code&gt;trace.playwright.dev&lt;/code&gt;), and a &lt;strong&gt;video&lt;/strong&gt; of the entire check execution. The &lt;code&gt;retries: 1&lt;/code&gt; setting re-runs a failed check once before declaring failure — this eliminates most transient network blips without masking real incidents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — Schedule checks against production
&lt;/h2&gt;

&lt;p&gt;A monitoring check runs on a clock, not on commits. The simplest scheduler is a GitHub Actions cron workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/browser-monitor.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;browser-monitoring&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*/5&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;checkout-check&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;20&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci &amp;amp;&amp;amp; npx playwright install --with-deps chromium&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test monitors/checkout.spec.ts&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://app.yourproduct.com&lt;/span&gt;
          &lt;span class="na"&gt;SYNTHETIC_EMAIL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SYNTHETIC_EMAIL }}&lt;/span&gt;
          &lt;span class="na"&gt;SYNTHETIC_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SYNTHETIC_PASSWORD }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failure()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failure-evidence-${{ github.run_id }}&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test-results/&lt;/span&gt;
          &lt;span class="na"&gt;retention-days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you browser monitoring today for free. The limits are honest: cron floors at ~1-minute granularity, GitHub-hosted runners give you one region (US), and you need to wire alerting separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — Add multi-region coverage
&lt;/h2&gt;

&lt;p&gt;Failures are often regional — a CDN edge cert expires in one zone, &lt;a href="https://devhelm.io/blog/how-to-fix-slow-dns-lookup" rel="noopener noreferrer"&gt;DNS propagates unevenly&lt;/a&gt;, a deploy rolls out region by region. Running checks from a single location is blind to all of these.&lt;/p&gt;

&lt;p&gt;For a self-hosted approach, deploy the same Playwright check as a container to multiple regions and report results to a central collector. For a managed approach, platforms like &lt;a href="https://devhelm.io/blog/checkly-alternative" rel="noopener noreferrer"&gt;Checkly&lt;/a&gt; run your Playwright scripts from 20+ regions with built-in scheduling and alerting.&lt;/p&gt;

&lt;p&gt;At minimum, run critical checks from two geographically distinct locations. If a check fails from one location but passes from others, you have a regional incident — a distinction that changes the severity and response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7 — Route failures to the right channel
&lt;/h2&gt;

&lt;p&gt;A failed browser check is only useful if it reaches a human who can act on it. Wire failure notifications to match your &lt;a href="https://devhelm.io/blog/incident-severity-levels" rel="noopener noreferrer"&gt;incident severity levels&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Revenue-critical journey failure&lt;/strong&gt; (checkout, signup) → page on-call via PagerDuty/OpsGenie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Important but not revenue-blocking&lt;/strong&gt; (settings page, profile update) → Slack channel, business hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secondary paths&lt;/strong&gt; (about page, blog) → logged, reviewed weekly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the GitHub Actions approach, add a failure notification step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Alert on failure&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failure()&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-H "Content-Type: application/json" \&lt;/span&gt;
            &lt;span class="s"&gt;-d '{"text": "Browser check FAILED: checkout journey. Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8 — Monitor the layer underneath
&lt;/h2&gt;

&lt;p&gt;A browser journey sits on top of API endpoints that can fail independently. When your checkout check goes red, you want to immediately know whether the problem is in the frontend, the API, or a third-party dependency.&lt;/p&gt;

&lt;p&gt;Monitoring those API endpoints directly — with response body assertions, latency thresholds, and multi-region coverage — turns "the whole flow is red" into "the &lt;code&gt;/api/payment-intent&lt;/code&gt; endpoint is returning 500 from us-east." That diagnostic specificity is the difference between a fast &lt;a href="https://devhelm.io/blog/mttr-full-form" rel="noopener noreferrer"&gt;MTTR&lt;/a&gt; and a slow one.&lt;/p&gt;

&lt;p&gt;Set up API and uptime monitors for the endpoints your browser checks depend on — with multi-region checks, config-as-code, and a status page that updates from the same monitoring data — at &lt;a href="https://app.devhelm.io" rel="noopener noreferrer"&gt;app.devhelm.io&lt;/a&gt;. Your first monitor is live in 60 seconds, no credit card.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to read next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://devhelm.io/blog/playwright-monitoring" rel="noopener noreferrer"&gt;Playwright monitoring&lt;/a&gt; — turn your existing E2E test suite into production monitors.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devhelm.io/blog/headless-browser-monitoring" rel="noopener noreferrer"&gt;Headless browser monitoring&lt;/a&gt; — when you need browser checks vs when HTTP is enough.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devhelm.io/blog/synthetic-monitoring-best-practices" rel="noopener noreferrer"&gt;Synthetic monitoring best practices&lt;/a&gt; — intervals, de-flaking, test data safety.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devhelm.io/blog/best-synthetic-monitoring-tools" rel="noopener noreferrer"&gt;Best synthetic monitoring tools&lt;/a&gt; — when the cron approach stops being enough.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devhelm.io/blog/browser-monitoring-playwright" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>guides</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Headless Browser Monitoring: What It Is and When You Need It</title>
      <dc:creator>DevHelm</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:27:40 +0000</pubDate>
      <link>https://dev.to/devhelm/headless-browser-monitoring-what-it-is-and-when-you-need-it-b15</link>
      <guid>https://dev.to/devhelm/headless-browser-monitoring-what-it-is-and-when-you-need-it-b15</guid>
      <description>&lt;p&gt;An HTTP check tells you a server responded. A headless browser check tells you a user could actually complete a task. The difference matters every time a deploy ships a JavaScript error that breaks a button, a third-party script blocks rendering, a payment iframe fails to load, or a frontend routing change returns 200 OK with an empty page body.&lt;/p&gt;

&lt;p&gt;Headless browser monitoring runs a real browser — Chromium, headless, with no visible window — against your production application on a recurring schedule. It clicks buttons, fills forms, waits for elements, and asserts on outcomes exactly the way a user would, except it does it every 30 seconds from multiple geographic regions without getting tired or forgetting to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;A headless browser monitor executes a script — typically written in &lt;a href="https://devhelm.io/blog/playwright-monitoring" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; or Puppeteer — inside a Chromium instance that runs without a display. The script performs a user journey: navigate to a URL, interact with the page, assert that the expected outcome appears.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://app.example.com/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;synthetic@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SYNTHETIC_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sign in&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;**/dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Welcome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isVisible&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Login journey failed: dashboard heading not visible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this script runs on a schedule — say every 60 seconds from three regions — it becomes a monitor. A failure triggers an alert. The evidence (screenshot, trace, network waterfall) ships with the alert so on-call knows &lt;em&gt;what&lt;/em&gt; broke without re-running the check manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  What headless browser monitoring catches that API checks miss
&lt;/h2&gt;

&lt;p&gt;An API check validates that an endpoint returns a response matching expectations. A headless browser check validates that &lt;strong&gt;the assembled experience works end-to-end&lt;/strong&gt;. The gap between those two is where most user-facing incidents live:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client-side JavaScript errors.&lt;/strong&gt; A deploy ships a bundling error that throws &lt;code&gt;Uncaught TypeError&lt;/code&gt; on the checkout page. The API returns 200 with valid JSON. The button does nothing. An HTTP check passes. A browser check fails because &lt;code&gt;page.getByText("Order confirmed")&lt;/code&gt; never appears.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third-party script failures.&lt;/strong&gt; Your payment provider's iframe stops loading because their CDN has a regional outage. Your server is fine. Your API is fine. The user sees a blank payment box. Only a browser check that actually attempts the payment flow detects this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSS/layout regressions.&lt;/strong&gt; A deploy changes a z-index and the "Submit" button is now behind an overlay. The button exists in the DOM. The API endpoint works. But &lt;code&gt;page.getByRole("button", { name: "Submit" }).click()&lt;/code&gt; fails because the button is not interactable — it's obscured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SPA routing failures.&lt;/strong&gt; A React/Next.js app has a routing bug that renders a blank page on certain navigation paths. The server returns 200 with the HTML shell. The JavaScript that should render the page crashes silently. Only a browser check that navigates the path and asserts on rendered content catches this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication flow breakage.&lt;/strong&gt; OAuth redirects, SAML flows, cookie-based sessions — these involve multiple round trips across domains. An API check on any single endpoint passes. The combined flow fails because a redirect URI changed or a cookie domain attribute is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you need headless browser monitoring — and when you do not
&lt;/h2&gt;

&lt;p&gt;Headless browser monitoring is expensive relative to HTTP checks. A browser check consumes 10–100x more compute, takes 5–30 seconds to complete (versus ~200ms for an HTTP check), and requires maintaining scripts that break when the UI changes. You should use it selectively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need it when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Revenue-critical user journeys exist (checkout, signup, upgrade, payment method update).&lt;/li&gt;
&lt;li&gt;Your application is an SPA or heavily JavaScript-dependent — the server response alone does not represent the user experience.&lt;/li&gt;
&lt;li&gt;You depend on third-party iframes or scripts (payment, auth, analytics) that can break independently of your infrastructure.&lt;/li&gt;
&lt;li&gt;Your deploy pipeline does not include end-to-end tests that cover production-specific configurations (real OAuth, real payment providers, production CDN).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You do not need it when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your service is a pure API (no browser-rendered UI). &lt;a href="https://devhelm.io/blog/best-api-monitoring-tools" rel="noopener noreferrer"&gt;API monitoring&lt;/a&gt; with response body assertions covers this.&lt;/li&gt;
&lt;li&gt;The same Playwright tests already run in CI against a staging environment that mirrors production faithfully. (Though even then, a scheduled production run catches configuration drift and third-party failures — see &lt;a href="https://devhelm.io/blog/playwright-monitoring" rel="noopener noreferrer"&gt;Playwright monitoring&lt;/a&gt;.)&lt;/li&gt;
&lt;li&gt;Your pages are mostly static content served from a CDN. A simple HTTP check with content assertion is cheaper and sufficient.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The cost question
&lt;/h2&gt;

&lt;p&gt;Every headless browser monitor has three costs: &lt;strong&gt;compute&lt;/strong&gt; (running Chromium), &lt;strong&gt;maintenance&lt;/strong&gt; (updating scripts when the UI changes), and &lt;strong&gt;signal quality&lt;/strong&gt; (flaky checks that erode trust).&lt;/p&gt;

&lt;p&gt;Compute cost depends on your tooling choice. Managed platforms like &lt;a href="https://devhelm.io/blog/checkly-alternative" rel="noopener noreferrer"&gt;Checkly&lt;/a&gt; bill per-run (~$4–6.50 per 1,000 browser runs). Self-hosted approaches (GitHub Actions cron, dedicated containers) trade billing for infrastructure management. Either way, a single browser check at 30-second intervals from 3 regions is ~260,000 runs/month — budget accordingly.&lt;/p&gt;

&lt;p&gt;Maintenance cost scales with UI volatility. If your team ships frontend changes daily, browser checks will break often unless they use stable selectors (&lt;code&gt;data-testid&lt;/code&gt;, ARIA roles) rather than CSS classes or XPath. The &lt;a href="https://devhelm.io/blog/synthetic-monitoring-best-practices" rel="noopener noreferrer"&gt;best practices guide&lt;/a&gt; covers selector discipline in detail.&lt;/p&gt;

&lt;p&gt;Signal quality is the hidden cost. A browser check that flakes once a week trains your team to ignore it. Invest in confirm-on-failure (re-run from a second region before alerting), conditional waits (never &lt;code&gt;waitForTimeout&lt;/code&gt;), and isolated test accounts. A reliable check that alerts once a quarter is worth more than a flaky check that alerts twice a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Headless browser monitoring vs synthetic monitoring vs RUM
&lt;/h2&gt;

&lt;p&gt;These three terms overlap and confuse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Headless browser monitoring&lt;/strong&gt; is a specific implementation: run Chromium headlessly against production on a schedule. It is a subset of synthetic monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devhelm.io/blog/what-is-synthetic-monitoring" rel="noopener noreferrer"&gt;Synthetic monitoring&lt;/a&gt;&lt;/strong&gt; is the broader category: any proactive, scripted check against production — including HTTP pings, multi-step API checks, and browser checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://devhelm.io/blog/synthetic-monitoring-vs-rum" rel="noopener noreferrer"&gt;Real User Monitoring (RUM)&lt;/a&gt;&lt;/strong&gt; is passive: instrument real browser sessions and record what actual users experience.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Headless browser monitoring catches failures &lt;em&gt;before&lt;/em&gt; users encounter them. RUM catches failures &lt;em&gt;as&lt;/em&gt; users encounter them. The first is proactive; the second is reactive. Most teams need both — synthetic checks for critical journeys (instant detection), RUM for coverage breadth and performance baselines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;The fastest path from zero to a working headless browser monitor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write a Playwright test that asserts on your most important user journey (login, checkout, or the core product action).&lt;/li&gt;
&lt;li&gt;Run it locally against production: &lt;code&gt;npx playwright test --headed&lt;/code&gt; to verify it passes.&lt;/li&gt;
&lt;li&gt;Schedule it: a &lt;a href="https://devhelm.io/blog/playwright-monitoring" rel="noopener noreferrer"&gt;GitHub Actions cron workflow&lt;/a&gt; is the simplest starting point.&lt;/li&gt;
&lt;li&gt;Add failure evidence: screenshots, traces, and video on failure so alerts carry context.&lt;/li&gt;
&lt;li&gt;Wire alerting: route failures to your on-call channel, matched to your &lt;a href="https://devhelm.io/blog/incident-severity-levels" rel="noopener noreferrer"&gt;severity levels&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you have one check running reliably, add checks for your top 3–5 revenue-critical paths. Stop there until you've proven the maintenance cost is manageable at your team's deployment frequency.&lt;/p&gt;

&lt;p&gt;For the API endpoints and uptime layer underneath your browser checks — multi-region HTTP monitoring with response body assertions, config-as-code, and a status page that updates from the same data — start at &lt;a href="https://app.devhelm.io" rel="noopener noreferrer"&gt;app.devhelm.io&lt;/a&gt;. Your first monitor is live in 60 seconds, no credit card.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devhelm.io/blog/headless-browser-monitoring" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>guides</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Checkly Alternatives in 2026: Synthetic Monitoring Tools Compared</title>
      <dc:creator>DevHelm</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:27:03 +0000</pubDate>
      <link>https://dev.to/devhelm/checkly-alternatives-in-2026-synthetic-monitoring-tools-compared-4a76</link>
      <guid>https://dev.to/devhelm/checkly-alternatives-in-2026-synthetic-monitoring-tools-compared-4a76</guid>
      <description>&lt;p&gt;Checkly built its reputation on a single conviction: synthetic monitoring should be code, not click-through wizards. You write Playwright tests, Checkly runs them on a schedule from global locations, and you get alerts when a user journey breaks. For teams already fluent in TypeScript and Playwright, it fits like a natural extension of CI.&lt;/p&gt;

&lt;p&gt;But Checkly is not the only option, and its trade-offs become clear at scale. Per-run billing means a busy monitoring setup can produce surprise invoices — a single browser check running every 30 seconds from three regions generates 259,200 billable runs per month. There is no recorder for non-developers, no native on-call, and SSO requires the Enterprise plan. If any of those gaps matter to your team, an alternative deserves evaluation.&lt;/p&gt;

&lt;p&gt;We compared five alternatives across the dimensions where teams actually switch: pricing predictability, authoring mode, on-call integration, browser engine fidelity, and developer surface (CLI, Terraform, MCP).&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes Checkly good — and where teams hit limits
&lt;/h2&gt;

&lt;p&gt;Checkly's strengths are real. It runs your actual Playwright suites (multi-file, fixtures, stored state) with the highest fidelity in the category. The developer surface is deep: a CLI (&lt;code&gt;checkly test&lt;/code&gt; and &lt;code&gt;checkly deploy&lt;/code&gt;), a Terraform provider, Pulumi support, and Prometheus export. If your definition of &lt;a href="https://devhelm.io/blog/monitoring-as-code" rel="noopener noreferrer"&gt;monitoring as code&lt;/a&gt; is "my monitors live in the same repo as my app and deploy in the same pipeline," Checkly delivers that better than anyone else.&lt;/p&gt;

&lt;p&gt;The limits show up in three places:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing at scale.&lt;/strong&gt; Browser checks cost ~$4–6.50 per 1,000 runs, and the platform splits into three separately metered products (Synthetics, Alerting, Private Locations). A team with 20 browser checks at 1-minute intervals from 3 regions pays for ~2.6 million runs/month. That's $10,000–17,000/year just for the browser runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-developer authoring.&lt;/strong&gt; There is no recorder. Every check is TypeScript. If your QA team or product managers need to create checks, they cannot — they need a developer to write the code. This limits adoption to engineering teams only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On-call and incident management.&lt;/strong&gt; Checkly alerts via webhook, Slack, PagerDuty, or OpsGenie — but has no native escalation, rotation, or incident timeline. You need a separate on-call tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Datadog Synthetic Monitoring
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.datadoghq.com/product/synthetic-monitoring/" rel="noopener noreferrer"&gt;Datadog&lt;/a&gt; is the enterprise pick, and its differentiator is correlation. A failed browser check links directly to the APM trace, the infrastructure metrics, and the RUM session that explains it. No other tool in this list can show you "the checkout button failed because the payment-intent endpoint spiked to 4s latency because the Postgres replica lagged 12s behind primary" in a single pane.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it wins over Checkly:&lt;/strong&gt; nine test types (including mobile device), self-healing locators in the recorder, SOC 2 + ISO 27001 + HIPAA + FedRAMP compliance, SAML/SCIM with custom RBAC, and native integration with 700+ other Datadog products.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it loses:&lt;/strong&gt; Browser checks cost ~$12–18 per 1,000 runs — roughly 3x Checkly's rate. CI test runs draw from the same quota. The frequency floor in the UI is 5 minutes (1-minute requires a support ticket). The code-first story is weaker — you can write tests in JavaScript, but the workflow assumes the recorder as the starting point. Session replay sits behind separately-billed RUM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; enterprises already on Datadog that want synthetic checks correlated with full-stack observability and can absorb metered browser pricing. For the broader platform comparison, see &lt;a href="https://devhelm.io/blog/datadog-vs-dynatrace" rel="noopener noreferrer"&gt;Datadog vs Dynatrace&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grafana Cloud Synthetic Monitoring (k6)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://grafana.com/products/cloud/synthetic-monitoring/" rel="noopener noreferrer"&gt;Grafana Cloud&lt;/a&gt; has the most generous free tier in synthetic monitoring — 100,000 API plus 10,000 browser executions per month, no credit card — backed by the credibility of open-source k6. If your team values owning the stack and paying nothing to start, Grafana is the obvious candidate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it wins over Checkly:&lt;/strong&gt; free tier that actually covers meaningful usage, OSS foundation (k6 is MIT-licensed), Playwright-to-k6 script conversion, a first-party authoring MCP, and Terraform support. If you already run Grafana for dashboards, adding synthetic monitoring is a config change, not a vendor decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it loses:&lt;/strong&gt; past the free tier, browser pricing gets steep and confusing (~$50 per 10,000 executions, billed per-probe-per-minute). There is no in-product recorder — k6 Studio is a separate desktop app. The browser interval floor is 60 seconds. And the whole-stack complexity is real: you are adopting Grafana's ecosystem, not a focused monitoring tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; engineering teams that value OSS, need a strong free tier, and author everything in code. Not for teams that need a recorder or sub-minute browser intervals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Better Stack
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://betterstack.com" rel="noopener noreferrer"&gt;Better Stack&lt;/a&gt; bundles uptime monitoring, Playwright browser checks, incident management, on-call rotations, logs, and status pages in one product. Its native on-call and escalation are the best in this list — the one thing Checkly fundamentally lacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it wins over Checkly:&lt;/strong&gt; native on-call with escalation policies and rotations, a bundled status page, Playwright/Chromium engine (not Selenium), trace-viewer artifacts on failure, and a Terraform provider. For small teams that want monitoring + on-call + status pages in one bill, Better Stack eliminates three separate vendor relationships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it loses:&lt;/strong&gt; locations are coarse (four regional groups — US, EU, Asia, Australia). Private synthetic locations are weak and lightly documented. There is no AI authoring and no visual regression. Pricing is per-minute (~$1 per 100 Playwright-minutes) on top of a required $29/responder seat, which gets unpredictable at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; small-to-mid teams that want monitoring, on-call, and status pages bundled. Teams that need deep location control or high-volume browser checks at predictable pricing will hit limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sematext
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://sematext.com" rel="noopener noreferrer"&gt;Sematext&lt;/a&gt; is the one predictable pricing model in synthetic monitoring: a flat per-monitor fee (~$2 for HTTP, ~$7 for a browser monitor per month) with no per-run meter. The engine runs Playwright on Chromium, and private locations deploy as Docker containers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it wins over Checkly:&lt;/strong&gt; completely predictable billing. A team with 20 browser monitors pays $140/month regardless of check frequency or location count. No surprise invoices. No metering math. Docker-based private locations that work without an enterprise contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it loses:&lt;/strong&gt; the developer surface is minimal — no Terraform provider, no CLI for synthetics, no MCP. There is no recorder, no video capture, no HAR archive. Multi-step journeys only report the last page's metrics. The browser interval floor is 5 minutes. It is a thin feature set with one clear advantage: predictability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that want predictable per-monitor pricing on a handful of browser checks and do not need a developer surface or deep forensics.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you need the layer underneath
&lt;/h2&gt;

&lt;p&gt;All synthetic monitoring tools — Checkly included — run checks from the outside. They tell you &lt;em&gt;that&lt;/em&gt; a journey failed. They do not tell you &lt;em&gt;which&lt;/em&gt; API endpoint caused the failure, or whether the root cause is your infrastructure or a degraded third-party dependency.&lt;/p&gt;

&lt;p&gt;Layering &lt;a href="https://devhelm.io/blog/best-api-monitoring-tools" rel="noopener noreferrer"&gt;API monitoring&lt;/a&gt; underneath your browser checks turns "the checkout flow is red" into "the &lt;code&gt;/payment-intent&lt;/code&gt; endpoint is returning 500 because Stripe's API is degraded." That correlation is the difference between a 5-minute diagnosis and a 45-minute scramble.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;p&gt;The decision tree is shorter than the feature matrix suggests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You write Playwright and want maximum fidelity:&lt;/strong&gt; Checkly remains the best if you can manage per-run billing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need APM correlation:&lt;/strong&gt; Datadog, if you can absorb the price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want free and open source:&lt;/strong&gt; Grafana Cloud / k6.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need on-call bundled:&lt;/strong&gt; Better Stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need predictable billing above all:&lt;/strong&gt; Sematext.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need API and uptime monitoring with config-as-code underneath your synthetic layer:&lt;/strong&gt; &lt;a href="https://app.devhelm.io" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt; — 50 monitors on the free tier, flat pricing, multi-region, with a status page that updates from the same check data. Your first monitor takes 60 seconds, no credit card.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whatever you choose, read the &lt;a href="https://devhelm.io/blog/synthetic-monitoring-best-practices" rel="noopener noreferrer"&gt;synthetic monitoring best practices&lt;/a&gt; guide before you configure your first check — the difference between a useful setup and a flaky one that trains your team to ignore alerts is in the details, not the vendor.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devhelm.io/blog/checkly-alternative" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>comparisons</category>
      <category>guides</category>
    </item>
    <item>
      <title>Opsgenie Is Shutting Down: What You Need to Know and When to Migrate</title>
      <dc:creator>DevHelm</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:26:27 +0000</pubDate>
      <link>https://dev.to/devhelm/opsgenie-is-shutting-down-what-you-need-to-know-and-when-to-migrate-55il</link>
      <guid>https://dev.to/devhelm/opsgenie-is-shutting-down-what-you-need-to-know-and-when-to-migrate-55il</guid>
      <description>&lt;p&gt;Atlassian has confirmed that Opsgenie will reach end-of-life in April 2027. New signups are already blocked, and existing customers are being guided toward Jira Service Management (JSM) Cloud as the replacement. If your team relies on Opsgenie for on-call scheduling, alert routing, or escalation policies, you have roughly ten months to plan and execute a migration.&lt;/p&gt;

&lt;p&gt;This article covers what's happening, why, and what your options look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;Atlassian announced that Opsgenie — acquired in 2018 as a standalone incident alerting and on-call product — will be fully deprecated. The core capabilities (alert routing, on-call schedules, escalation policies, and incident timelines) have been rebuilt inside &lt;a href="https://www.atlassian.com/software/jira/service-management/features/incident-management" rel="noopener noreferrer"&gt;Jira Service Management Cloud&lt;/a&gt;, and Atlassian no longer sees a reason to maintain a separate product.&lt;/p&gt;

&lt;p&gt;Existing Opsgenie customers received migration notices starting in early 2026. The Opsgenie web console now displays a banner pointing to migration documentation, and the API will continue functioning until the shutdown date — but no new features will ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeline
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1 2026&lt;/td&gt;
&lt;td&gt;New Opsgenie signups blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2 2026&lt;/td&gt;
&lt;td&gt;Migration tooling available in JSM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;April 2027&lt;/td&gt;
&lt;td&gt;Opsgenie reaches end-of-life — APIs, web console, and mobile app stop functioning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Atlassian is providing roughly 12–14 months of overlap between the announcement and the hard cutoff. That sounds generous, but migrations that involve on-call schedules, hundreds of integrations, and team-specific escalation logic rarely go smoothly in a single sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Atlassian is doing this
&lt;/h2&gt;

&lt;p&gt;The strategic logic is straightforward: Atlassian wants one incident management surface, not two. Maintaining a standalone product that overlaps with JSM's built-in incident features creates engineering duplication, confuses the sales motion, and splits the user base.&lt;/p&gt;

&lt;p&gt;JSM Cloud now includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alert routing and deduplication (ported from Opsgenie's engine)&lt;/li&gt;
&lt;li&gt;On-call scheduling with rotation rules&lt;/li&gt;
&lt;li&gt;Escalation policies with multi-channel notification&lt;/li&gt;
&lt;li&gt;Incident timelines and postmortem workflows&lt;/li&gt;
&lt;li&gt;A mobile app for on-call acknowledgment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For teams already paying for JSM, this consolidation removes a separate line item. For teams that used Opsgenie standalone (without Jira), it forces a platform decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes for existing users
&lt;/h2&gt;

&lt;p&gt;If you do nothing, your on-call system stops working in April 2027. Specifically:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrations break.&lt;/strong&gt; Every monitoring tool, CI pipeline, or custom webhook that currently POSTs to Opsgenie's API will need a new destination. If you route alerts from Datadog, Prometheus, Grafana, CloudWatch, or any other source through Opsgenie, those integrations require rewiring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On-call schedules need recreation.&lt;/strong&gt; While Atlassian provides migration tooling, the schedule model in JSM differs from Opsgenie's. Complex rotations with overrides, restrictions, and multi-team handoffs may not map 1:1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile app changes.&lt;/strong&gt; The Opsgenie mobile app will stop receiving alerts after shutdown. JSM uses the Jira Cloud mobile app (or the Jira Ops companion app) for on-call notifications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data export.&lt;/strong&gt; Atlassian provides export tooling for historical alert and incident data. If you need audit trails or postmortem archives, export before the cutoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration options
&lt;/h2&gt;

&lt;p&gt;You have three paths, and the right one depends on your existing Atlassian footprint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: Move to JSM Cloud
&lt;/h3&gt;

&lt;p&gt;This is the path of least resistance if your team already uses Jira for ticketing. The migration tooling handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On-call schedules and rotations&lt;/li&gt;
&lt;li&gt;Escalation policies&lt;/li&gt;
&lt;li&gt;Integration configurations (partial — some require manual reconnection)&lt;/li&gt;
&lt;li&gt;Team structures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Considerations:&lt;/strong&gt; JSM pricing is per-agent, which can be significantly more expensive than Opsgenie's per-user model for large teams. JSM also requires Jira Cloud — if you're on Jira Server or Data Center, this migration includes a platform migration too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: Evaluate alternatives
&lt;/h3&gt;

&lt;p&gt;If you were already unhappy with Opsgenie — pricing, mobile app reliability, or integration depth — this is a natural breakpoint to evaluate the market. The &lt;a href="https://devhelm.io/blog/opsgenie-alternatives" rel="noopener noreferrer"&gt;on-call and alerting alternatives&lt;/a&gt; space has matured since 2018, with several strong options in PagerDuty, Rootly, incident.io, Grafana OnCall, and others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When this makes sense:&lt;/strong&gt; your team doesn't use Jira, you want to avoid JSM's per-agent pricing, or you need capabilities Opsgenie never had (AI-powered triage, native Slack workflows, or deeper runbook automation).&lt;/p&gt;

&lt;h3&gt;
  
  
  Option C: Hybrid migration
&lt;/h3&gt;

&lt;p&gt;Some teams separate concerns: move on-call scheduling to a dedicated tool while keeping Jira for ticket tracking and postmortems. This avoids JSM lock-in for the real-time alerting path while preserving the Jira integration for retrospective workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeline recommendations
&lt;/h3&gt;

&lt;p&gt;Regardless of which path you choose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Now (Q2 2026):&lt;/strong&gt; Audit your current Opsgenie configuration. Document every integration, escalation policy, and schedule. Identify which are actively used vs. legacy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q3 2026:&lt;/strong&gt; Run a proof-of-concept migration in a staging environment. Test alert routing, escalation timing, and mobile notifications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q4 2026:&lt;/strong&gt; Execute production migration with a parallel-run period. Keep Opsgenie active as a fallback while validating the new system handles real incidents correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q1 2027:&lt;/strong&gt; Decommission Opsgenie integrations and export historical data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to evaluate in any migration
&lt;/h2&gt;

&lt;p&gt;Whether you're moving to JSM or a third-party tool, these are the dimensions that matter for &lt;a href="https://devhelm.io/blog/on-call-rotation-best-practices" rel="noopener noreferrer"&gt;on-call rotation setup&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On-call scheduling flexibility.&lt;/strong&gt; Can you model your actual rotation? Multi-team handoffs, timezone-aware shifts, holiday overrides, temporary swaps. Simple round-robin is table stakes — the complexity lives in the exceptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalation policies.&lt;/strong&gt; How many escalation layers? Can you escalate to a different team after N minutes? Do escalations respect business hours vs. 24/7? Can you route by &lt;a href="https://devhelm.io/blog/incident-severity-levels" rel="noopener noreferrer"&gt;incident severity level&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration breadth.&lt;/strong&gt; Count your current Opsgenie integrations. Verify that the destination tool supports each one natively or via webhook. Pay special attention to bidirectional integrations (where the on-call tool writes back to the source).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing model.&lt;/strong&gt; Opsgenie charged per-user with a generous free tier. JSM charges per-agent. PagerDuty charges per-user with add-on costs. Some newer tools offer flat-rate pricing. Model your actual team size and growth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile experience.&lt;/strong&gt; On-call is a mobile-first workflow. Test the actual notification reliability, acknowledgment flow, and override UX on both iOS and Android. Unreliable push notifications during an outage are worse than no tool at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start planning now
&lt;/h2&gt;

&lt;p&gt;Ten months feels like plenty of time until you factor in procurement cycles, security reviews, integration testing, and the reality that on-call migrations can only be validated during actual incidents. Teams that start evaluating in Q2 2026 will have time for a proper parallel-run period. Teams that wait until Q1 2027 will be scrambling.&lt;/p&gt;

&lt;p&gt;Document your current state, pick a direction, and get a proof-of-concept running before summer ends.&lt;/p&gt;




&lt;p&gt;For the monitoring and alerting layer that feeds your on-call tool — multi-region checks, notification policies, and a status page — &lt;a href="https://app.devhelm.io" rel="noopener noreferrer"&gt;start at app.devhelm.io&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devhelm.io/blog/opsgenie-shutdown" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>guides</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Opsgenie Alternatives in 2026: Where to Migrate Before the Shutdown</title>
      <dc:creator>DevHelm</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:25:50 +0000</pubDate>
      <link>https://dev.to/devhelm/opsgenie-alternatives-in-2026-where-to-migrate-before-the-shutdown-2ife</link>
      <guid>https://dev.to/devhelm/opsgenie-alternatives-in-2026-where-to-migrate-before-the-shutdown-2ife</guid>
      <description>&lt;p&gt;Atlassian announced it is sunsetting Opsgenie as a standalone product, folding its on-call features into Jira Service Management. If your team runs on Opsgenie today, you have a deadline — and a decision.&lt;/p&gt;

&lt;p&gt;The forced migration is an opportunity to re-evaluate. Opsgenie was a solid mid-market on-call tool, but the market has changed since most teams adopted it. Newer entrants like incident.io and Rootly have reimagined how on-call connects to incident response. Grafana OnCall made open-source on-call viable. PagerDuty matured its platform further. And Atlassian's own JSM absorbed Opsgenie's features into a broader ITSM surface.&lt;/p&gt;

&lt;p&gt;This guide compares six alternatives on the dimensions that actually matter for an &lt;a href="https://devhelm.io/blog/on-call-rotation-best-practices" rel="noopener noreferrer"&gt;on-call rotation&lt;/a&gt;: scheduling flexibility, escalation depth, integration breadth, pricing model, mobile experience, and how well each tool connects to the rest of your incident workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look for in an Opsgenie replacement
&lt;/h2&gt;

&lt;p&gt;Before comparing tools, clarify what your team actually needs. Opsgenie covered a broad surface — on-call scheduling, escalation policies, alert routing, and integrations with monitoring tools. Not every replacement covers all of it equally well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On-call scheduling.&lt;/strong&gt; The basics are table stakes: rotations, overrides, time-of-day restrictions. The differentiators are schedule previews, gap detection, and how painful it is to set up a follow-the-sun rotation across three time zones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalation policies.&lt;/strong&gt; How many levels can you define? Can you branch based on alert severity or service? Can you escalate to a Slack channel instead of a person? Some tools treat escalation as a linear chain; others allow conditional routing trees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration count and quality.&lt;/strong&gt; Opsgenie had 200+ integrations. If your monitoring stack sends alerts through a specific integration, verify the replacement supports it natively — not just via generic webhook. Native integrations carry metadata (severity, service, deduplication keys) that generic webhooks lose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing model.&lt;/strong&gt; The industry split is per-seat vs. usage-based. Per-seat is predictable but punishes large on-call rosters where most people are only paged occasionally. Usage-based (per-incident or per-notification) is cheaper for large teams but can spike during outages — exactly when you need the tool most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alert routing intelligence.&lt;/strong&gt; Can the tool suppress duplicates? Correlate related alerts? Auto-resolve when the source clears? Route based on alert content, not just the integration it arrived on? Opsgenie's alert policies were underrated — make sure your replacement covers the same ground.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile app quality.&lt;/strong&gt; On-call is a mobile-first job. The acknowledge-and-escalate flow needs to work reliably on a lock screen notification at 3 AM. Test the mobile app before committing.&lt;/p&gt;

&lt;h2&gt;
  
  
  PagerDuty
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.pagerduty.com" rel="noopener noreferrer"&gt;PagerDuty&lt;/a&gt; is the incumbent and the most mature platform in the category. It has been doing on-call since 2009, and the depth shows — 700+ integrations, multi-level escalation with conditional branching, event intelligence (ML-based alert grouping and suppression), and a mobile app that has had fifteen years of iteration.&lt;/p&gt;

&lt;p&gt;The escalation engine is the deepest in this list. You can define policies that route by urgency, time of day, and service. The event orchestration layer lets you transform, suppress, or re-route alerts before they page anyone. If you have complex routing needs — "page the database team for Postgres alerts, but only if severity is critical and it's outside business hours" — PagerDuty can express that without custom code.&lt;/p&gt;

&lt;p&gt;The downside is cost. Plans start at $21/user/month for the base tier and reach $49/user/month for the full platform (AIOps, analytics, status pages). For a 20-person on-call roster, that is $5,000–12,000/year. The platform also carries the weight of its age — the UI has layers of legacy concepts, and configuring event orchestration requires working through a learning curve that newer tools avoid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; mid-to-large engineering orgs with complex routing needs, deep integration requirements, and budget for the premium tier. Overkill for a 5-person startup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grafana OnCall
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://grafana.com/products/oncall/" rel="noopener noreferrer"&gt;Grafana OnCall&lt;/a&gt; is the open-source option. You can self-host it (Apache 2.0 license) or use the managed version on Grafana Cloud, which includes a free tier for up to 100 users.&lt;/p&gt;

&lt;p&gt;The primary value proposition is native integration with the Grafana ecosystem. If you already use Grafana for dashboards and Grafana Alerting for your alert rules, OnCall plugs in without a new vendor relationship. Alerts flow directly from Grafana Alerting into on-call schedules and escalation chains — no webhook glue required.&lt;/p&gt;

&lt;p&gt;The scheduling and escalation features cover the essentials: rotations, overrides, multi-step escalation, and notification through Slack, Telegram, phone, and SMS. The web UI is clean and functional. Where it lags behind PagerDuty is in the edges — the mobile app is newer and less polished, the alert routing logic is simpler (no ML-based grouping), and the integration catalog outside the Grafana ecosystem is smaller.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams already invested in Grafana that want on-call without adding another vendor or another bill. Also strong for teams that value open source and want the option to self-host.&lt;/p&gt;

&lt;h2&gt;
  
  
  incident.io
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://incident.io" rel="noopener noreferrer"&gt;incident.io&lt;/a&gt; started as an incident management tool and expanded into on-call. The result is a product where on-call and incident response are tightly coupled — when a page fires, the same tool handles the response, the communication, and the retrospective.&lt;/p&gt;

&lt;p&gt;The differentiator is Slack-native workflows. Declaring an incident creates a dedicated channel, assigns roles, posts status updates, and tracks action items — all without leaving Slack. The on-call layer feeds directly into this: when an alert fires and nobody acknowledges, escalation can auto-declare an incident with the full response machinery attached.&lt;/p&gt;

&lt;p&gt;On-call features include rotations, escalation policies, and a catalog-driven routing model where you define services and link them to teams. Pricing starts at $20/user/month for the on-call product, with incident management as a separate (or bundled) line item.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that run their incident response in Slack and want on-call tightly integrated with the declare-respond-retrospect lifecycle. Less compelling if your team uses Microsoft Teams or prefers a standalone on-call tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rootly
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://rootly.com" rel="noopener noreferrer"&gt;Rootly&lt;/a&gt; occupies similar territory to incident.io — incident management with on-call — but differentiates on AI-powered retrospectives and broader chat platform support (both Slack and Microsoft Teams).&lt;/p&gt;

&lt;p&gt;The incident timeline is the standout feature. Rootly automatically constructs a chronological record of actions taken during an incident — who was paged, what was acknowledged, which runbooks were triggered, what messages were posted. The retrospective template then pulls from this timeline, reducing the manual work of writing a postmortem.&lt;/p&gt;

&lt;p&gt;On-call scheduling and escalation are solid but not as deep as PagerDuty's. The integration catalog is growing but smaller than the incumbents. Pricing is competitive with incident.io.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that value automated retrospectives and want incident management + on-call in one tool. Particularly relevant if your organization uses Microsoft Teams, where incident.io's Slack-native approach is a non-starter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Better Stack
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://betterstack.com" rel="noopener noreferrer"&gt;Better Stack&lt;/a&gt; takes a bundled approach: uptime monitoring, on-call, incident management, and status pages in one product. If you want to consolidate vendors — replace Opsgenie and your uptime monitoring tool at the same time — Better Stack is the most integrated option.&lt;/p&gt;

&lt;p&gt;The on-call features are competent: rotations, escalation policies, multi-channel notifications (phone, SMS, Slack, Teams, email). The scheduling UI is straightforward. What makes it interesting for Opsgenie refugees is the monitoring layer underneath — you get HTTP, keyword, and heartbeat checks that feed directly into on-call without configuring a separate integration.&lt;/p&gt;

&lt;p&gt;The trade-off is depth. Better Stack's on-call is solid for straightforward routing (alert fires, page the on-call engineer, escalate if unacknowledged), but it lacks the conditional routing and event orchestration that PagerDuty offers. For teams with simple on-call needs and a desire to reduce vendor count, that trade-off is acceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; small-to-mid teams that want monitoring + on-call + status pages in one subscription. Not for teams with complex multi-service routing needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jira Service Management (JSM)
&lt;/h2&gt;

&lt;p&gt;JSM is Atlassian's own migration path. The on-call features in JSM are, in large part, Opsgenie's features rebuilt into the JSM platform. If you are already paying for JSM Cloud, you get on-call included at no additional cost on Premium and Enterprise plans.&lt;/p&gt;

&lt;p&gt;The integration with Jira is the obvious advantage. Alerts can create Jira issues. Incidents link to change requests. The service catalog connects to your CMDB. If your organization's workflow revolves around Jira, the operational data staying in the same platform has genuine value.&lt;/p&gt;

&lt;p&gt;The downsides: JSM is an ITSM tool first, and on-call is one feature among many. The configuration surface is large, the UI carries Jira's complexity, and the mobile experience for on-call is embedded within the broader JSM app rather than being a focused paging tool. For more on the &lt;a href="https://devhelm.io/blog/opsgenie-shutdown" rel="noopener noreferrer"&gt;Opsgenie shutdown&lt;/a&gt; timeline and migration planning, see our detailed breakdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; organizations already on Atlassian Cloud (Jira, Confluence, JSM) that want the simplest migration path and value tight Jira integration over a standalone on-call UX.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration decision framework
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;PagerDuty&lt;/th&gt;
&lt;th&gt;Grafana OnCall&lt;/th&gt;
&lt;th&gt;incident.io&lt;/th&gt;
&lt;th&gt;Rootly&lt;/th&gt;
&lt;th&gt;Better Stack&lt;/th&gt;
&lt;th&gt;JSM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starting price&lt;/td&gt;
&lt;td&gt;$21/user/mo&lt;/td&gt;
&lt;td&gt;Free (Cloud)&lt;/td&gt;
&lt;td&gt;$20/user/mo&lt;/td&gt;
&lt;td&gt;Custom&lt;/td&gt;
&lt;td&gt;$29/mo (team)&lt;/td&gt;
&lt;td&gt;Included w/ JSM Premium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integrations&lt;/td&gt;
&lt;td&gt;700+&lt;/td&gt;
&lt;td&gt;50+ (Grafana-native)&lt;/td&gt;
&lt;td&gt;100+&lt;/td&gt;
&lt;td&gt;80+&lt;/td&gt;
&lt;td&gt;100+&lt;/td&gt;
&lt;td&gt;200+ (Jira ecosystem)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack-native&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teams support&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (Apache 2.0)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundled monitoring&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (Grafana Cloud)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile app maturity&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event orchestration&lt;/td&gt;
&lt;td&gt;Deep&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Catalog-based&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is no single best tool here. The decision depends on your existing stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Already on Grafana?&lt;/strong&gt; Grafana OnCall is the lowest-friction path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run incidents in Slack?&lt;/strong&gt; incident.io or Rootly, depending on whether you need Teams support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want the deepest routing engine?&lt;/strong&gt; PagerDuty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want to consolidate monitoring + on-call?&lt;/strong&gt; Better Stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Already paying for JSM Cloud Premium?&lt;/strong&gt; JSM is free and familiar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget-constrained?&lt;/strong&gt; Grafana OnCall (free) or JSM (included).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whichever tool you choose, the on-call layer is only as good as the alerts feeding it. Noisy, low-context alerts create &lt;a href="https://devhelm.io/blog/monitoring-alerts" rel="noopener noreferrer"&gt;alert fatigue&lt;/a&gt; regardless of how well the escalation policy is configured. The monitoring system upstream — what generates the alerts, how it classifies severity, and how quickly it detects problems — determines whether your on-call engineers get paged for real incidents or wake up for false positives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The monitoring layer underneath
&lt;/h2&gt;

&lt;p&gt;Your on-call tool routes alerts. Something else has to generate them.&lt;/p&gt;

&lt;p&gt;For the multi-region monitoring and alerting layer that feeds your on-call tool — HTTP, DNS, TCP, and heartbeat checks with configurable notification policies and a public status page — take a look at &lt;a href="https://app.devhelm.io" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt;. Your first 50 monitors are free, with checks running from multiple regions and alerts routed to whichever on-call platform you picked from this list.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devhelm.io/blog/opsgenie-alternatives" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>comparisons</category>
      <category>guides</category>
    </item>
    <item>
      <title>On-Call Rotation Best Practices for Engineering Teams</title>
      <dc:creator>DevHelm</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:25:14 +0000</pubDate>
      <link>https://dev.to/devhelm/on-call-rotation-best-practices-for-engineering-teams-clo</link>
      <guid>https://dev.to/devhelm/on-call-rotation-best-practices-for-engineering-teams-clo</guid>
      <description>&lt;p&gt;On-call is the tax engineering teams pay for running production systems. Every organization that ships software to users eventually reaches the point where someone needs to be reachable when things break at 2 AM. Done well, on-call is sustainable — a shared responsibility that distributes evenly across the team and improves with every incident. Done poorly, it burns people out, concentrates knowledge in a few overloaded individuals, and quietly degrades the product as exhausted engineers stop investing in reliability.&lt;/p&gt;

&lt;p&gt;The difference between those outcomes is almost never the tooling. It is the structure: how rotations are designed, how escalations work, how burden is distributed, and how the team treats on-call as a first-class engineering practice rather than an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotation structures
&lt;/h2&gt;

&lt;p&gt;There is no single correct rotation model. The right choice depends on team size, geographic distribution, and the criticality of the services being covered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weekly rotation&lt;/strong&gt; is the most common default. One engineer carries the pager for a full week, then passes it to the next person. Simple to understand, simple to schedule. The downside is that a bad week — multiple incidents, high alert volume — falls entirely on one person with no relief until the handoff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Biweekly rotation&lt;/strong&gt; extends the shift to reduce the overhead of context-switching between on-call and non-on-call weeks. It works when alert volume is low (fewer than five pages per week on average). Beyond that threshold, two weeks starts to feel punishing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow-the-sun&lt;/strong&gt; distributes coverage across time zones so no one takes overnight pages. A team with engineers in US Pacific, European, and Asian time zones can cover 24 hours without anyone waking up at 3 AM. The tradeoff is coordination cost: handoff quality becomes critical, and you need at least three engineers per timezone to avoid single points of failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Primary/secondary split&lt;/strong&gt; assigns two engineers per shift. The primary takes the initial page; if they don't acknowledge within the escalation window (typically 5–10 minutes), the secondary gets paged. This provides redundancy without doubling the roster size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service-ownership rotation&lt;/strong&gt; maps on-call to specific services rather than a team-wide roster. The payments team covers payments; the platform team covers infrastructure. This works at scale (50+ engineers) where generalist on-call produces too much context-switching, but requires well-defined service boundaries and ownership clarity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schedule design
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Shift length matters.&lt;/strong&gt; Seven-day shifts are standard, but consecutive days beyond seven correlate strongly with burnout and error rates. If your rotation runs longer than a week, build in explicit rest days or shorten the active hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overlap periods&lt;/strong&gt; between shifts prevent the "not my problem anymore" gap. A 30-minute handoff window — where both outgoing and incoming engineer are reachable — catches incidents that fire right at the boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handoff rituals&lt;/strong&gt; are the difference between "good luck" and useful context transfer. At minimum, the outgoing engineer should communicate: open incidents, anything flapping or degraded, recent deploys that haven't fully baked, and upcoming maintenance windows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weekend coverage&lt;/strong&gt; deserves explicit design. Some teams split weekends separately from weekdays; others absorb them into the weekly rotation. The key decision is whether weekend pages carry additional compensation or comp time. Leaving this ambiguous creates resentment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time-zone-aware scheduling&lt;/strong&gt; goes beyond follow-the-sun. Even within a single-timezone team, shift start times matter. Starting on Monday morning rather than Sunday night means the outgoing engineer doesn't stay up late on their last day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Escalation policies
&lt;/h2&gt;

&lt;p&gt;A page that goes unacknowledged is worse than no page at all — it means the system thinks someone is handling the incident while no one is. Escalation policies exist to guarantee every alert eventually reaches a human who acts on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic chain:&lt;/strong&gt; Primary (acknowledge within 5 min) → Secondary (acknowledge within 5 min) → Engineering Manager → VP Engineering. Each tier adds urgency without skipping the people closest to the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time-based escalation&lt;/strong&gt; increases the responder pool as time passes without acknowledgment. This is the minimum viable escalation policy. If you have nothing else, implement this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Severity-based routing&lt;/strong&gt; sends different &lt;a href="https://devhelm.io/blog/incident-severity-levels" rel="noopener noreferrer"&gt;severity levels&lt;/a&gt; to different responders. A P3 informational alert goes to Slack only. A P2 goes to the primary on-call. A P1 pages both primary and secondary immediately. A P0 pages the entire escalation chain simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;De-escalation matters too.&lt;/strong&gt; When an incident resolves without needing backup, the secondary should be notified (not paged) so they know the situation is handled. Over-escalation erodes trust in the system and trains people to ignore pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing on-call burden
&lt;/h2&gt;

&lt;p&gt;The single highest-impact action for improving on-call is reducing alert noise. Teams that page 20+ times per week cannot retain engineers on the rotation. The target is fewer than five pages per on-call shift — ideally fewer than two that require actual intervention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alert quality over quantity.&lt;/strong&gt; Every alert should be actionable. If the on-call engineer looks at a page and routinely says "I can ignore this," that alert should be tuned, suppressed, or deleted. Document what the responder should actually do in structured &lt;a href="https://devhelm.io/blog/runbooks" rel="noopener noreferrer"&gt;runbooks&lt;/a&gt; — not just what triggered the alert.&lt;/p&gt;

&lt;p&gt;For the full policy model behind that cleanup, see &lt;a href="https://devhelm.io/blog/monitoring-alerts" rel="noopener noreferrer"&gt;Monitoring Alerts: The Definitive Guide to Alerting Without Alert Fatigue&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-remediation for known responses.&lt;/strong&gt; If the runbook for an alert is "restart the pod" or "clear the queue," that's a candidate for automation. Every automated response removes a page from the rotation permanently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blameless postmortems&lt;/strong&gt; feed back into the system. When an incident occurs, the postmortem should ask: could this alert have been prevented? Could the responder have been given better information? Could the resolution have been automated? Each answer improves future on-call shifts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consolidation windows&lt;/strong&gt; batch non-urgent alerts rather than paging for each one individually. A monitoring system that fires three separate alerts for related symptoms — high latency, increased errors, connection pool exhaustion — should consolidate into a single incident rather than paging three times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compensation and fairness
&lt;/h2&gt;

&lt;p&gt;On-call carries real cost to the engineer: interrupted sleep, restricted evenings, constrained weekend plans. Teams that treat this as "just part of the job" with no explicit compensation build resentment and face retention problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paid on-call&lt;/strong&gt; is the clearest model. Common structures include a flat per-shift stipend ($200–500/week in US markets), a per-page bonus, or a combination. Some organizations pay a higher rate for pages outside business hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comp time&lt;/strong&gt; offers time off in exchange for on-call shifts — typically 0.5–1 day per week of on-call. This works well when the team values time flexibility more than additional pay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotation equity tracking&lt;/strong&gt; ensures burden distributes fairly over time. Track pages-per-person, incidents-per-person, and weekend-shifts-per-person quarterly. If one engineer consistently gets paged more due to scheduling luck, adjust the rotation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opt-out for life events.&lt;/strong&gt; Moving, new baby, medical issues, family emergencies — there should be an explicit, no-questions-asked path to temporarily exit the rotation. The team absorbs the extra coverage. Building this in structurally prevents people from burning through goodwill to get relief.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common anti-patterns
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The hero.&lt;/strong&gt; One engineer who "doesn't mind" being on-call all the time. They accumulate all the context, make the rotation smaller for everyone else, and become an invisible single point of failure. When they leave — and they eventually leave — the team discovers how much tribal knowledge walked out the door.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alerts that cry wolf.&lt;/strong&gt; Pages that fire but require no action train the on-call engineer to ignore alerts. This is the most dangerous anti-pattern: it directly causes real incidents to get slow responses because the engineer assumes "probably nothing again." See &lt;a href="https://devhelm.io/blog/opsgenie-alternatives" rel="noopener noreferrer"&gt;Opsgenie alternatives&lt;/a&gt; for tools that help with noise reduction at the routing layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No handoff notes.&lt;/strong&gt; The incoming engineer starts their shift blind, with no context about what has been flapping, what was recently deployed, or what the previous shift was watching. Every shift start should include at minimum a 5-minute written or async handoff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope creep punishment.&lt;/strong&gt; "You're on-call anyway, so can you also handle this customer request / deploy this hotfix / review this PR?" On-call is for incident response. Loading unrelated work onto on-call engineers makes the rotation feel punitive and discourages participation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No feedback loop.&lt;/strong&gt; Pages fire, engineers respond, nothing changes. Without postmortems that feed back into alert tuning, runbook updates, and automation investments, on-call burden stays static or grows. The rotation should get measurably better each quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the signals
&lt;/h2&gt;

&lt;p&gt;Rotation design determines who gets woken up and how quickly they respond. But the quality of on-call depends entirely on the quality of the alerts feeding it. A rotation staffed by great engineers still fails if the monitoring underneath generates noise instead of signal.&lt;/p&gt;

&lt;p&gt;On-call works when the alerts feeding it are precise. Multi-region monitoring with configurable severity and notification policies reduces noise before it reaches your rotation — start at &lt;a href="https://app.devhelm.io" rel="noopener noreferrer"&gt;app.devhelm.io&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devhelm.io/blog/on-call-rotation-best-practices" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>guides</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>SSL Certificate Monitoring: Prevent Outages Before Your Certs Expire</title>
      <dc:creator>DevHelm</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:24:37 +0000</pubDate>
      <link>https://dev.to/devhelm/ssl-certificate-monitoring-prevent-outages-before-your-certs-expire-1odc</link>
      <guid>https://dev.to/devhelm/ssl-certificate-monitoring-prevent-outages-before-your-certs-expire-1odc</guid>
      <description>&lt;p&gt;A Let's Encrypt certificate renews every 90 days. When auto-renewal fails silently — a DNS record changed, an ACME challenge path broke during a migration, a permissions change on the webroot — your users see a browser security warning and your site is effectively down. Browsers refuse to load the page, API clients reject the connection, and mobile apps show a blank error screen.&lt;/p&gt;

&lt;p&gt;The frustrating part: every one of these outages is preventable. Certificate expiry is not unpredictable. The expiration date is baked into the certificate itself, readable by any TLS client. The only reason teams get bitten is that nobody was checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SSL certificates expire (and why auto-renewal fails)
&lt;/h2&gt;

&lt;p&gt;TLS certificates have a maximum validity period — 90 days for Let's Encrypt, up to 398 days for commercial CAs. This is by design: shorter lifetimes reduce the window during which a compromised private key can be exploited.&lt;/p&gt;

&lt;p&gt;Most teams rely on automated renewal through ACME clients like certbot, acme.sh, or cloud-native solutions (AWS ACM, Cloudflare Origin CA). When these work, you never think about certificate expiry. When they fail, the failure is silent.&lt;/p&gt;

&lt;p&gt;Common failure modes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS changes that break ACME challenges.&lt;/strong&gt; You migrate DNS from one provider to another. The ACME DNS-01 challenge was configured for the old provider's API. Certbot tries to renew, cannot create the required TXT record, and silently gives up. The certificate continues serving for 60 more days until it expires.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure migrations that break file paths.&lt;/strong&gt; You move from nginx to a reverse proxy. The &lt;code&gt;/.well-known/acme-challenge/&lt;/code&gt; path no longer routes to the certbot webroot. HTTP-01 challenges fail. The cert renewal log shows errors, but nobody is watching that log.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permissions and credential rotation.&lt;/strong&gt; Cloud-provider IAM credentials used by the ACME client get rotated. The renewal script runs as a different service account than when it was first configured. It fails with a 403, and the retry logic gives up after three attempts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vendor certificates you do not control.&lt;/strong&gt; A SaaS dependency serves its API behind a certificate you cannot renew. A CDN edge node has its own certificate lifecycle. A legacy internal service uses a certificate issued by an internal CA with its own renewal cadence. None of these are in your automation — they expire on their own schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SSL certificate monitoring actually checks
&lt;/h2&gt;

&lt;p&gt;A useful SSL monitoring system goes beyond "is the certificate expired right now." By the time the answer is "yes," the outage is already happening. Meaningful checks include:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Days until expiry.&lt;/strong&gt; The primary signal. Alerting at 30 days gives your team time to investigate a broken renewal pipeline before it becomes an outage. Alerting at 14 days is the escalation threshold — something is wrong, and it needs attention today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Certificate chain validity.&lt;/strong&gt; An expired intermediate certificate breaks the chain even when the leaf certificate is current. Older clients (Android &amp;lt; 7.1, some embedded devices) that do not perform AIA fetching will reject a connection with a missing intermediate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hostname matching.&lt;/strong&gt; A certificate issued for &lt;code&gt;*.example.com&lt;/code&gt; does not cover &lt;code&gt;example.com&lt;/code&gt; (the bare apex) unless explicitly included as a SAN. After a CDN migration or load balancer swap, the served certificate may not match the requested hostname.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issuer changes.&lt;/strong&gt; If the certificate issuer changes unexpectedly — say, your Let's Encrypt cert is suddenly signed by an unknown CA — that is a signal worth investigating. It may indicate a CDN misconfiguration, a MITM proxy in the path, or a compromised renewal pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you need dedicated SSL monitoring
&lt;/h2&gt;

&lt;p&gt;If you have a single domain with one certificate behind a managed provider (ACM, Cloudflare), and your infrastructure never changes, you might survive without monitoring. Everyone else needs it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple domains and subdomains.&lt;/strong&gt; Each certificate has its own renewal lifecycle. Ten domains means ten independent renewal processes, each with its own failure modes. One forgotten subdomain is all it takes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wildcard certificates.&lt;/strong&gt; Wildcards cover &lt;code&gt;*.example.com&lt;/code&gt; but not nested subdomains (&lt;code&gt;api.staging.example.com&lt;/code&gt;). Teams assume the wildcard covers everything, discover the gap at 2 AM when the staging API breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Internal services with self-signed certificates.&lt;/strong&gt; Internal CA certificates have their own expiry. The internal PKI that issues them may not have automated renewal. The operations team that set it up two years ago may no longer be on the team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vendor certificates you depend on.&lt;/strong&gt; Your payment gateway, your authentication provider, your CDN — all serve certificates you cannot renew. If their certificate expires or their chain breaks, your integration fails. Monitoring their certificate from outside tells you about the problem before your on-call pager does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approaches to SSL monitoring
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Manual calendar reminders.&lt;/strong&gt; You look up the expiration date, set a calendar event 30 days before, and hope whoever gets the reminder knows what to do. This breaks the moment someone changes teams, the domain list grows, or the certificate gets replaced early (resetting the expiry date without updating the calendar).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cron scripts with openssl.&lt;/strong&gt; A bash script runs &lt;code&gt;openssl s_client -connect example.com:443&lt;/code&gt; and parses the &lt;code&gt;notAfter&lt;/code&gt; date. This works until the script's host is down, the output format changes between OpenSSL versions, or the alert channel it writes to gets archived. It also only checks from one location — useless for catching regional CDN cert issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assertion-based monitoring.&lt;/strong&gt; An HTTP monitor runs from multiple regions on a fixed schedule and includes an SSL expiry assertion alongside its other checks — status code, response time, body content. The SSL check is part of the monitor, not a separate system. When the certificate drops below the threshold, it triggers the same alert pipeline as a 500 error or a timeout. This is the approach that scales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up SSL monitoring with assertions
&lt;/h2&gt;

&lt;p&gt;DevHelm's HTTP monitors support an &lt;code&gt;ssl_expiry&lt;/code&gt; assertion that checks the certificate's remaining validity on every request. You configure the minimum days remaining, and the assertion fails when the certificate crosses that threshold.&lt;/p&gt;

&lt;p&gt;A YAML configuration that monitors a production API with both a warning and a failure threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Production API SSL&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://api.example.com/health&lt;/span&gt;
&lt;span class="na"&gt;frequency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;300s&lt;/span&gt;
&lt;span class="na"&gt;regions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;us-east&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;eu-west&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ap-southeast&lt;/span&gt;
&lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status_code&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssl_expiry&lt;/span&gt;
    &lt;span class="na"&gt;minDaysRemaining&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warn&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssl_expiry&lt;/span&gt;
    &lt;span class="na"&gt;minDaysRemaining&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same monitor via CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;devhelm monitor create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; http &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; https://api.example.com/health &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--frequency&lt;/span&gt; 300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--regions&lt;/span&gt; us-east,eu-west,ap-southeast &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assertion&lt;/span&gt; &lt;span class="s2"&gt;"status_code=200"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assertion&lt;/span&gt; &lt;span class="s2"&gt;"ssl_expiry&amp;gt;=30"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;minDaysRemaining: 30&lt;/code&gt; threshold fires a warning when the certificate has 30 days left — enough time to investigate why auto-renewal is failing, fix the issue, and verify the fix before the certificate actually expires. The &lt;code&gt;minDaysRemaining: 14&lt;/code&gt; threshold fires a failure alert — this is the escalation point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-region coverage matters here.&lt;/strong&gt; A CDN edge node may have an expired or misconfigured certificate in one region while other regions serve a valid cert. If you only monitor from one location, you will not see the problem until users in that region report it. Running the assertion from multiple regions catches the discrepancy — the same monitor passes from &lt;code&gt;us-east&lt;/code&gt; and fails from &lt;code&gt;eu-west&lt;/code&gt;, telling you exactly where the problem is.&lt;/p&gt;

&lt;p&gt;For teams managing their monitors as code, the ssl_expiry assertion fits into the same version-controlled config as your other monitoring definitions — reviewed in PRs, deployed through CI, reproducible across environments. See &lt;a href="https://devhelm.io/blog/monitoring-as-code" rel="noopener noreferrer"&gt;monitoring as code&lt;/a&gt; for the full workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond expiry: what else to monitor
&lt;/h2&gt;

&lt;p&gt;Certificate expiry is the most common failure, but it is not the only one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Certificate chain changes.&lt;/strong&gt; If the chain your server presents changes — different intermediate, different root, different leaf issuer — that is worth alerting on. It can indicate a CDN misconfiguration (wrong origin pull), a man-in-the-middle proxy that was not there yesterday, or an unintended renewal that picked up a different issuer. For more on &lt;a href="https://devhelm.io/blog/what-ssl-error-means-and-how-to-fix-it" rel="noopener noreferrer"&gt;what SSL errors mean and how to diagnose them&lt;/a&gt;, the chain is usually where it starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protocol downgrades.&lt;/strong&gt; A server that suddenly negotiates TLS 1.0 instead of TLS 1.3 may have a misconfigured load balancer or a fallback rule that should not be active. Compliance frameworks (PCI DSS 4.0) require TLS 1.2 minimum — a protocol downgrade is a compliance violation before it is a security one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mixed content and HSTS gaps.&lt;/strong&gt; A site that serves HTTPS but loads resources over HTTP gets degraded in browsers. If your monitoring confirms the TLS connection is valid but users still see warnings, the problem may be mixed content rather than the certificate itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building SSL monitoring into your stack
&lt;/h2&gt;

&lt;p&gt;SSL certificate monitoring is not a separate discipline. It is one assertion on an HTTP monitor you are probably already running. The same monitor that checks your API returns a 200 in under 500ms can also check that the certificate will not expire in the next 30 days, using the same alert pipeline and the same on-call routing.&lt;/p&gt;

&lt;p&gt;For a broader view of how SSL monitoring fits alongside uptime, latency, and content checks, see the &lt;a href="https://devhelm.io/blog/best-website-monitoring-tools" rel="noopener noreferrer"&gt;best website monitoring tools&lt;/a&gt; comparison.&lt;/p&gt;

&lt;p&gt;The practical setup for most teams: add an &lt;code&gt;ssl_expiry&lt;/code&gt; assertion with &lt;code&gt;minDaysRemaining: 30&lt;/code&gt; to every external-facing HTTP monitor. For critical services — payment endpoints, authentication providers, API gateways — add a second assertion at 14 days with failure severity. For vendor dependencies you cannot renew yourself, the monitoring is your only early-warning system.&lt;/p&gt;

&lt;p&gt;Add an ssl_expiry assertion to any HTTP monitor in 60 seconds — from the dashboard, the CLI, Terraform, or YAML config. Start at &lt;a href="https://app.devhelm.io" rel="noopener noreferrer"&gt;app.devhelm.io&lt;/a&gt;, free for your first 50 monitors.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devhelm.io/blog/ssl-certificate-monitoring" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>guides</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>DNS Monitoring: What to Track, Why It Breaks, and How to Set It Up</title>
      <dc:creator>DevHelm</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:24:01 +0000</pubDate>
      <link>https://dev.to/devhelm/dns-monitoring-what-to-track-why-it-breaks-and-how-to-set-it-up-2kj</link>
      <guid>https://dev.to/devhelm/dns-monitoring-what-to-track-why-it-breaks-and-how-to-set-it-up-2kj</guid>
      <description>&lt;p&gt;Every request your application serves starts with a DNS lookup. If that lookup fails — or returns the wrong IP — your perfectly healthy server is unreachable. The database is fine. The load balancer is fine. But users see a blank page because a record expired, a zone file has a typo, or a resolver is returning stale data.&lt;/p&gt;

&lt;p&gt;Most outages traced to "DNS issues" were detectable hours before users noticed. The record was already wrong. Nobody was checking.&lt;/p&gt;

&lt;p&gt;This guide covers what DNS monitoring tracks, why DNS breaks in production, the types of checks that catch real problems, and how to set it up with automated assertions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DNS monitoring tracks
&lt;/h2&gt;

&lt;p&gt;DNS monitoring verifies that your domain's DNS infrastructure is healthy across five dimensions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resolution success.&lt;/strong&gt; Can the domain be resolved at all? An NXDOMAIN response or a SERVFAIL means the domain is effectively offline for anyone whose cache has expired. This is the baseline — if resolution fails, nothing else matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Response time.&lt;/strong&gt; A healthy DNS lookup completes in under 50 ms. When resolution crosses 200 ms, it adds perceptible latency to every new connection. Slow DNS is slow everything: every HTTPS handshake, every API call, every page load starts with a lookup. For a deeper dive into diagnosing resolution latency, see &lt;a href="https://devhelm.io/blog/how-to-fix-slow-dns-lookup" rel="noopener noreferrer"&gt;How to Fix Slow DNS Lookup&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Record accuracy.&lt;/strong&gt; The resolved values need to be correct, not just present. An A record pointing to a decommissioned IP, a CNAME targeting a deleted CDN distribution, or a missing MX record silently breaks traffic routing, email delivery, or TLS certificate validation. Accuracy checks verify that A/AAAA records match expected IPs, CNAME records point to the right targets, and TXT records contain the correct SPF, DKIM, and domain verification strings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TTL health.&lt;/strong&gt; Time-to-live values control how long resolvers cache a record. A TTL that's too low (under 60 seconds) forces constant re-resolution, creating unnecessary load on authoritative servers and adding latency. A TTL that's too high (over 86,400 seconds) means changes take a day or more to propagate — dangerous during a migration or an incident. Monitoring TTL drift catches both extremes before they cause problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Propagation consistency.&lt;/strong&gt; A record change that's visible in us-east might still be stale in eu-west for hours, depending on TTL and resolver cache behavior. Multi-region DNS checks detect propagation failures that single-location monitoring misses entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DNS breaks
&lt;/h2&gt;

&lt;p&gt;DNS failures rarely look like DNS failures. They look like "the site is down" or "email stopped arriving" or "the CDN is serving the old version." Here are the actual root causes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Registrar expiry.&lt;/strong&gt; The domain registration lapses because the renewal credit card expired. The registrar points the nameservers to a parking page. Every record disappears. This happens to large companies more often than anyone admits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zone file typos.&lt;/strong&gt; A missing trailing dot on a CNAME target, a transposed octet in an A record, or a malformed SPF string in a TXT record. The change passes the registrar's syntax check but breaks resolution for specific record types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TTL misconfiguration.&lt;/strong&gt; Setting TTL to 60 seconds before a migration (to speed propagation) and forgetting to raise it afterward creates a thundering herd — every resolver re-queries your authoritative server every minute instead of every hour. Conversely, a 24-hour TTL on a record you're about to change means the old value persists in caches long after you've updated it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS hijacking.&lt;/strong&gt; An attacker modifies DNS responses — through cache poisoning, BGP hijacking, or compromised registrar credentials — to redirect traffic to a server they control. Without record-value assertions, you won't know until users report seeing a different site. &lt;a href="https://developers.cloudflare.com/dns/dnssec/" rel="noopener noreferrer"&gt;Cloudflare's DNSSEC documentation&lt;/a&gt; covers how DNSSEC validation protects against some of these vectors, but DNSSEC misconfiguration is itself a common source of outages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provider outages.&lt;/strong&gt; Your authoritative DNS provider has an incident. If you run a single-provider setup with no secondary, resolution fails for every domain hosted there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of DNS checks
&lt;/h2&gt;

&lt;p&gt;DNS monitoring maps to four categories, each catching a different failure class.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resolution checks
&lt;/h3&gt;

&lt;p&gt;The most basic assertion: does the domain resolve? A &lt;code&gt;dns_resolves&lt;/code&gt; check queries the domain and passes if it gets a valid answer — no NXDOMAIN, no TIMEOUT, no SERVFAIL. This catches expired domains, deleted zones, and authoritative server outages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance checks
&lt;/h3&gt;

&lt;p&gt;DNS response time directly affects connection setup latency. A &lt;code&gt;dns_response_time&lt;/code&gt; assertion fails when resolution exceeds a threshold (e.g., 500 ms), catching overloaded resolvers, network path degradation, or authoritative server issues before they compound into visible user-facing slowness. A &lt;code&gt;dns_response_time_warn&lt;/code&gt; variant produces a warning instead of a failure for softer thresholds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accuracy checks
&lt;/h3&gt;

&lt;p&gt;Record-value assertions verify that DNS returns what you expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IP matching&lt;/strong&gt; (&lt;code&gt;dns_expected_ips&lt;/code&gt;) — A/AAAA records resolve to addresses in your configured allow-list. Catches migrations where old IPs linger and hijacking where IPs change without authorization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CNAME verification&lt;/strong&gt; (&lt;code&gt;dns_expected_cname&lt;/code&gt;) — CNAME records point to the expected target. Critical for CDN configurations where a wrong CNAME means serving from the wrong origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TXT validation&lt;/strong&gt; (&lt;code&gt;dns_txt_contains&lt;/code&gt;) — TXT records contain the correct SPF, DKIM, or domain verification strings. A broken SPF record means your email gets flagged as spam.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact match&lt;/strong&gt; (&lt;code&gt;dns_record_equals&lt;/code&gt;) and &lt;strong&gt;substring match&lt;/strong&gt; (&lt;code&gt;dns_record_contains&lt;/code&gt;) — verify any record type against an expected value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record count&lt;/strong&gt; (&lt;code&gt;dns_min_answers&lt;/code&gt;, &lt;code&gt;dns_max_answers&lt;/code&gt;) — verify records haven't been silently deleted or duplicated. A domain that should have two A records for failover dropping to one is a signal, even though DNS still "works."&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Health checks
&lt;/h3&gt;

&lt;p&gt;TTL assertions monitor cache hygiene. &lt;code&gt;dns_ttl_low&lt;/code&gt; warns when any record's TTL drops below a floor (catching the "forgot to raise TTL after migration" pattern), while &lt;code&gt;dns_ttl_high&lt;/code&gt; warns when TTL exceeds a ceiling (catching stale-cache risk before a planned change).&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-region DNS monitoring
&lt;/h2&gt;

&lt;p&gt;DNS propagation is not instant. When you update a record, the old value persists in every resolver's cache until its TTL expires. A record change at 14:00 UTC with a 3,600-second TTL won't be fully propagated until 15:00 UTC — and that's the optimistic case. Some resolvers ignore TTL or cap it at their own maximum.&lt;/p&gt;

&lt;p&gt;Running DNS checks from a single location tells you whether &lt;em&gt;that resolver&lt;/em&gt; sees the correct value. It says nothing about what users in other regions see. Multi-region monitoring catches the scenarios that actually cause user-facing incidents: a propagation failure that affects Frankfurt but not Virginia, a geo-DNS rule returning wrong IPs for Asian resolvers, or a CDN CNAME that's correct in one region and stale in another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up DNS monitoring
&lt;/h2&gt;

&lt;p&gt;DevHelm's DNS monitor type supports stacking multiple assertions on a single check, so you can verify resolution, performance, accuracy, and TTL health in one monitor running from multiple regions.&lt;/p&gt;

&lt;h3&gt;
  
  
  YAML configuration
&lt;/h3&gt;

&lt;p&gt;A &lt;a href="https://devhelm.io/blog/monitoring-as-code" rel="noopener noreferrer"&gt;monitoring-as-code&lt;/a&gt; configuration covering the most common assertions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;monitors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Production&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;DNS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;example.com"&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dns&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example.com&lt;/span&gt;
    &lt;span class="na"&gt;frequency_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
    &lt;span class="na"&gt;regions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;us-east&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;eu-west&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ap-southeast&lt;/span&gt;
    &lt;span class="na"&gt;assertions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dns_resolves&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dns_response_time&lt;/span&gt;
        &lt;span class="na"&gt;max_ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dns_expected_ips&lt;/span&gt;
        &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;203.0.113.10"&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;203.0.113.11"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dns_ttl_low&lt;/span&gt;
        &lt;span class="na"&gt;min_ttl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dns_ttl_high&lt;/span&gt;
        &lt;span class="na"&gt;max_ttl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;43200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CLI
&lt;/h3&gt;

&lt;p&gt;Create the same monitor from the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;devhelm monitor create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; dns &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--frequency&lt;/span&gt; 300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east &lt;span class="nt"&gt;--region&lt;/span&gt; eu-west &lt;span class="nt"&gt;--region&lt;/span&gt; ap-southeast &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assertion&lt;/span&gt; dns_resolves &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assertion&lt;/span&gt; &lt;span class="s2"&gt;"dns_response_time&amp;lt;500"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assertion&lt;/span&gt; &lt;span class="s2"&gt;"dns_expected_ips=203.0.113.10,203.0.113.11"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assertion&lt;/span&gt; &lt;span class="s2"&gt;"dns_ttl_low&amp;gt;120"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assertion&lt;/span&gt; &lt;span class="s2"&gt;"dns_ttl_high&amp;lt;43200"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Common configurations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Domain migration monitoring.&lt;/strong&gt; Before cutting DNS to a new provider, add &lt;code&gt;dns_expected_ips&lt;/code&gt; with the new IP addresses and run checks from all regions. Once every region returns the new IPs consistently, the migration is complete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email deliverability.&lt;/strong&gt; Monitor your SPF and DKIM records to catch silent breakage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;devhelm monitor create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; dns &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assertion&lt;/span&gt; dns_resolves &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assertion&lt;/span&gt; &lt;span class="s2"&gt;"dns_txt_contains=v=spf1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assertion&lt;/span&gt; &lt;span class="s2"&gt;"dns_min_answers:mx&amp;gt;=1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;CDN CNAME verification.&lt;/strong&gt; Verify that your CDN CNAME hasn't drifted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;devhelm monitor create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; dns &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; cdn.example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assertion&lt;/span&gt; &lt;span class="s2"&gt;"dns_expected_cname=d1234.cloudfront.net"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When DNS monitoring fires
&lt;/h2&gt;

&lt;p&gt;When an alert triggers, the assertion type tells you where to look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dns_resolves&lt;/code&gt; fails&lt;/strong&gt; — check authoritative nameserver health, domain registration status, and zone file presence. Run &lt;code&gt;dig +trace example.com&lt;/code&gt; to find where the resolution chain breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dns_response_time&lt;/code&gt; exceeds threshold&lt;/strong&gt; — compare response times across resolvers. If slow from all regions, the authoritative server is overloaded or rate-limiting. If slow from one region, it's a network path issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dns_expected_ips&lt;/code&gt; mismatch&lt;/strong&gt; — verify the zone file. If the IP is one you don't recognize, investigate immediately — this is a hijacking signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dns_ttl_low&lt;/code&gt; fires&lt;/strong&gt; — someone set a low TTL during a migration and forgot to raise it. Update the TTL in your DNS provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dns_txt_contains&lt;/code&gt; fails&lt;/strong&gt; — check whether a recent zone change removed or modified your SPF/DKIM records. Email deliverability may already be degraded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A well-configured DNS monitor with stacked assertions turns these from "something feels off" into a specific, actionable alert within minutes — not hours after users start complaining. For more on building an &lt;a href="https://devhelm.io/blog/best-api-monitoring-tools" rel="noopener noreferrer"&gt;API monitoring&lt;/a&gt; layer alongside DNS checks, see our guide on the tools developers actually use.&lt;/p&gt;




&lt;p&gt;DevHelm's DNS monitors check resolution, response time, record values, and TTL health from multiple regions — catching propagation failures and hijacking before users notice. Start at &lt;a href="https://app.devhelm.io" rel="noopener noreferrer"&gt;app.devhelm.io&lt;/a&gt;, free for your first 50 monitors.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devhelm.io/blog/dns-monitoring-tools" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>guides</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Monitoring Alerts: The Definitive Guide to Alerting Without Alert Fatigue</title>
      <dc:creator>DevHelm</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:23:24 +0000</pubDate>
      <link>https://dev.to/devhelm/monitoring-alerts-the-definitive-guide-to-alerting-without-alert-fatigue-mh5</link>
      <guid>https://dev.to/devhelm/monitoring-alerts-the-definitive-guide-to-alerting-without-alert-fatigue-mh5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;How do you set up monitoring alerts that wake the responsible on-call engineer, but do not train the team to ignore the pager? Your API can fail from two regions while Slack gets five duplicate warnings. A certificate can expire in 30 days and still page someone at 3 AM. A checkout outage can go to a general engineering channel because nobody set the owner.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Alerting fails when engineers have not defined which cases should wake the on-call engineer. A short warning gets treated like a production outage. Three signs of the same dependency failure become three incidents. Database alerts reach people who cannot fix the database. The fix starts with a clear decision: keep alerts that need action now, downgrade alerts that can wait, and remove alerts nobody uses.&lt;/p&gt;

&lt;p&gt;Monitoring alerts connect a failed check to a human response. That connection is expensive. It can stop work, wake someone from sleep, or break a weekend. If the alert is real, that cost is worth it. If the alert is unclear, repeated, or impossible to act on, it creates alert fatigue.&lt;/p&gt;

&lt;p&gt;This guide explains a simple rule: every page should be urgent, useful, and require a human. We will cover what to alert on, how severity should control routing, how a notification policy reduces noise, how alert quality affects the on-call engineer, and how to set up DevHelm so the right channel gets the right alert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why monitoring alerts matter
&lt;/h2&gt;

&lt;p&gt;Monitoring answers "is the system healthy?" Alerting answers "who needs to act now?"&lt;/p&gt;

&lt;p&gt;Those are different questions. A dashboard can show many metrics because people open it when they need details. An alert is pushed to a person. It needs a higher bar.&lt;/p&gt;

&lt;p&gt;A healthy monitoring system usually has three output paths:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal type&lt;/th&gt;
&lt;th&gt;Destination&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Urgent and actionable&lt;/td&gt;
&lt;td&gt;Page on-call&lt;/td&gt;
&lt;td&gt;Checkout API returns 500s from multiple regions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Important but not urgent&lt;/td&gt;
&lt;td&gt;Ticket or Slack&lt;/td&gt;
&lt;td&gt;TLS certificate expires in 21 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Useful context only&lt;/td&gt;
&lt;td&gt;Dashboard or log&lt;/td&gt;
&lt;td&gt;CPU crossed 70% for two minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mistake is sending all three to the same place. A Slack channel with deploy notes, warning alerts, dependency pings, and real outages becomes a junk drawer. A phone alert for non-urgent warnings becomes noise. The alerting layer should sort the message before a human sees it.&lt;/p&gt;

&lt;p&gt;Start with good monitoring coverage: uptime checks, DNS checks, SSL checks, API checks, and the user journeys that prove your product works. This guide starts after those checks exist. The goal is to turn many check results into a small number of trusted monitoring alerts.&lt;/p&gt;

&lt;p&gt;The right approach is easier to see side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bad approach&lt;/th&gt;
&lt;th&gt;Right approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Every failed check pages the on-call engineer&lt;/td&gt;
&lt;td&gt;Only urgent, user-impacting failures page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warnings and outages use the same channel&lt;/td&gt;
&lt;td&gt;Critical, warning, and info alerts use different routes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alerts go to a general engineering channel&lt;/td&gt;
&lt;td&gt;Alerts go to the responsible team or on-call rotation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One outage creates five separate alerts&lt;/td&gt;
&lt;td&gt;Related alerts are grouped into one incident&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old noisy alerts stay forever&lt;/td&gt;
&lt;td&gt;Unused alerts are deleted or downgraded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planned maintenance pages the team&lt;/td&gt;
&lt;td&gt;Maintenance windows mute only expected alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Alert fatigue: what to page, downgrade, or remove
&lt;/h2&gt;

&lt;p&gt;Alert fatigue happens when engineers receive so many low-value alerts that they stop trusting the alert channel. This is usually a process problem, not a personal problem, and the common causes are predictable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The alert has no clear action.&lt;/strong&gt; "CPU is high" tells the responder almost nothing. Is customer traffic failing? Is a queue backing up? Is there a deploy in progress? If the alert does not point to a next step, it should not page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The alert is not urgent.&lt;/strong&gt; A certificate that expires in 30 days matters, but it does not need a 3 AM phone call. It belongs in Slack, email, or a ticket queue. A certificate that expires tomorrow may need business-hours escalation. An expired production certificate needs a page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The alert repeats another alert.&lt;/strong&gt; One database outage can trigger API errors, checkout failures, queue warnings, failed jobs, and status page changes. If those arrive as separate alerts, the on-call engineer spends the first ten minutes sorting them instead of fixing the issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The alert has no owner.&lt;/strong&gt; A general engineering channel is not ownership. Every alert should map to a service, a team, or an on-call rotation. "Someone should look" usually means nobody will.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The alert has too little context.&lt;/strong&gt; The responder needs the failed service, severity, region, status, runbook link, dashboard link, and escalation path. An unclear alert makes the engineer rebuild context while half awake.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The durable fix is a notification policy that sorts alerts before they reach people.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring alerts: signal vs noise
&lt;/h3&gt;

&lt;p&gt;Good alerts start from user impact. Bad alerts start from a metric that was easy to graph.&lt;/p&gt;

&lt;p&gt;Use this test: if the alert fires, can the responder take a clear action right now to protect users or revenue? If yes, it may deserve a page. If no, downgrade it.&lt;/p&gt;

&lt;p&gt;Strong monitoring alerts usually look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;th&gt;Best route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Customer-facing endpoint is down from more than one region&lt;/td&gt;
&lt;td&gt;Users are blocked, not just one probe&lt;/td&gt;
&lt;td&gt;Page the on-call engineer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error rate crosses the service's SLO burn threshold&lt;/td&gt;
&lt;td&gt;You are spending the error budget too fast&lt;/td&gt;
&lt;td&gt;Page or Slack with notification, based on severity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P95 or P99 latency stays above the user-visible limit&lt;/td&gt;
&lt;td&gt;The product is slow enough for users to feel it&lt;/td&gt;
&lt;td&gt;Slack with notification, page if revenue path is affected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core background job stops making progress&lt;/td&gt;
&lt;td&gt;Work is stuck even if the API still returns 200&lt;/td&gt;
&lt;td&gt;Page if user data or billing is affected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database load is high because one user runs repeated expensive queries&lt;/td&gt;
&lt;td&gt;One customer can slow the whole product for everyone&lt;/td&gt;
&lt;td&gt;Slack with notification, page if production health is at risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency needed for checkout, login, billing, or deployment is unreachable&lt;/td&gt;
&lt;td&gt;Your product may fail because another service failed&lt;/td&gt;
&lt;td&gt;Page for critical paths, Slack for non-critical paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certificate is expired or close enough to expiry that renewal probably failed&lt;/td&gt;
&lt;td&gt;Users or API clients may be blocked soon&lt;/td&gt;
&lt;td&gt;Slack or email early, page when expired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS records no longer match expected values&lt;/td&gt;
&lt;td&gt;Traffic may go to the wrong place&lt;/td&gt;
&lt;td&gt;Page for production domains&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For database load, make the alert specific. "DB load high" is weak. "User 123 is running a 10-year count query 80 times in 5 minutes and production latency is rising" is useful. That tells the responsible engineer what to check and what to stop.&lt;/p&gt;

&lt;p&gt;Weak alert candidates should usually be downgraded:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Better route&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single-server CPU or memory usage without customer impact&lt;/td&gt;
&lt;td&gt;Dashboard or log warning&lt;/td&gt;
&lt;td&gt;It may be useful context, but it is not a page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One failed check from one region that recovers on the next run&lt;/td&gt;
&lt;td&gt;Log warning or silent Slack&lt;/td&gt;
&lt;td&gt;One probe can fail because of network noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A warning that has fired every week for months without action&lt;/td&gt;
&lt;td&gt;Delete, tune, or make a ticket&lt;/td&gt;
&lt;td&gt;Repeated ignored alerts create alert fatigue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Informational deploy events&lt;/td&gt;
&lt;td&gt;Log or silent Slack&lt;/td&gt;
&lt;td&gt;Useful history, not an alert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Errors from non-production routed to production on-call&lt;/td&gt;
&lt;td&gt;Team Slack only&lt;/td&gt;
&lt;td&gt;Staging should not wake production on-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runbook says "wait and see"&lt;/td&gt;
&lt;td&gt;Dashboard or ticket&lt;/td&gt;
&lt;td&gt;If no action is expected, it should not page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Some important alerts start inside your code, not only in external monitoring. Use your logger and error tools to mark business-critical cases.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code-level signal&lt;/th&gt;
&lt;th&gt;Log level&lt;/th&gt;
&lt;th&gt;Sentry/Grafana rule&lt;/th&gt;
&lt;th&gt;Best route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parser fails for a top-tier customer feed&lt;/td&gt;
&lt;td&gt;Critical/error&lt;/td&gt;
&lt;td&gt;Alert when failures &amp;gt; 0 for that customer in 5 minutes&lt;/td&gt;
&lt;td&gt;Page responsible engineer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Billing webhook signature fails repeatedly&lt;/td&gt;
&lt;td&gt;Critical/error&lt;/td&gt;
&lt;td&gt;Alert when failures spike above baseline&lt;/td&gt;
&lt;td&gt;Page on-call engineer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Login token validation fails for many users&lt;/td&gt;
&lt;td&gt;Critical/error&lt;/td&gt;
&lt;td&gt;Alert on error rate by endpoint and status code&lt;/td&gt;
&lt;td&gt;Page on-call engineer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data export job misses its SLA&lt;/td&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;td&gt;Alert when job age exceeds expected window&lt;/td&gt;
&lt;td&gt;Slack with notification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optional enrichment API times out&lt;/td&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;td&gt;Track rate and latency on dashboard&lt;/td&gt;
&lt;td&gt;Slack or ticket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debug-only parsing mismatch in staging&lt;/td&gt;
&lt;td&gt;Warning/info&lt;/td&gt;
&lt;td&gt;Dashboard only&lt;/td&gt;
&lt;td&gt;Log warning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, if your app has parsers that extract data for top-tier clients, do not rely only on a generic "worker failed" alert. Add a clear code-level signal such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;client tier: top-tier, paid, trial&lt;/li&gt;
&lt;li&gt;parser name: invoices, shipments, uptime imports, HTML status parser&lt;/li&gt;
&lt;li&gt;input source: webhook, S3 file, API sync&lt;/li&gt;
&lt;li&gt;failure reason: schema mismatch, HTML changed, selector missing, auth error, empty response, timeout&lt;/li&gt;
&lt;li&gt;affected customer or workspace id&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then build rules on top of that signal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Critical:&lt;/strong&gt; top-tier parser fails and no successful run happens within the next 5 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warning:&lt;/strong&gt; paid-customer parser has more than 3 failures in 30 minutes but later recovers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log only:&lt;/strong&gt; trial-customer parser has one malformed optional field and the import still succeeds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Parser alerts need a clear failure reason. "Parser failed" is too broad. If your parser reads HTML from a third-party site and that site changes its markup, the alert should say what changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;expected selector was not found&lt;/li&gt;
&lt;li&gt;table column count changed&lt;/li&gt;
&lt;li&gt;required JSON-LD field is missing&lt;/li&gt;
&lt;li&gt;date format changed&lt;/li&gt;
&lt;li&gt;login page appeared instead of data page&lt;/li&gt;
&lt;li&gt;response is empty or blocked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That detail makes the fix much faster. The responsible engineer can open the parser code, update the selector or schema handling, add a test case for the new HTML, and deploy the fix. Without the reason, they first have to reproduce the failure, compare old and new HTML, and guess what broke.&lt;/p&gt;

&lt;p&gt;Tools like Sentry can turn repeated exceptions into issues and alert rules. Grafana can turn logs and metrics into dashboards and alert rules. The important part is the filter: do not alert on every exception. Alert on the exceptions that match a real business case, a responsible owner, and a clear action.&lt;/p&gt;

&lt;p&gt;Logging is the raw material for many code-level alerts. If your team is still deciding how to structure logs in Node.js, see &lt;a href="https://devhelm.io/blog/winston-vs-pino" rel="noopener noreferrer"&gt;Winston vs Pino: Choosing a Node.js Logger in 2026&lt;/a&gt;. For the broader split between logs, metrics, and alerts, see &lt;a href="https://devhelm.io/blog/monitoring-and-logging" rel="noopener noreferrer"&gt;Monitoring and Logging: How They Work Together&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining alert rules in DevHelm, Sentry, and Grafana
&lt;/h3&gt;

&lt;p&gt;Most teams use more than one alert source. That is fine, but each tool should own the rule type it is best at.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best alert rules&lt;/th&gt;
&lt;th&gt;Bad alert rules&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DevHelm&lt;/td&gt;
&lt;td&gt;External checks: uptime, API, DNS, SSL, heartbeat, dependency health&lt;/td&gt;
&lt;td&gt;Internal stack traces that only exist inside app code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;td&gt;Exceptions, failed jobs, parser errors, customer-impacting code failures&lt;/td&gt;
&lt;td&gt;Simple uptime checks or "is the site reachable?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grafana&lt;/td&gt;
&lt;td&gt;Metrics and logs: database load, queue depth, CPU, memory, error-rate trends&lt;/td&gt;
&lt;td&gt;Single business events that need customer context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use DevHelm when the question is "does the service work from outside?"&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;DevHelm rule&lt;/th&gt;
&lt;th&gt;Example route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API returns 5xx from two regions&lt;/td&gt;
&lt;td&gt;Page on-call engineer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL certificate is under 30 days&lt;/td&gt;
&lt;td&gt;Slack or ticket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS record changed from expected value&lt;/td&gt;
&lt;td&gt;Page platform on-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heartbeat job missed two runs&lt;/td&gt;
&lt;td&gt;Slack with notification, page if billing/data is affected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use Sentry when the question is "did the code fail in a way that matters?"&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sentry rule&lt;/th&gt;
&lt;th&gt;Example route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New critical exception in checkout flow&lt;/td&gt;
&lt;td&gt;Page on-call engineer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parser fails for top-tier customer&lt;/td&gt;
&lt;td&gt;Page responsible engineer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same auth exception affects 20 users in 5 minutes&lt;/td&gt;
&lt;td&gt;Slack with notification or page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error appears only in staging&lt;/td&gt;
&lt;td&gt;Team Slack or issue, no production page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use Grafana when the question is "is a metric moving into a dangerous range?"&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grafana rule&lt;/th&gt;
&lt;th&gt;Example route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database CPU/load stays high for 10 minutes&lt;/td&gt;
&lt;td&gt;Slack with notification, page if latency rises&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue depth grows for 15 minutes&lt;/td&gt;
&lt;td&gt;Slack with notification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error rate crosses SLO burn threshold&lt;/td&gt;
&lt;td&gt;Page or Slack based on severity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory rises slowly but users are fine&lt;/td&gt;
&lt;td&gt;Dashboard or ticket&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule of thumb: &lt;strong&gt;DevHelm watches the outside user path, Sentry watches code failures, and Grafana watches system trends.&lt;/strong&gt; Route them through the same notification policy model so the on-call engineer sees one clear incident instead of three disconnected alerts.&lt;/p&gt;

&lt;p&gt;The stronger pattern is to alert on user-visible symptoms and keep causes as context. "Checkout is failing from us-east and eu-west" is the page. "Database connection pool is full" is useful detail. The on-call engineer needs both, but the user-visible problem should decide whether someone gets woken up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sre.google/sre-book/monitoring-distributed-systems/" rel="noopener noreferrer"&gt;Google's SRE guidance&lt;/a&gt; makes this point clearly: pages should be urgent, useful, new, and tied to user-visible problems. &lt;a href="https://prometheus.io/docs/alerting/latest/alertmanager/" rel="noopener noreferrer"&gt;Prometheus Alertmanager&lt;/a&gt; follows the same idea with grouping, inhibition, and silences. The modern version is simple: classify first, notify second.&lt;/p&gt;

&lt;h3&gt;
  
  
  How severity levels route monitoring alerts
&lt;/h3&gt;

&lt;p&gt;Severity connects detection to notification. Without severity, every alert looks equally urgent. With severity, critical outages can page people, while warnings stay out of the pager path.&lt;/p&gt;

&lt;p&gt;Use a small scale. Most teams need three alert severities:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Notification behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;Customer-facing outage, data loss risk, or broken revenue path&lt;/td&gt;
&lt;td&gt;Page on-call immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;td&gt;Degradation, higher risk, or upcoming failure&lt;/td&gt;
&lt;td&gt;Slack, email, or business-hours escalation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Info&lt;/td&gt;
&lt;td&gt;Useful operational context&lt;/td&gt;
&lt;td&gt;Dashboard, log, or digest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This should match your incident severity model. A critical alert usually maps to a Sev1 or Sev2 incident. A warning may become a Sev3 if it continues or gets worse. Info should rarely become an incident unless a human promotes it.&lt;/p&gt;

&lt;p&gt;Severity should be set at the source. Do not ask the notification channel to guess. The monitor, alert rule, or policy should know whether a failed check is critical or warning based on the service, environment, assertion, and confirmation window.&lt;/p&gt;

&lt;p&gt;For a deeper incident triage model, see &lt;a href="https://devhelm.io/blog/incident-severity-levels" rel="noopener noreferrer"&gt;Incident Severity Levels: Sev1-Sev4 with Triage Matrix&lt;/a&gt;. The simple alerting rule is this: if two severities notify the same people, through the same channel, with the same urgency, one level is not needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Notification channels for monitoring alerts
&lt;/h3&gt;

&lt;p&gt;Alert fatigue often gets worse because teams treat channels as personal choices instead of alerting tools. Slack, email, SMS, phone, webhooks, PagerDuty, and OpsGenie each have a job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slack is good for team awareness.&lt;/strong&gt; It works for warnings, incident channels, resolved alerts, and routing context. It is weak as the only path for critical pages because people mute channels, miss threads, and close laptops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email is good for low-urgency follow-up.&lt;/strong&gt; It works for reports, digests, and non-urgent warnings. It is poor for incident response because inboxes are crowded and delivery is not always fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SMS and phone are good for critical pages.&lt;/strong&gt; They are loud by design. Save them for problems that need a human now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhooks are good for automation.&lt;/strong&gt; Send alerts to a workflow tool, ticket system, incident tool, or custom responder. A webhook should not be the only record of a critical incident unless the receiver is reliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PagerDuty and OpsGenie-style tools are good for on-call schedules.&lt;/strong&gt; They know who is on-call, how to escalate, and whether the first responder acknowledged. They work best when the monitoring tool sends clean alerts with clear severity.&lt;/p&gt;

&lt;p&gt;Here are ten common routing examples:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Best route&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Checkout API returns 500s from two or more regions&lt;/td&gt;
&lt;td&gt;Page the on-call engineer&lt;/td&gt;
&lt;td&gt;Users cannot pay, so action is needed now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Login is down for production users&lt;/td&gt;
&lt;td&gt;Page the on-call engineer&lt;/td&gt;
&lt;td&gt;Users cannot enter the product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database is unavailable for the main app&lt;/td&gt;
&lt;td&gt;Page the responsible backend or platform on-call&lt;/td&gt;
&lt;td&gt;The owner can act fastest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment provider webhook is failing&lt;/td&gt;
&lt;td&gt;Page on-call and post to incident Slack&lt;/td&gt;
&lt;td&gt;Revenue path is at risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API latency is high but requests still succeed&lt;/td&gt;
&lt;td&gt;Slack with notification&lt;/td&gt;
&lt;td&gt;The team should look soon, but it may not need a page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL certificate expires in 21-30 days&lt;/td&gt;
&lt;td&gt;Email or ticket&lt;/td&gt;
&lt;td&gt;Important, but not urgent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL certificate expires tomorrow&lt;/td&gt;
&lt;td&gt;Slack with notification, then escalate during business hours&lt;/td&gt;
&lt;td&gt;Renewal likely failed and needs fast follow-up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging monitor fails&lt;/td&gt;
&lt;td&gt;Slack without notifying the whole team&lt;/td&gt;
&lt;td&gt;Useful for developers, not production on-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU crosses 70% for two minutes&lt;/td&gt;
&lt;td&gt;Log warning or dashboard&lt;/td&gt;
&lt;td&gt;Context only unless users are affected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A deploy starts or finishes&lt;/td&gt;
&lt;td&gt;Log or silent Slack message&lt;/td&gt;
&lt;td&gt;Useful history, not an alert&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Notification policy setup for monitoring alerts
&lt;/h2&gt;

&lt;p&gt;A notification policy is a set of rules for alerts. It decides where an alert goes, who sees it, how it groups with related alerts, and whether it should be muted.&lt;/p&gt;

&lt;p&gt;Think of it as a routing tree:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What service or monitor generated the alert?&lt;/li&gt;
&lt;li&gt;What environment did it come from?&lt;/li&gt;
&lt;li&gt;What severity is it?&lt;/li&gt;
&lt;li&gt;Has the same incident already opened?&lt;/li&gt;
&lt;li&gt;Is a maintenance window active?&lt;/li&gt;
&lt;li&gt;Is quiet-hours behavior different for this severity?&lt;/li&gt;
&lt;li&gt;Which channel or escalation chain should receive it?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That tree gives you control. A critical checkout outage can page the primary on-call engineer, post to the incident channel, and update the status page. A warning on a staging monitor can go to Slack during business hours. A planned maintenance event can mute expected failures without turning the monitor off.&lt;/p&gt;

&lt;p&gt;Good policies usually route by these attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Severity:&lt;/strong&gt; critical, warning, info.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment:&lt;/strong&gt; production, staging, development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service or tag:&lt;/strong&gt; checkout, API, auth, billing, database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Region:&lt;/strong&gt; global failure vs. single-region degradation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor type:&lt;/strong&gt; HTTP, DNS, SSL, TCP, heartbeat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ownership:&lt;/strong&gt; team or rotation responsible for the service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time window:&lt;/strong&gt; business hours, quiet hours, planned maintenance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The default policy matters too. Alerts that do not match a specific rule should not disappear. Send them to a visible catch-all channel with enough context to fix the policy. A missing route is a config bug.&lt;/p&gt;

&lt;p&gt;For teams using &lt;a href="https://devhelm.io/blog/monitoring-as-code" rel="noopener noreferrer"&gt;monitoring as code&lt;/a&gt;, notification policies should live beside monitor definitions. The monitor says what to check. The policy says who gets notified when that check fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert routing and escalation for on-call engineers
&lt;/h3&gt;

&lt;p&gt;Routing answers one question: who should get this alert first? Escalation answers a different one: what happens if they do not respond?&lt;/p&gt;

&lt;p&gt;Mixing these two ideas creates fragile alerting. A route should be based on ownership and context. An escalation chain should be based on time and responsibility.&lt;/p&gt;

&lt;p&gt;Example escalation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time since alert&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 minutes&lt;/td&gt;
&lt;td&gt;Notify primary on-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 minutes unacknowledged&lt;/td&gt;
&lt;td&gt;Notify secondary on-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15 minutes unacknowledged&lt;/td&gt;
&lt;td&gt;Notify engineering lead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 minutes unresolved for critical severity&lt;/td&gt;
&lt;td&gt;Open broader incident response&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Test both layers. A good routing policy is useless if the backup never gets paged. A good escalation chain is useless if every database alert goes to the wrong team first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert deduplication, grouping, and suppression
&lt;/h3&gt;

&lt;p&gt;The most useful alerting work usually happens before the notification is sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deduplication&lt;/strong&gt; stops the same alert from notifying again and again. If the checkout monitor fails every 30 seconds for ten minutes, the on-call engineer should not receive twenty identical pages. They should receive the first alert, updates when the state changes, and a resolved message when the monitor recovers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grouping&lt;/strong&gt; combines related alerts into one incident or notification. During a dependency outage, many monitors may fail for the same reason. Grouping by service, dependency, severity, and time window keeps the responder focused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suppression&lt;/strong&gt; stops expected or lower-value alerts from notifying people. Maintenance windows are the common case. If you know the database will restart during a planned migration, mute that alert for the window but keep the monitor running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inhibition&lt;/strong&gt; mutes smaller symptoms when a bigger alert already explains them. If "cluster unavailable" is firing, individual pod warnings do not need to page. They can stay visible as context without becoming separate pages.&lt;/p&gt;

&lt;p&gt;These controls separate alert management from simple message delivery. A PagerDuty alternative, an incident management platform, or a monitoring tool can all send messages. The important question is whether the tool reduces duplicate human work before the alert reaches a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  On-call engineers need trusted alerts
&lt;/h2&gt;

&lt;p&gt;An on-call rotation depends on alert quality. You can design a fair schedule, pay engineers properly, and write good handoff notes. The team can still burn out if monitoring alerts are low value.&lt;/p&gt;

&lt;p&gt;The target is trusted alerts. A quiet system may not monitor enough. A noisy system can still miss the failure that matters.&lt;/p&gt;

&lt;p&gt;Credible alerts have five traits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The responder understands what broke.&lt;/li&gt;
&lt;li&gt;The alert maps to customer or business impact.&lt;/li&gt;
&lt;li&gt;The route points to the team that can act.&lt;/li&gt;
&lt;li&gt;The alert includes context and a runbook.&lt;/li&gt;
&lt;li&gt;The alert history shows that pages usually matter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For schedule design, handoffs, pay, and rotation models, read &lt;a href="https://devhelm.io/blog/on-call-rotation-best-practices" rel="noopener noreferrer"&gt;On-Call Rotation Best Practices for Engineering Teams&lt;/a&gt;. This guide focuses on the input side: making sure the on-call engineer receives signal instead of noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quiet hours and maintenance windows for alert fatigue
&lt;/h3&gt;

&lt;p&gt;Quiet hours do not mean critical alerts go silent. They mean non-critical alerts wait until a better time.&lt;/p&gt;

&lt;p&gt;A good quiet-hours policy usually looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alert severity&lt;/th&gt;
&lt;th&gt;During business hours&lt;/th&gt;
&lt;th&gt;During quiet hours&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;Page immediately&lt;/td&gt;
&lt;td&gt;Page immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;td&gt;Slack or ticket&lt;/td&gt;
&lt;td&gt;Queue for morning unless worsening&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Info&lt;/td&gt;
&lt;td&gt;Dashboard or digest&lt;/td&gt;
&lt;td&gt;Dashboard or digest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Maintenance windows mute expected alerts during planned work. The important word is expected. If you are restarting a database, mute the database availability alert for that window. Keep the rest of production alerting active.&lt;/p&gt;

&lt;p&gt;Use narrow matchers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Specific service or monitor.&lt;/li&gt;
&lt;li&gt;Specific environment.&lt;/li&gt;
&lt;li&gt;Specific time window.&lt;/li&gt;
&lt;li&gt;Specific expected failure mode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Broad silences are dangerous because they hide unrelated incidents. A maintenance window for checkout should not mute auth, DNS, or the public status page. Keep the policy narrow so an unrelated critical failure still reaches on-call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measuring monitoring alert quality
&lt;/h3&gt;

&lt;p&gt;You can measure alert quality. If you do not measure it, alerting gets worse over time.&lt;/p&gt;

&lt;p&gt;Track these metrics monthly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Alerts per on-call shift&lt;/td&gt;
&lt;td&gt;Human load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages per incident&lt;/td&gt;
&lt;td&gt;How well duplicate alerts are grouped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actionable alert rate&lt;/td&gt;
&lt;td&gt;Signal-to-noise ratio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTTA&lt;/td&gt;
&lt;td&gt;Whether alerts are trusted and routed to the right person&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False positive count&lt;/td&gt;
&lt;td&gt;Whether thresholds are too sensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alerts with runbooks&lt;/td&gt;
&lt;td&gt;Whether responders have clear next steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alerts by service owner&lt;/td&gt;
&lt;td&gt;Ownership gaps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Suppressed alerts during maintenance&lt;/td&gt;
&lt;td&gt;Whether planned work is being handled cleanly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;MTTA is especially useful. It measures the time between alert delivery and human acknowledgment. A rising MTTA can mean the alert is hard to see, routed to the wrong person, or ignored because the channel is noisy. For the full incident metric model, see &lt;a href="https://devhelm.io/blog/mtta-mttr-mtbf-difference" rel="noopener noreferrer"&gt;MTTA, MTTR, MTBF, MTTF - The Four Incident Metrics, Compared&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Run a monthly alert review with three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which alerts fired most often?&lt;/li&gt;
&lt;li&gt;Which alerts did not lead to action?&lt;/li&gt;
&lt;li&gt;Which incidents produced more than one page?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Delete, downgrade, group, or rewrite anything that fails the review. Alerting is not a one-time setup. Review it every month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring alert setup example
&lt;/h2&gt;

&lt;p&gt;Here is a simple policy for a small SaaS team with an API, web app, checkout flow, and public status page.&lt;/p&gt;

&lt;p&gt;Start with monitors:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Monitor&lt;/th&gt;
&lt;th&gt;Failure behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Homepage uptime&lt;/td&gt;
&lt;td&gt;Warning for one-region failure, critical for multi-region failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API health endpoint&lt;/td&gt;
&lt;td&gt;Critical on multi-region 5xx or timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout endpoint&lt;/td&gt;
&lt;td&gt;Critical on failed assertion or elevated latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS records&lt;/td&gt;
&lt;td&gt;Warning on record drift, critical on resolution failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL certificate&lt;/td&gt;
&lt;td&gt;Warning under 30 days, critical when expired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background job heartbeat&lt;/td&gt;
&lt;td&gt;Warning after one missed interval, critical after sustained failure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then define alert channels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Engineering Slack&lt;/td&gt;
&lt;td&gt;Warnings, resolved alerts, context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary pager&lt;/td&gt;
&lt;td&gt;Critical production pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secondary pager&lt;/td&gt;
&lt;td&gt;Unacknowledged critical escalation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email or ticket queue&lt;/td&gt;
&lt;td&gt;Non-urgent follow-up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhook&lt;/td&gt;
&lt;td&gt;Automation, incident creation, or audit record&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then define notification policies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Destination&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;production + critical&lt;/td&gt;
&lt;td&gt;Primary pager immediately, engineering Slack for visibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;production + critical + unacknowledged after 5 minutes&lt;/td&gt;
&lt;td&gt;Secondary pager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;production + warning&lt;/td&gt;
&lt;td&gt;Engineering Slack during business hours, queue after hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;staging + critical&lt;/td&gt;
&lt;td&gt;Team Slack only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;info&lt;/td&gt;
&lt;td&gt;Dashboard or digest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maintenance window match&lt;/td&gt;
&lt;td&gt;Mute matched warning/critical alerts for that service only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Finally, attach runbook and ownership metadata:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metadata&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service owner&lt;/td&gt;
&lt;td&gt;Prevents "who owns this?" delay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runbook URL&lt;/td&gt;
&lt;td&gt;Gives the first responder a next step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard URL&lt;/td&gt;
&lt;td&gt;Speeds diagnosis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status page component&lt;/td&gt;
&lt;td&gt;Keeps customer updates tied to the same signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recent deploy link&lt;/td&gt;
&lt;td&gt;Helps spot change-related incidents&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Runbooks are the most important link in this table. A good alert should not just say what broke; it should point to the first action. For a deeper format, see &lt;a href="https://devhelm.io/blog/runbooks" rel="noopener noreferrer"&gt;Runbooks: Anatomy, Examples, and the AI-Executable Format&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This setup is small on purpose. Start with a few alerts you trust. Add more only after the signal is good. Teams get into trouble when they create dozens of alerts before they have routing, severity, and review habits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up monitoring alerts with DevHelm
&lt;/h3&gt;

&lt;p&gt;DevHelm splits alerting into two reusable parts: alert channels and notification policies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alert channels&lt;/strong&gt; are destinations. They can be Slack, email, SMS, webhooks, PagerDuty, OpsGenie, or another place an alert can go. Create the destination once, test it, and reuse it across monitors. For the exact setup flow and supported channel types, see the &lt;a href="https://docs.devhelm.io/alerting/channels" rel="noopener noreferrer"&gt;alert channels reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notification policies&lt;/strong&gt; are routing rules. They decide which channel fires for each monitor, severity, tag, service, or escalation step. This is where you define quiet hours, severity routing, deduplication, escalation chains, and maintenance windows. For match rules, priority, and evaluation order, see the &lt;a href="https://docs.devhelm.io/alerting/notification-policies" rel="noopener noreferrer"&gt;notification policies reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A clean DevHelm setup follows this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the first monitor and first alert channel. If you are starting from zero, follow &lt;a href="https://docs.devhelm.io/guides/first-http-monitor" rel="noopener noreferrer"&gt;First HTTP monitor&lt;/a&gt;, then &lt;a href="https://docs.devhelm.io/guides/first-alert" rel="noopener noreferrer"&gt;First alert&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Create channels for the real destinations: engineering Slack, critical pager, secondary escalation, and ticket/email follow-up. For end-to-end setup, see the &lt;a href="https://docs.devhelm.io/guides/alerting" rel="noopener noreferrer"&gt;alerting guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Tag monitors by service and environment: production, staging, checkout, API, auth, billing. If you route by ownership, use &lt;a href="https://docs.devhelm.io/guides/alert-routing-by-tag" rel="noopener noreferrer"&gt;alert routing by tag&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Set severity based on impact: critical for customer-facing outages, warning for degradation or upcoming failure, info for context.&lt;/li&gt;
&lt;li&gt;Build notification policies from specific to broad: checkout critical first, production critical next, warnings after that, catch-all last.&lt;/li&gt;
&lt;li&gt;Add escalation only where the first responder might miss the alert. For a three-step model, see &lt;a href="https://docs.devhelm.io/guides/tiered-escalation" rel="noopener noreferrer"&gt;tiered escalation&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Add quiet-hours behavior for warnings and info.&lt;/li&gt;
&lt;li&gt;Add maintenance windows for planned work. For scoped planned downtime, see &lt;a href="https://docs.devhelm.io/guides/maintenance-windows" rel="noopener noreferrer"&gt;maintenance windows&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Test the full path from failed monitor to delivered notification before trusting it in production. For the test flow, see &lt;a href="https://docs.devhelm.io/guides/testing-your-alerts" rel="noopener noreferrer"&gt;testing your alerts&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key is reuse. Do not configure custom alert behavior on every monitor unless the monitor is truly special. A policy should define the team's alerting model once. New monitors should follow that model through tags, severity, and ownership.&lt;/p&gt;

&lt;p&gt;That is how you keep alerting clean. When the payments team changes its escalation chain, update the payments policy. When Slack channel names change, update the channel. When checkout becomes more critical, update the severity rule. Do not edit twenty monitors by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  PagerDuty alternative vs better monitoring alerts
&lt;/h2&gt;

&lt;p&gt;Many teams searching for a PagerDuty alternative are really searching for less alert fatigue. These problems are related, but each layer has a different job.&lt;/p&gt;

&lt;p&gt;PagerDuty, OpsGenie, Grafana OnCall, incident.io, Rootly, Better Stack, and other incident tools handle schedules, escalation, acknowledgment, and response workflows. They are useful when you need on-call schedules and escalation. They do not automatically fix bad monitoring alerts.&lt;/p&gt;

&lt;p&gt;If the monitoring layer sends noisy alerts with little context, the on-call tool will just route noisy alerts faster. Signal quality has to improve before the pager.&lt;/p&gt;

&lt;p&gt;The better evaluation question is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can the monitoring layer classify severity before paging?&lt;/li&gt;
&lt;li&gt;Can related failures deduplicate into one incident?&lt;/li&gt;
&lt;li&gt;Can warnings avoid the pager path?&lt;/li&gt;
&lt;li&gt;Can maintenance windows suppress expected failures without hiding unrelated outages?&lt;/li&gt;
&lt;li&gt;Can the alert include owner, runbook, region, and service context?&lt;/li&gt;
&lt;li&gt;Can the incident tool escalate only after a clean alert has reached the right first responder?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For tool comparisons, see &lt;a href="https://devhelm.io/blog/best-incident-management-tools" rel="noopener noreferrer"&gt;Best Incident Management Tools in 2026&lt;/a&gt; and &lt;a href="https://devhelm.io/blog/opsgenie-alternatives" rel="noopener noreferrer"&gt;Opsgenie Alternatives in 2026&lt;/a&gt;. This guide has a narrower point: whatever pager you use, fix the signal before you optimize the route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring alert audit checklist
&lt;/h2&gt;

&lt;p&gt;Use this checklist for every paging alert:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What a good answer looks like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Is the condition user-visible or almost user-visible?&lt;/td&gt;
&lt;td&gt;It affects users, revenue, data, or a critical dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does it require action now?&lt;/td&gt;
&lt;td&gt;Waiting until tomorrow would make the incident worse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is the responder able to act?&lt;/td&gt;
&lt;td&gt;The route goes to the team that owns the service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is the owner clear?&lt;/td&gt;
&lt;td&gt;The alert names a service, team, or on-call rotation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is the severity correct?&lt;/td&gt;
&lt;td&gt;Critical pages; warning goes to Slack or ticket; info stays quiet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is the runbook linked?&lt;/td&gt;
&lt;td&gt;The first responder has a clear next step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Will duplicate alerts group together?&lt;/td&gt;
&lt;td&gt;One root cause creates one incident, not five pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is there a business-hours path for warnings?&lt;/td&gt;
&lt;td&gt;Non-urgent warnings do not wake people at night&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is maintenance muting narrow?&lt;/td&gt;
&lt;td&gt;Only the planned service and time window are muted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did this alert lead to action in the last 90 days?&lt;/td&gt;
&lt;td&gt;If not, delete it, tune it, or downgrade it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If an alert fails the checklist, do not leave it as a page. Rewrite it, downgrade it, group it, or delete it.&lt;/p&gt;

&lt;p&gt;The best alerting systems are boring in the right way. Most signals go to dashboards, tickets, or quiet Slack channels. A small number reach the on-call engineer. When they do, the engineer trusts the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the difference between monitoring and alerting?
&lt;/h3&gt;

&lt;p&gt;Monitoring collects health signals from systems, services, and user journeys. Alerting decides which signals need human attention and sends them to the right place.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the best way to reduce alert fatigue quickly?
&lt;/h3&gt;

&lt;p&gt;Start with the ten noisiest alerts from the last 30 days. Delete alerts that never led to action, downgrade non-urgent warnings, and group duplicate symptoms from the same incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should warning alerts ever wake up an on-call engineer?
&lt;/h3&gt;

&lt;p&gt;Usually no. A warning should page only when it predicts a near customer-facing failure and waiting until business hours would make the incident worse.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many notification policies should a small team have?
&lt;/h3&gt;

&lt;p&gt;Most small teams can start with four notification policies: production critical, production warning, non-production, and catch-all. Add service-specific policies only when ownership or severity handling is different.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is PagerDuty a monitoring tool?
&lt;/h3&gt;

&lt;p&gt;PagerDuty is mainly an on-call and incident response tool. It routes, escalates, and tracks alerts from monitoring systems, but it usually depends on another tool to detect the failure first.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should every alert include?
&lt;/h3&gt;

&lt;p&gt;Every alert should include the affected service, severity, environment, current state, first failed time, owner, runbook link, dashboard link, and the notification policy that routed it.&lt;/p&gt;




&lt;p&gt;DevHelm routes monitoring alerts through reusable alert channels and notification policies, with severity-based routing, escalation chains, quiet hours, deduplication, and maintenance windows. Start with your first production monitor at &lt;a href="https://app.devhelm.io" rel="noopener noreferrer"&gt;app.devhelm.io&lt;/a&gt;, then set up the alerting policy before the next incident tests it for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devhelm.io/blog/monitoring-alerts" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>guides</category>
      <category>reliability</category>
    </item>
    <item>
      <title>Alert Fatigue: Why Your Team Ignores Pages and How to Fix It</title>
      <dc:creator>DevHelm</dc:creator>
      <pubDate>Wed, 08 Jul 2026 15:22:47 +0000</pubDate>
      <link>https://dev.to/devhelm/alert-fatigue-why-your-team-ignores-pages-and-how-to-fix-it-671</link>
      <guid>https://dev.to/devhelm/alert-fatigue-why-your-team-ignores-pages-and-how-to-fix-it-671</guid>
      <description>&lt;p&gt;Your on-call engineer gets 47 alerts before lunch. Twelve are the same flapping health check. Eight are downstream effects of a single database hiccup. Six are informational warnings that never require action. By 2 PM, a new alert arrives — checkout is returning 500s from three regions — and it sits in the channel for nine minutes because the person on call has learned to stop reading.&lt;/p&gt;

&lt;p&gt;That is alert fatigue. It is one of the most common ways a well-monitored system ends up with a longer incident response time than a system with no monitoring at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What alert fatigue looks like in practice
&lt;/h2&gt;

&lt;p&gt;Alert fatigue is not laziness. It is a rational response to a noisy system. When most alerts are false positives, low-priority warnings, or duplicates of the same underlying problem, engineers train themselves to ignore the pager — because ignoring it is usually the right call.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pagerduty.com/resources/reports/digital-operations/" rel="noopener noreferrer"&gt;PagerDuty's State of Digital Operations research&lt;/a&gt; found that the median on-call team receives over 300 alerts per week. Of those, roughly 30–40% are noise: alerts that fire, get acknowledged, and close without any action taken. The remaining alerts often cluster around a handful of real incidents, buried under duplicates and false alarms.&lt;/p&gt;

&lt;p&gt;The damage compounds over time. Engineers rotate off on-call shifts feeling burned out. Response times creep upward. Critical pages blend in with the noise. The team builds informal workarounds — muting channels, filtering notifications, checking Slack "when they get around to it" — that undermine the entire alerting pipeline. For more on how this affects on-call teams specifically, see &lt;a href="https://devhelm.io/blog/on-call-rotation-best-practices" rel="noopener noreferrer"&gt;on-call rotation best practices&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your alerting is too noisy
&lt;/h2&gt;

&lt;p&gt;Alert fatigue rarely has one cause. It accumulates from a series of reasonable decisions that compound into an unreasonable system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Too many monitors, not enough intent
&lt;/h3&gt;

&lt;p&gt;Teams add monitors reactively. An outage happens, someone creates an alert so it "never happens again," and the alert stays forever — even after the underlying architecture changes. The monitor count grows while the number of meaningful signals stays flat.&lt;/p&gt;

&lt;p&gt;The question is not "does this metric matter?" It usually does. The question is "does this metric need to wake someone up?" Most metrics belong on a dashboard, not in a pager.&lt;/p&gt;

&lt;h3&gt;
  
  
  No severity differentiation
&lt;/h3&gt;

&lt;p&gt;When every alert pages the on-call engineer with the same urgency, the engineer cannot tell what is actually urgent. A certificate expiring in 28 days should not arrive in the same channel, with the same sound, as a checkout outage affecting live transactions.&lt;/p&gt;

&lt;p&gt;Severity levels exist to solve this. Critical means customers are affected right now. Warning means something needs attention during business hours. Info means the team should know, but nobody needs to act. Without that separation, everything feels equally important — which means nothing does. See &lt;a href="https://devhelm.io/blog/incident-severity-levels" rel="noopener noreferrer"&gt;incident severity levels&lt;/a&gt; for a practical framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicate alerts from correlated failures
&lt;/h3&gt;

&lt;p&gt;A single database going unhealthy can trigger alerts from every service that depends on it. The API returns 500s — alert. The background job queue stalls — alert. The health check for the admin panel fails — alert. The status page component goes red — alert. One root cause, four pages.&lt;/p&gt;

&lt;p&gt;The on-call engineer spends the first ten minutes triaging instead of fixing the database. Worse, the volume itself signals "something big is happening" without clarifying what.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alerts without actions
&lt;/h3&gt;

&lt;p&gt;"CPU at 78%." So what? Should someone scale up? Is it a runaway query? Is it normal traffic? If the alert does not point to a specific action or a &lt;a href="https://devhelm.io/blog/runbooks" rel="noopener noreferrer"&gt;runbook&lt;/a&gt;, it creates noise without value. Every alert that fires without a next step trains the team to treat alerts as background chatter.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to fix it
&lt;/h2&gt;

&lt;p&gt;The fix is not adding more rules. It is removing the ones that do not earn their place and restructuring the rest so each alert is worth the interruption.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audit the alert-to-action ratio
&lt;/h3&gt;

&lt;p&gt;Pull the last 30 days of alert history. For each alert rule, count how many times it fired and how many of those resulted in a human taking action. If an alert fires 50 times a month and gets acted on twice, it needs to be downgraded, tuned, or removed.&lt;/p&gt;

&lt;p&gt;A healthy target: at least 70% of pages should lead to a meaningful response. If your ratio is below 50%, the pager has become a notification feed, not an incident signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set severity thresholds that mean something
&lt;/h3&gt;

&lt;p&gt;Map each alert to a severity level and route accordingly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;Customers affected now, revenue at risk&lt;/td&gt;
&lt;td&gt;Phone call or push notification to on-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;td&gt;Needs attention within hours, not minutes&lt;/td&gt;
&lt;td&gt;Slack or email to the owning team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Info&lt;/td&gt;
&lt;td&gt;Useful context, no action required&lt;/td&gt;
&lt;td&gt;Dashboard or daily digest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then enforce the mapping in your notification policies. Critical pages the on-call rotation. Warning posts to a team channel. Info goes nowhere near a phone. This separation alone can cut page volume by 40–60% without losing coverage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Group correlated failures into one alert
&lt;/h3&gt;

&lt;p&gt;When five services fail because one dependency went down, the on-call engineer needs one alert that says "the database is unhealthy and these services are affected" — not five independent pages.&lt;/p&gt;

&lt;p&gt;Resource groups help here. Group monitors by shared dependency — your payment provider, your primary database, your CDN — so that correlated failures produce a single notification instead of a cascade. The engineer sees one alert with context, not a wall of symptoms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tune flapping and transient detection
&lt;/h3&gt;

&lt;p&gt;A health check that fails once because of a network blip and recovers 30 seconds later is not an incident. But if your alerting fires on the first failure, the on-call engineer gets paged for something that resolved before they could open a terminal.&lt;/p&gt;

&lt;p&gt;Require consecutive failures before alerting — two or three failed checks in a row from the same region, or failures from multiple regions simultaneously. This filters transient noise without delaying real outage detection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Require every alert to have a runbook link
&lt;/h3&gt;

&lt;p&gt;This is the simplest and most effective rule to adopt. If the alert does not link to a runbook or documented response procedure, it should not page anyone. The runbook does not need to be perfect — a three-line document that says "check the database dashboard, look for long-running queries, escalate to the database team if replication lag exceeds 30 seconds" is enough.&lt;/p&gt;

&lt;p&gt;This forces the team to think through the response before wiring the alert, which naturally filters out alerts nobody knows how to act on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Review and prune quarterly
&lt;/h3&gt;

&lt;p&gt;Alert rules are not permanent. Services change, architectures evolve, and the alert that mattered six months ago may now fire on a deprecated endpoint. Schedule a quarterly review: sort alerts by frequency, check the action ratio, and delete or downgrade anything that has become noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The alert your team cannot afford to miss
&lt;/h2&gt;

&lt;p&gt;Alert fatigue is not a tooling problem at its root. It is a prioritization problem. The goal is not zero alerts — it is a system where every page is urgent, actionable, and trusted. When the on-call engineer's phone buzzes, they should think "something real is happening" instead of "probably another flapping check."&lt;/p&gt;

&lt;p&gt;Start with the audit. Pull your alert history. Find the rules that fire without action. Set &lt;a href="https://devhelm.io/blog/incident-severity-levels" rel="noopener noreferrer"&gt;severity levels&lt;/a&gt; that route alerts to the right channel. Group correlated failures so one root cause produces one page. Attach a &lt;a href="https://devhelm.io/blog/runbooks" rel="noopener noreferrer"&gt;runbook&lt;/a&gt; to every alert rule. And prune what no longer matters.&lt;/p&gt;

&lt;p&gt;The on-call shift should be boring most of the time. When it is not boring, every alert should matter. If you're ready to fix the noise, &lt;a href="https://devhelm.io/product/alerting" rel="noopener noreferrer"&gt;DevHelm's notification policies&lt;/a&gt; let you map severity levels to channels, group correlated monitors into resource groups, and require multi-region confirmation before any alert pages your team. For a deeper dive into the configuration, see the full &lt;a href="https://devhelm.io/blog/monitoring-alerts" rel="noopener noreferrer"&gt;monitoring alerts&lt;/a&gt; guide. To reduce &lt;a href="https://devhelm.io/blog/mttr-full-form" rel="noopener noreferrer"&gt;mean time to resolution&lt;/a&gt; once a real alert fires, pair it with standardized runbooks and clear escalation paths.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://devhelm.io/blog/alert-fatigue" rel="noopener noreferrer"&gt;DevHelm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>guides</category>
      <category>reliability</category>
    </item>
  </channel>
</rss>
