<?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: P Bhatnagar</title>
    <description>The latest articles on DEV Community by P Bhatnagar (@p_bhatnagar).</description>
    <link>https://dev.to/p_bhatnagar</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%2F3991056%2Fb14fd6de-60e7-47a3-877f-e6bb4bd8e9ba.png</url>
      <title>DEV Community: P Bhatnagar</title>
      <link>https://dev.to/p_bhatnagar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/p_bhatnagar"/>
    <language>en</language>
    <item>
      <title>The Monitor That Only Passed When It Won a Race</title>
      <dc:creator>P Bhatnagar</dc:creator>
      <pubDate>Sun, 27 Sep 2026 10:06:48 +0000</pubDate>
      <link>https://dev.to/p_bhatnagar/the-monitor-that-only-passed-when-it-won-a-race-1pa8</link>
      <guid>https://dev.to/p_bhatnagar/the-monitor-that-only-passed-when-it-won-a-race-1pa8</guid>
      <description>&lt;p&gt;A browser monitor I was migrating failed about two runs in every three. The rest passed cleanly. The application behind it was healthy. Nobody could see a pattern.&lt;/p&gt;

&lt;p&gt;The run history for that stretch was 67 runs. 25 passed. 42 failed. That is a 37.3% pass rate on a monitor watching an application that was working.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fke5ri5lqgv0ax823bip9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fke5ri5lqgv0ax823bip9.png" alt="Run history before the fix: 67 runs, 25 passed, 42 failed" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post is about how that failure was found and fixed. The fix itself was small. The idea behind it is the part worth keeping:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A check that passes only when it wins a race is not a passing check.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why intermittent failures are the hardest kind
&lt;/h2&gt;

&lt;p&gt;A monitor that always fails is easy. You look at it, find the broken thing, and fix it.&lt;/p&gt;

&lt;p&gt;A monitor that fails sometimes is harder, for three reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It teaches people to ignore it.&lt;/strong&gt; After a week of flapping, a red run stops meaning anything. That is the same noise problem I wrote about in &lt;a href="https://dev.to/p_bhatnagar/cutting-alert-noise-without-going-blind-a-practical-slo-driven-approach-1pkm"&gt;Cutting Alert Noise Without Going Blind&lt;/a&gt;. A flapping monitor is noise with a green light mixed in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It passes on a rerun.&lt;/strong&gt; So the easy "fix" is to add a retry and move on. The dashboard goes green. The race is still there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no single broken thing to look at.&lt;/strong&gt; The passing runs and the failing runs use the same script against the same application. Comparing them line by line shows nothing.&lt;/p&gt;

&lt;p&gt;So the useful question is not "what is broken?" It is "what is different between a run that passes and a run that fails?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the pattern
&lt;/h2&gt;

&lt;p&gt;Two things stood out in the step results.&lt;/p&gt;

&lt;p&gt;First, the failures happened at the same step: sign in. The monitor signs in through a single sign on page before it reaches the application. That is where the failing runs stopped.&lt;/p&gt;

&lt;p&gt;Second, a run could fail and the next one could pass with nothing changed in between.&lt;/p&gt;

&lt;p&gt;Same step. Random outcome. Passes on a rerun. When I see that combination, I treat it as a timing problem until something proves otherwise. The script and the page are not waiting for each other properly.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/p_bhatnagar/the-health-check-that-was-red-and-green-at-the-same-time-552g"&gt;The Health Check That Was Red and Green at the Same Time&lt;/a&gt;, I wrote about reading failure duration first. A very fast failure points at the environment: network, DNS, routing. A failure that runs its course and then fails a check points at the script side: a selector, an assertion, or sign in. This one belonged on the script side. The application was fine. The script's timing was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The race
&lt;/h2&gt;

&lt;p&gt;After you enter credentials, the identity provider does some work before it hands you back to the application. Along the way it can show an optional prompt asking if you want to stay signed in. How long all of this takes is not fixed. It changes from run to run.&lt;/p&gt;

&lt;p&gt;The script did not account for that. It used a fixed wait after sign in, then moved on as if the next page would be there.&lt;/p&gt;

&lt;p&gt;When the sign in flow settled inside that wait, the run passed. When it settled later, the script acted on a page that was not ready yet, and the step failed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F99sibo55z4rq5wdfopdn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F99sibo55z4rq5wdfopdn.png" alt="Two timelines. In run A the page is ready before the script checks, and the step passes. In run B the page is ready after the script checks, and the step fails." width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This changes how you read the 25 green runs. They were not proof that the script worked. They were runs where the timing happened to line up. &lt;strong&gt;The greens were luck, not health.&lt;/strong&gt; A monitor like that is not telling you anything about the application. It is telling you about a race it sometimes wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the obvious fixes are wrong
&lt;/h2&gt;

&lt;p&gt;There are two quick ways to make this monitor go green. Both are traps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the wait longer.&lt;/strong&gt; This makes every run slower, even the ones that were ready early. And it is still a guess. The day the identity provider has a slow afternoon, the race comes back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add retries.&lt;/strong&gt; This hides the flap from the dashboard. It also hides a real sign in outage. If sign in is genuinely broken for a while, a retrying monitor turns a clear red into a slow, confusing mix of passes and failures. You find out later than you should.&lt;/p&gt;

&lt;p&gt;Both fixes change what the dashboard shows. Neither changes what the script actually knows. That is the same rule I keep coming back to: never loosen a check just to make a dashboard green.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: poll and proceed
&lt;/h2&gt;

&lt;p&gt;The real fix is to stop guessing a duration and wait for a condition instead.&lt;/p&gt;

&lt;p&gt;Instead of "wait N seconds, then continue," the script asks a question again and again, for a limited time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the landing page there? Then sign in is done. Move on.&lt;/li&gt;
&lt;li&gt;Is the stay signed in prompt there? Then answer it, and keep checking.&lt;/li&gt;
&lt;li&gt;Neither yet? Wait briefly and check again.&lt;/li&gt;
&lt;li&gt;Past the time limit? Fail, and say exactly why.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqynnybypatmjvgdz3peg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqynnybypatmjvgdz3peg.png" alt="Flowchart: after submitting credentials, check for the landing page. If yes, proceed. If not, check for the prompt and answer it. If past the time limit, fail with a reason. Otherwise wait briefly and poll again." width="799" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the shape of it in simplified pseudocode. This is not the monitoring tool's syntax, and it is not the real script. It is just the logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before: wait a fixed time and hope.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;submit_credentials()
sleep(FIXED_WAIT)          # a guess about the identity provider
continue_to_application()  # assumes the page is ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After: poll for a condition, with a limit.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;submit_credentials()

deadline = now() + TIME_LIMIT
loop:
    if landing_page_visible():
        break                          # signed in, move on
    if stay_signed_in_prompt_visible():
        answer_prompt()                # optional: handle it only if it shows
    if now() &amp;gt; deadline:
        fail("sign in did not complete within TIME_LIMIT")
    sleep(SHORT_POLL)

continue_to_application()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things make this version better.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It waits for the page, not the clock.&lt;/strong&gt; Fast runs move on as soon as they are ready. Slow runs get the time they need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It handles the optional prompt either way.&lt;/strong&gt; The prompt can show or not show. The script no longer cares which.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is still bounded.&lt;/strong&gt; If sign in never completes, the step fails with a clear reason. A real sign in outage still shows up as red. That is the part retries break.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the numbers support, and what they do not
&lt;/h2&gt;

&lt;p&gt;This is the section I care about most, because it is where it is easiest to overclaim.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwf6i3fhnbm44f3mu4nw0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwf6i3fhnbm44f3mu4nw0.png" alt="Four cards. Before the fix: 25 of 67. First runs after: 2 of 2, too few to prove anything. Non production after the fix: 256 of 333, remaining failures were environmental. Production: 100% of the current sample, P90 about 58 seconds." width="799" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first two runs after the fix passed. Two runs prove nothing on their own, so I did not call it done there.&lt;/p&gt;

&lt;p&gt;The longer window in nonproduction after the fix was 256 passes out of 333 runs, or 76.9%. On its own, that looks like the fix only half worked. It did not mean that. This application was scoped for production monitoring, and the failures that remained in that window were environmental, not the sign in race. The monitor was later paused in nonproduction, because monitoring for that application lives in production.&lt;/p&gt;

&lt;p&gt;In production, the monitor is at 100% on the current sample, with a P90 of about 58 seconds.&lt;/p&gt;

&lt;p&gt;So the honest claim is: &lt;strong&gt;the flap is gone, and the application runs healthy in production.&lt;/strong&gt; I do not say "100% after the patch." The full window does not support that sentence, and a number you cannot defend is worse than a smaller number you can.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second sign in failure, with a different shape
&lt;/h2&gt;

&lt;p&gt;In the same migration, another production application also failed at authentication. It looked similar at first. It was not.&lt;/p&gt;

&lt;p&gt;This one did not flap. It failed from our private runner, while the same check in the legacy system showed the application healthy at 100%. A consistent failure that is healthy at the source does not point at timing. It points at the environment between the runner and the application.&lt;/p&gt;

&lt;p&gt;I did not have access to the cluster that hosted that private location. So I designed a diagnostic and asked the engineer who owned it to run it: open a shell inside the runner pod and call the endpoint directly from there. That removes the monitoring script from the picture completely.&lt;/p&gt;

&lt;p&gt;The result was clear. The TLS connection succeeded. The response was HTTP 401 with a message that a bearer token was required. It was not a firewall block and not a script defect. The API gateway was enforcing OAuth on the runner's path. The blocker went to the gateway team with that evidence attached, and the test was left as it was.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fivgqvlcny6x76bbsvi57.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fivgqvlcny6x76bbsvi57.png" alt="Two cards comparing the failures. The sign in race: intermittent, same step, fix the script. The gateway 401: consistent, healthy at the source, prove it and escalate." width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both failures happened at sign in. What decided the next move was not the step they failed on. It was the shape of the failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Intermittent at the same step&lt;/strong&gt; means timing. Fix the script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent, but healthy at the source&lt;/strong&gt; means environment. Prove it, escalate with evidence, and do not touch the test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One small note on diagnosing through someone else's hands. When you cannot run a check yourself, make it as small as possible and decide in advance what each possible result would tell you. Then one run by another person is enough to settle the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Intermittent means timing until proven otherwise.&lt;/strong&gt; Same step, random outcome, passes on a rerun: that is the signature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A check that passes only when it wins a race is not a passing check.&lt;/strong&gt; Treat its green runs as luck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait for conditions, not durations.&lt;/strong&gt; And put a limit on every wait, so a real outage still fails loudly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not fix a flap with longer sleeps or retries.&lt;/strong&gt; Both hide the race instead of removing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the shape of a failure pick the next step.&lt;/strong&gt; Intermittent points at the script. Consistent and healthy at the source points at the environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claim what the numbers support.&lt;/strong&gt; "The flap is gone and it is healthy in production" is a smaller sentence than "100% after the patch." It is also the one that survives a follow up question.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is the fourth post in a series on the practice of running monitoring. The earlier ones cover &lt;a href="https://dev.to/p_bhatnagar/cutting-alert-noise-without-going-blind-a-practical-slo-driven-approach-1pkm"&gt;alert noise&lt;/a&gt;, &lt;a href="https://dev.to/p_bhatnagar/the-health-check-that-was-red-and-green-at-the-same-time-552g"&gt;a health check that was red and green at once&lt;/a&gt;, and &lt;a href="https://dev.to/p_bhatnagar/zero-blast-radius-shipping-monitoring-changes-without-causing-an-incident-3jli"&gt;shipping monitoring changes without causing an incident&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>observability</category>
      <category>sre</category>
      <category>testing</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Zero Blast Radius: Shipping Monitoring Changes Without Causing an Incident</title>
      <dc:creator>P Bhatnagar</dc:creator>
      <pubDate>Thu, 06 Aug 2026 05:15:57 +0000</pubDate>
      <link>https://dev.to/p_bhatnagar/zero-blast-radius-shipping-monitoring-changes-without-causing-an-incident-3jli</link>
      <guid>https://dev.to/p_bhatnagar/zero-blast-radius-shipping-monitoring-changes-without-causing-an-incident-3jli</guid>
      <description>&lt;p&gt;The first time I saw a monitoring change cause a problem, nothing was actually broken.&lt;/p&gt;

&lt;p&gt;We were building out dashboards and alerts for a set of production applications. Building an alert meant configuring its notification path, and the notification path pointed at the application team that owned the service: their distribution list, their paging rotation. The alerts were still under test. The thresholds had not been agreed with anyone. Nobody had signed off on receiving them, or on what they were supposed to do when one arrived.&lt;/p&gt;

&lt;p&gt;They received them anyway. People were pulled toward something that was not an incident, for an alert they had never agreed to own, at a threshold nobody had defended yet.&lt;/p&gt;

&lt;p&gt;Nothing was down. It still cost hours, and it cost something slower to earn back, which is that team's willingness to treat the next alert from us as real. The fix was small and it became a standing rule for the work that followed: recipients go in last, after signoff, not while you are still deciding whether the alert is any good.&lt;/p&gt;

&lt;p&gt;That rule is the narrow version of a wider one. Monitoring changes get treated as low risk, because they are not production code and they do not touch customer traffic. A detector with a badly chosen threshold pages an on-call engineer at 3 AM for something nobody can act on. A monitor pointed at the wrong endpoint reports a healthy service as down, and someone starts a bridge call. A bulk write against a monitoring API overwrites an alert routing rule that took two teams a week to agree on. None of that is an application outage, and every one of them burns trust in the alerting layer, which is the thing you were trying to improve.&lt;/p&gt;

&lt;p&gt;I later spent a long stretch moving a synthetic monitoring estate from a legacy platform to a new one, then building detectors on top of it across production applications. Most of that work was done through APIs, in a live environment, with credentials scoped to production. So I settled on a routine built around one rule. A monitoring change should never be the thing that causes an outage. Its job is to report one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Create paused, with no recipients.&lt;/strong&gt; The object exists, it cannot fire, and nobody is wired to it.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Verify by hand.&lt;/strong&gt; Run the check, read what it actually asserts on.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Backtest.&lt;/strong&gt; Run the condition against history before it runs against now.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Approve activation separately.&lt;/strong&gt; Not the same change request that created it.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Enable with the rollback ready.&lt;/strong&gt; One object at a time.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Reconcile.&lt;/strong&gt; Nothing gets left paused by accident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On paper the sequence is straightforward. The return paths are where most of the effort goes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftly8wzc6bih4fb5m6muh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftly8wzc6bih4fb5m6muh.png" alt="Five gates for shipping a monitoring change: create in the off position, verify before anyone sees it, take a separate approval to activate, enable one object at a time, then reconcile the window. Three return paths: a failed backtest sends you back to redesign, a wrong firing triggers the one line rollback, and an unexplained paused object at reconciliation gets enabled or deleted. Every write at every gate is preceded by a GET, a timestamped export, and an environment scoped token." width="800" height="1126"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Create in the off position
&lt;/h2&gt;

&lt;p&gt;Every monitor is created paused. Every detector is created disabled. And the recipient list is the last field you populate, not the first. Creation and activation are two different decisions, and combining them removes your only chance to inspect the thing before it can act. When a detector is created live, the first evidence you get about whether its threshold is sane is a page, and it is somebody else's page.&lt;/p&gt;

&lt;p&gt;Most platforms support pausing directly. Where they do not, an alert created with no recipients at all is the fallback. That is weaker than a real disabled state, because a half-configured object in production is its own hazard, but it beats going live blind. The important part is not the API flag. It is that you have separated "the thing exists" from "the thing can wake someone up."&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify before anyone sees it
&lt;/h2&gt;

&lt;p&gt;Once the object exists in the off position, run it manually and read the result properly.&lt;/p&gt;

&lt;p&gt;For a synthetic monitor that means executing the check against production and looking at the run in detail: did it complete, how long did it take, what did it actually assert on, and does the pass reflect the service being healthy or the assertion being too loose. A monitor that passes for the wrong reason is worse than one that fails honestly, because it will keep passing during the outage you built it for.&lt;/p&gt;

&lt;p&gt;For a detector or alert rule, it means running the condition against history before it is allowed to run against the present. Some platforms give you this directly: as you configure the rule, the preview chart applies your condition to the stored signal and shows the windows where it would have entered the alerting state. Widen that preview to two or three weeks and count them. Where the platform has no preview, you can do the same thing by hand, by taking the rule's own query, bucketing it to the rule's evaluation interval, applying the same trigger condition, and counting what survives. The bucket has to match the interval. A rule that evaluates every five minutes, checked against hourly buckets, produces a number that means nothing.&lt;/p&gt;

&lt;p&gt;Two things make the count readable. First, separate crossings from episodes: a condition that flips true, false, true across twenty minutes is one event and can produce eight notifications. Second, a count of zero is only informative if the window you checked contained an incident you can name. Over a quiet fortnight, zero tells you the fortnight was quiet.&lt;/p&gt;

&lt;p&gt;Here's a manual backtest for an alert that fires when the HTTP 500 rate exceeds 5% in any five-minute window, run over the last three weeks:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The exact syntax varies by platform. The example below uses Splunk SPL because it makes the backtesting approach explicit, but the same idea applies regardless of your monitoring stack.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;index=web_logs sourcetype=access_combined
| bin _time span=5m
| stats count(eval(status&amp;gt;=500)) as errors count as total by _time
| eval error_rate=if(total&amp;gt;0,round(errors*100/total,2),0)
| where error_rate&amp;gt;5
| stats count as breaching_windows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That counts &lt;strong&gt;breaching windows&lt;/strong&gt;. To estimate distinct alert episodes instead, treat breaching windows separated by less than fifteen minutes as part of the same event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;index=web_logs sourcetype=access_combined
| bin _time span=5m
| stats count(eval(status&amp;gt;=500)) as errors count as total by _time
| eval error_rate=if(total&amp;gt;0,round(errors*100/total,2),0)
| where error_rate&amp;gt;5
| sort 0 _time
| streamstats current=f last(_time) as prev_time
| eval episode_start=if(isnull(prev_time) OR (_time-prev_time)&amp;gt;900,1,0)
| stats sum(episode_start) as episodes count as breaching_windows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gap between those two numbers tells you which knob to turn. Sixty breaching windows across four episodes means the threshold is roughly right, but the rule probably needs a duration clause before it notifies. Sixty breaching windows across fifty episodes usually means the threshold itself is too sensitive.&lt;/p&gt;

&lt;p&gt;Either way, this is the highest-value step in the sequence because it is the only one that tests the rule against reality rather than against your intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Activation needs its own approval
&lt;/h2&gt;

&lt;p&gt;The change request that let you create the object should not be the change request that lets it fire.&lt;/p&gt;

&lt;p&gt;At first glance this looks like process for its own sake. In practice it does something specific: it forces the person approving activation to look at evidence that did not exist when the work was first proposed. At creation time all anyone can review is your intent. At activation time there is a real object, a real manual run, and a real backtest. The second conversation is short, because you are handing over evidence rather than a plan.&lt;/p&gt;

&lt;p&gt;It also gives you a clean place to stop. If the backtest looks wrong, nothing goes live and nothing needs reverting, because the object was never able to do anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable, one at a time
&lt;/h2&gt;

&lt;p&gt;Enable one object and stay with it. Not the batch.&lt;/p&gt;

&lt;p&gt;The first evaluation cycles after activation are where you find out whether the backtest matched reality, and you only get that signal if you are still watching. Enable twenty at once and the first firing arrives with no way to tell which change produced it, which is the position you spent the previous four steps avoiding.&lt;/p&gt;

&lt;p&gt;Treat that first firing as information about the threshold rather than as a result. A detector that fires within minutes of going live is more often telling you something about your condition than about the service. Check the underlying signal before anyone acts on it. If the threshold turns out to be wrong, nothing has reached a human yet and the rollback line is already written.&lt;/p&gt;

&lt;h2&gt;
  
  
  GET before you write, always
&lt;/h2&gt;

&lt;p&gt;Every write against a production monitoring API is preceded by a read of the current state of that same object, and the response is saved to a timestamped file.&lt;/p&gt;

&lt;p&gt;This costs one extra call. Monitoring APIs are frequently full-replacement rather than partial-update, so a PUT built from a partially populated payload will quietly drop fields you never intended to touch: notification rules, tags, team assignments, custom properties. You will not see this in the response, because the write succeeds. You see it a week later when an alert routes to the wrong team.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; backups
&lt;span class="nv"&gt;STAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y%m%dT%H%M%SZ&lt;span class="si"&gt;)&lt;/span&gt;

curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AUTH_HEADER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API&lt;/span&gt;&lt;span class="s2"&gt;/detector/&lt;/span&gt;&lt;span class="nv"&gt;$ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="s2"&gt;"backups/detector-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STAMP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'.id'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details in there matter. The object ID plus a UTC timestamp in the filename means that during a rollback you are not guessing which of four exports was the pre-change one. And the check on the end means an auth failure or an empty body cannot masquerade as a successful backup. A zero byte file in the backups directory is worse than no file, because you will trust it.&lt;/p&gt;

&lt;p&gt;For bulk operations, export everything in scope first, as a single operation, before the first write goes out. Do not export as you go. If the script fails halfway through, the objects you have not reached yet are the ones you most need originals for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the rollback first
&lt;/h2&gt;

&lt;p&gt;The rollback for any monitoring change should fit on a single command, and it goes into the change record before the change is applied, with the real filename in it rather than a placeholder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;abc123
&lt;span class="nv"&gt;BACKUP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;backups/detector-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;-20260805T181200Z&lt;/span&gt;.json

curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API&lt;/span&gt;&lt;span class="s2"&gt;/detector/&lt;/span&gt;&lt;span class="nv"&gt;$ID&lt;/span&gt;&lt;span class="s2"&gt;"&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;"&lt;/span&gt;&lt;span class="nv"&gt;$AUTH_HEADER&lt;/span&gt;&lt;span class="s2"&gt;"&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;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; @&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing it afterwards defeats the point. The moment you need it is the moment you are least able to compose it, so the discipline is in having decided in advance what the undo is, not in the complexity of the command. For most of this work the undo genuinely is one line: disable the detector, pause the monitor, or PUT back the saved export.&lt;/p&gt;

&lt;p&gt;If you cannot express the rollback in one line, that is useful information about the change. It usually means you are changing several objects at once and should be splitting the batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope the credentials
&lt;/h2&gt;

&lt;p&gt;Use a token scoped to the specific environment and, where the platform allows it, to the specific capability. An org-wide admin token used for a routine dashboard update is a blast radius you are carrying around for no benefit.&lt;/p&gt;

&lt;p&gt;This one is easy to skip because the wide token already exists and already works. The argument for the narrow one is not really about malice. It is about the script that has a bad loop bound, or the copy-pasted object ID from the wrong environment. A narrowly scoped token turns those into an error message instead of an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode this creates
&lt;/h2&gt;

&lt;p&gt;Every safety pattern introduces its own way of going wrong, and being honest about that is more useful than presenting the pattern as free.&lt;/p&gt;

&lt;p&gt;Creating things in the off position means you can forget to turn them on. A monitor built during a migration, verified, approved, and then left paused because the activation window slipped, is invisible coverage debt. Nobody notices, because a paused monitor never complains. You find out during the incident it would have caught.&lt;/p&gt;

&lt;p&gt;The fix is a reconciliation step, not a better memory. At the end of every change window, list every object in the paused or disabled state and account for each one: intentionally off, or dropped. Anything older than the window that nobody can explain gets either enabled or deleted. Most monitoring APIs will give you that list in one call, so this is a five minute task that closes the only real hole in the pattern.&lt;/p&gt;

&lt;p&gt;I would rather carry that failure mode than the alternative one. Forgetting to enable a monitor is a gap you can find with a query. Enabling a bad detector is a gap that finds you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually costs
&lt;/h2&gt;

&lt;p&gt;Yes, it is slower. Two approvals instead of one, an export step before every write, and a backtest that occasionally sends you back to redesign a threshold you thought was finished.&lt;/p&gt;

&lt;p&gt;What you get in exchange is that the monitoring layer stops being a source of incidents. Across the migration and the detector rollout that followed, just over forty monitor definitions and twenty-six detectors went live across sixteen production applications. Nothing I enabled had to be rolled back for firing wrongly, and no change I made took out an existing alert path. That is not because the thresholds were inspired. It is because every one of them was inspected while it was still incapable of paging anyone.&lt;/p&gt;

&lt;p&gt;Monitoring exists to reduce operational risk. When deploying it adds risk instead, the thing that needs fixing is the deployment process rather than the monitoring. The same pattern applies well beyond monitoring, and most infrastructure work has some version of the off position available. It is worth finding out what yours is before you need it.&lt;/p&gt;

&lt;p&gt;If you run a variant of this, particularly the reconciliation step, I would like to hear how you handle it.&lt;/p&gt;

</description>
      <category>observability</category>
      <category>sre</category>
      <category>monitoring</category>
      <category>devops</category>
    </item>
    <item>
      <title>The Health Check That Was Red and Green at the Same Time</title>
      <dc:creator>P Bhatnagar</dc:creator>
      <pubDate>Mon, 27 Jul 2026 15:05:31 +0000</pubDate>
      <link>https://dev.to/p_bhatnagar/the-health-check-that-was-red-and-green-at-the-same-time-552g</link>
      <guid>https://dev.to/p_bhatnagar/the-health-check-that-was-red-and-green-at-the-same-time-552g</guid>
      <description>&lt;p&gt;A business-critical health check was showing red. Red on the dashboard, red in the alert history, red enough that someone was about to get paged. But when I opened the failing runs, the service underneath was returning HTTP/2 200. The check was failing, and the thing it was checking was fine. Red and green at the same time.&lt;/p&gt;

&lt;p&gt;That contradiction turned into one of the more useful debugging lessons I've had in monitoring, and the thing that cracked it wasn't a better tool or a deeper log dive. It was a single habit: read the failure duration before anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the duration first
&lt;/h2&gt;

&lt;p&gt;When a synthetic monitor fails, the first number I look at isn't the error message. It's how long the check ran before it failed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast fail&lt;/strong&gt; (under roughly 150 milliseconds, with no response body read): the check died before it ever got a real answer from the application. That points at the environment: network, DNS, routing, egress, a blocked runner. The application was never actually reached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full duration, then an assertion failure&lt;/strong&gt;: the check ran the whole transaction, got a response, and &lt;em&gt;then&lt;/em&gt; decided that response was wrong. That points at the script: a selector, an SSO step, or the assertion logic itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This one split does most of your triage before you read a single log line. The duration tells you which half of the world to look in: the environment the test runs in, or the test itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3zho284olaky56j0s7dr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3zho284olaky56j0s7dr.png" alt="Synthetic failure triage: read the failure duration first. A fast fail points at the environment and means you should check reachability and leave the test alone; a full-duration failure points at the script and means you fix the test." width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Back to the red-and-green health check. It was failing in about 24 milliseconds. No real round trip completes in 24 milliseconds, and the response headers showed HTTP/2 200 underneath. Fast fail plus a healthy status: by the heuristic, this wasn't the application failing. It was the assertion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The assertion that lied
&lt;/h2&gt;

&lt;p&gt;The check had exactly one assertion: pass only if the status code equals 200.&lt;/p&gt;

&lt;p&gt;That looks sensible until you notice how much it depends on. The status code has to survive the whole path from the service, through whatever proxies and gateways sit in between, to the bit of code inside the check that reads it and compares it. Anywhere along that path it can arrive in a form the check doesn't recognise, and when that happens the check reports a failure even though the service answered correctly. That's what was happening here. The response headers said HTTP/2 200. The assertion still called it red.&lt;/p&gt;

&lt;p&gt;A status-code-only assertion is weak in both directions, and the direction nobody notices is the dangerous one. The false red is loud. Someone gets paged, someone investigates, someone eventually finds out the application was healthy the whole time. The false green is silent. Gateways, CDNs, and friendly error pages will happily hand you an HTTP 200 wrapped around an empty body or an error message, and a check that only reads the status code will call that a pass. Your dashboard stays green through a real outage.&lt;/p&gt;

&lt;p&gt;The fix was to make the assertion ask a question worth asking. Instead of checking the status code, check that the response body actually contains what a healthy response contains, a specific content marker that only a genuinely working service returns, and pair that with a duration sanity window. The duration window catches both ends: a hang, and a suspiciously instant response that couldn't possibly have done any real work. Status code stays in as a supporting signal, never as the sole criterion.&lt;/p&gt;

&lt;p&gt;The rewritten check went green immediately and stayed there. Three hundred consecutive passes, median around 23 milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tooling copied the mistake faithfully
&lt;/h2&gt;

&lt;p&gt;That bad assertion didn't come from nowhere. It came from an old test template, and the conversion tooling that migrated the legacy scripts had reproduced it exactly, everywhere the template had been used. This is worth sitting with if you're using AI-assisted conversion for any kind of migration. The tooling did precisely what it was asked to do. It was faithful. It just had no opinion about whether the thing it was faithfully reproducing was any good, so a single bad pattern from years ago got copied forward at machine speed into a brand new platform.&lt;/p&gt;

&lt;p&gt;Assertions are the part of a migrated test that most needs a human to read it, because an assertion is the only place where the test states what "working" means. Everything else is mechanics.&lt;/p&gt;

&lt;p&gt;But going green was never the point. The point was making sure it couldn't come back. I generalized the fix into a platform rule, no status-code-only assertions anywhere, and replicated the corrected pattern to the sibling monitors that had inherited the same template. The goal was to make the mistake structurally unrepeatable, not just fixed in one spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The discipline that matters most
&lt;/h2&gt;

&lt;p&gt;There's a version of this job where every red monitor is an annoyance to be silenced, and the fastest way to silence one is to loosen its assertion until it passes. Resist that completely.&lt;/p&gt;

&lt;p&gt;Not long after, two monitors started failing together while their sibling checks in other environments passed the same set of test cases. The pattern was suspicious in a specific way, same environment, same data lookups, and it would have been trivial to relax the assertions and make the red go away. I didn't, because the evidence pointed at a genuine data-layer problem in that environment. Loosening the assertion wouldn't have fixed anything. It would have hidden a real "data not found" behind a green light. The right move was to keep the assertion honest and escalate the underlying cause.&lt;/p&gt;

&lt;p&gt;That's the rule I'd hand a new monitoring engineer on day one: if a check fails, diagnose the root cause, and never adjust the assertion just to make the test pass. The moment you loosen an assertion to force green, you've converted your monitor from something that tells you the truth into something that tells you what you want to hear.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it all comes down to
&lt;/h2&gt;

&lt;p&gt;A synthetic monitor has exactly one job: tell you the truth about what a real user would experience. Everything here is in service of that one job.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read the failure duration first.&lt;/strong&gt; It tells you whether to suspect the environment or the test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert on meaning, not on a status code.&lt;/strong&gt; Verify the response actually contains what a healthy response contains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never loosen an assertion to force a green.&lt;/strong&gt; Diagnose the cause instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review anything your automation generates.&lt;/strong&gt; It reproduces your bad assumptions as faithfully as your good ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The monitor that was red and green at the same time wasn't broken hardware or a flaky network. It was a monitor asking the wrong question. Once it asked the right one, it started telling the truth, and a monitor that tells the truth is the only kind worth having.&lt;/p&gt;

</description>
      <category>observability</category>
      <category>sre</category>
      <category>monitoring</category>
      <category>testing</category>
    </item>
    <item>
      <title>Cutting Alert Noise Without Going Blind: A Practical, SLO-Driven Approach</title>
      <dc:creator>P Bhatnagar</dc:creator>
      <pubDate>Wed, 22 Jul 2026 14:46:21 +0000</pubDate>
      <link>https://dev.to/p_bhatnagar/cutting-alert-noise-without-going-blind-a-practical-slo-driven-approach-1pkm</link>
      <guid>https://dev.to/p_bhatnagar/cutting-alert-noise-without-going-blind-a-practical-slo-driven-approach-1pkm</guid>
      <description>&lt;p&gt;There's a specific kind of failure that keeps me up at night, and it isn't the outage. It's the outage that sat unseen because the alert announcing it was buried under two hundred others that didn't matter.&lt;/p&gt;

&lt;p&gt;Most teams treat alert fatigue as an annoyance: a noisy channel, a phone that buzzes too much. It isn't an annoyance. It's a reliability risk. Every alert that fires without needing a human response is a small withdrawal from the same account: the team's trust that a page means something. Drain that account far enough and people start muting, skimming, and reflexively acknowledging without looking. At that point your monitoring is still running, still green on the dashboard, and functionally blind. The danger was never the noise itself. It's what the noise trains people to ignore.&lt;/p&gt;

&lt;p&gt;After years of keeping production systems observable, I've come to think the goal of good alerting isn't fewer alerts for their own sake. It's a team that trusts every page, because every page is real. Here's how I get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why noise accumulates
&lt;/h2&gt;

&lt;p&gt;Noise is rarely one bad decision. It's the compound interest of small, reasonable-at-the-time choices that nobody revisited. A few patterns show up almost everywhere:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static thresholds that outlived their context.&lt;/strong&gt; Someone set "alert if latency &amp;gt; 800ms" during a launch two years ago. Traffic patterns changed, the baseline moved, and the threshold quietly became either a constant false alarm or a rubber stamp that never fires. Thresholds decay. Most teams never schedule the review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alerting on causes instead of symptoms.&lt;/strong&gt; CPU at 90% pages someone. But 90% CPU is not a problem. It's a machine doing work. The problem is a user getting an error or waiting too long. When you alert on every internal cause, you generate a page for every twitch in the system, most of which resolve themselves before anyone reads the notification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No severity tiering, so everything pages.&lt;/strong&gt; When the informational, the interesting, and the genuinely urgent all arrive through the same channel with the same urgency, the person on call has to sort them out in their own head, doing the triage the alerting system should have done for them. That cognitive tax is where fatigue actually lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One incident wearing three costumes.&lt;/strong&gt; A single upstream failure lights up the load balancer, the application layer, and the database monitor as three independent alerts. The responder now spends their first five minutes proving to themselves that it's one incident, not three, during the exact window when minutes matter most.&lt;/p&gt;

&lt;p&gt;None of these come from carelessness. They come from adding monitoring faster than anyone prunes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe: alert on symptoms, not on every metric
&lt;/h2&gt;

&lt;p&gt;The shift that changes everything is deceptively small. Stop asking &lt;em&gt;"is this metric abnormal?"&lt;/em&gt; and start asking &lt;em&gt;"is a user being hurt right now, and can a human do something about it?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That second question is a much higher bar, and it's the right one. It moves your alerting off the internals of the system and onto the experience of the people using it. An abnormal metric is a curiosity. A hurt user with an actionable cause is a page.&lt;/p&gt;

&lt;p&gt;This is where Service Level Objectives earn their keep. When you define what "working" means from the user's side (a success rate, a latency ceiling, an availability target), you get a principled way to decide what deserves human attention. You're no longer alerting on raw metrics; you're alerting on whether you're spending your error budget faster than you can afford to. A slow burn against your budget is a ticket to look at tomorrow. A fast burn that will exhaust the month's budget in an hour is a page tonight. Same underlying data, completely different urgency, and the difference is decided by user impact, not by a number crossing a line.&lt;/p&gt;

&lt;h3&gt;
  
  
  A practical example
&lt;/h3&gt;

&lt;p&gt;Consider a checkout service. A traditional alert might look like this:&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;Alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CheckoutHighErrorRate&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;http_requests_total{status=~"5.."} &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;
&lt;span class="na"&gt;Duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is that raw error counts say nothing about customer impact. One customer stuck in a retry loop can generate hundreds of errors. Fifty different customers each failing once is a much bigger business problem, and this alert can't tell the two apart.&lt;/p&gt;

&lt;p&gt;A better alert ties together user impact and SLO burn:&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;Alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CheckoutSLOBurn&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;burn_rate &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="s"&gt;AND affected_customers &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;
  &lt;span class="s"&gt;AND duration &amp;gt; 10m&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same system. Very different signal. The first alert asks, "did errors increase?" The second asks, "are enough real users being hurt that someone should wake up?"&lt;/p&gt;

&lt;h3&gt;
  
  
  From metrics to action
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpsd0yxyzews0wqy19k4x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpsd0yxyzews0wqy19k4x.png" alt="From metrics to action: raw metrics are refined through correlation, user impact, and SLO burn rate before anything pages a human." width="800" height="120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Raw signals aren't the thing you page on. They're the thing you refine, through correlation and user-impact and burn rate, until what's left is worth a human's sleep.&lt;/p&gt;

&lt;h2&gt;
  
  
  A framework you can apply Monday
&lt;/h2&gt;

&lt;p&gt;Principles are easy to nod at. Here's the operational version: four moves you can start on this week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The actionability test.&lt;/strong&gt; For every alert that pages a human, answer one question: &lt;em&gt;what specific action does the person take when they receive it?&lt;/em&gt; If you can name the action (restart this, fail over to that, roll back the deploy), it's a legitimate page. If the honest answer is "check whether it's still happening," it isn't a page. It's a dashboard panel or, at most, a ticket. This single test retires more noise than any other. Run it against your current paging alerts and you'll be surprised how many fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Tie severity to impact, not to the metric.&lt;/strong&gt; Build three lanes and route deliberately. &lt;strong&gt;Page&lt;/strong&gt; for user-facing harm with an actionable cause and meaningful error-budget burn. &lt;strong&gt;Ticket&lt;/strong&gt; for things that need attention but not tonight: degradations, slow trends, capacity you'll hit next quarter. &lt;strong&gt;Silent dashboard&lt;/strong&gt; for the informational signal you want visible but never want to be woken by. The discipline is refusing to let a lane creep upward. Most alert bloat is ticket-worthy signal that somehow got wired to the pager.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Deduplicate and group.&lt;/strong&gt; Collapse correlated alerts before they reach a person. If a dependency is down, suppress the downstream alerts that are merely reporting the same root failure. Dependency-aware suppression turns three costumes back into one incident. Respect maintenance windows so planned work doesn't page anyone. The responder should receive &lt;em&gt;the incident&lt;/em&gt;, not the incident's echoes across every layer it touched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Enrich with context.&lt;/strong&gt; An alert that says "error rate high" makes the responder start from zero. An alert that arrives with the runbook link, the last few deploys, and a clear statement of &lt;em&gt;what&lt;/em&gt; is failing and &lt;em&gt;why we think so&lt;/em&gt; lets them start from the middle. Attach the filtering and correlation logic that already knows which paths are affected and which are noise. The responder's first job should be deciding what to do, not reconstructing what happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  A quick decision tree
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9prramy0emlpys1sknu0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9prramy0emlpys1sknu0.png" alt="Alert decision tree: an alert routes to a dashboard, a ticket, business-hours investigation, or a page depending on user impact, actionability, and whether it is burning the error budget." width="800" height="615"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If every alert had to walk this path before reaching a phone, most of them wouldn't make it, and that's the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowing it's working
&lt;/h2&gt;

&lt;p&gt;You can't manage what you don't measure, and "the channel feels quieter" is not a metric. Track the ones that are:&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;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Alert volume&lt;/td&gt;
&lt;td&gt;Measures overall noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Actionable pages&lt;/td&gt;
&lt;td&gt;Indicates trust in the system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-resolved alerts&lt;/td&gt;
&lt;td&gt;Shows suppression opportunities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False positives&lt;/td&gt;
&lt;td&gt;Direct measure of alert quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MTTR&lt;/td&gt;
&lt;td&gt;Measures operational improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If I had to pick one, it's this: &lt;strong&gt;measure the reduction in pages that led to no action.&lt;/strong&gt; That number is the purest expression of the whole effort. Driving it down means you're returning human attention to the system, one retired alert at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  One caution before you automate
&lt;/h2&gt;

&lt;p&gt;Everything above can be automated: generated thresholds, auto-suppression rules, ML-driven alerting. You should automate it. But keep one thing in mind, because it's the trap I see teams fall into most. &lt;strong&gt;Automation amplifies whatever judgment you feed it.&lt;/strong&gt; An auto-suppression rule will silence a noisy alert, and it will just as faithfully silence a real failure that happens to match the same pattern. A generated threshold propagates your best thinking and your worst assumptions with equal enthusiasm, and the bad ones scale faster because nobody's watching. The answer isn't to stop automating. It's to automate the execution and keep a human review gate on the judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you do nothing else, do these three things this week:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the &lt;strong&gt;actionability test&lt;/strong&gt; against your current paging alerts. Demote every one that has no concrete human action attached.&lt;/li&gt;
&lt;li&gt;Pick your single noisiest alert and either &lt;strong&gt;retire it, re-tier it, or enrich it&lt;/strong&gt; so it's worth reading.&lt;/li&gt;
&lt;li&gt;Start tracking &lt;strong&gt;pages that led to no action.&lt;/strong&gt; That one number tells you whether any of this is working.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Every mature monitoring platform eventually collects thousands of alerts. The best teams aren't the ones that monitor the most. They're the ones that continually remove everything that no longer deserves a human's attention.&lt;/p&gt;

&lt;p&gt;That's the real job: not building alerts, but building trust. Because at three in the morning, nobody is reading every notification. They're betting that the one which woke them up is worth believing.&lt;/p&gt;

</description>
      <category>observability</category>
      <category>sre</category>
      <category>monitoring</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
