<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
  <title>Hesklo blog</title>
  <link>https://www.hesklo.com/blog</link>
  <description>Practical writing on uptime monitoring, alerting and on-call.</description>
  <language>en</language>
  <atom:link href="https://www.hesklo.com/blog/feed.xml" rel="self" type="application/rss+xml"/>
  <item>
    <title>Alert fatigue is a design problem: a pruning checklist</title>
    <link>https://www.hesklo.com/blog/alert-fatigue-pruning-checklist</link>
    <guid isPermaLink="true">https://www.hesklo.com/blog/alert-fatigue-pruning-checklist</guid>
    <pubDate>Fri, 16 Oct 2026 00:00:00 +0000</pubDate>
    <description>Teams do not ignore alerts because they are careless. They ignore them because the alerts are badly designed. A way to audit a month of pages and cut the noise.</description>
    <content:encoded><![CDATA[<p>When a team starts muting a channel or swiping away pages without reading them, the usual diagnosis is discipline. It is almost never discipline. Every alert that does not need a human trains the humans to expect that the next one does not either, and eventually they are right about the wrong one.</p>
<p>Fatigue is a property of the alerting design, so it can be fixed by changing the design. This post is a method for doing that with data you already have: last month's pages.</p>
<h2>Start with a rule for what may page a person</h2>
<p>Before touching any monitor, write down the test an alert must pass to interrupt someone. A workable one has three parts:</p>
<ol>
<li><strong>Something is wrong now,</strong> or will be soon enough that waiting costs something. Not "something was odd a minute ago".</li>
<li><strong>A human can do something about it.</strong> If the right response is "wait and see", a person is the wrong recipient.</li>
<li><strong>It cannot wait until morning,</strong> or the person receiving it is already working.</li>
</ol>
<p>Anything that fails the test is not necessarily useless. It is just not a page. It may belong in a chat channel, a daily summary, or a log. The audit below is mostly the work of sorting alerts into those buckets honestly.</p>
<h2>The audit: label every page from the last month</h2>
<p>Export or list every alert that reached a person in the last 30 days. For each one, record four things: which monitor, what time, what the responder did, and how long it took. Then give each a label.</p>
<ul>
<li><strong>Actionable.</strong> A real problem, and someone did something that changed the outcome.</li>
<li><strong>Real but unactionable.</strong> The problem was real, but nobody could or needed to act: a provider outage you could only watch, or a blip that fixed itself before anyone opened a laptop.</li>
<li><strong>Planned.</strong> A deploy, reboot or migration that you caused on purpose.</li>
<li><strong>False.</strong> The service was fine. The check, the network on the sending side, or a threshold was wrong.</li>
<li><strong>Duplicate.</strong> The same incident delivered again, or to several people, or through several channels.</li>
</ul>
<p>Be strict. "It was real, but it recovered" goes under unactionable, not actionable. The useful number is the share of pages that were actionable. If it is under half, the team's reaction to pages is a reasonable response to the data.</p>
<h2>A fix for each label</h2>
<h3>False pages: raise the bar for "down"</h3>
<p>These are the cheapest to remove, because the alert should never have existed. Require several failed checks in a row before a monitor counts as down, and set the timeout from the endpoint's real slow tail instead of a default. The arithmetic behind that is in <a href="/blog/fail-three-times-before-paging">why a check should fail three times before it pages</a>. If failures cluster across many unrelated monitors at the same moment, suspect the sender, not the targets; a <a href="/blog/second-vantage-point-monitoring">second vantage point</a> is the cure there.</p>
<h3>Planned pages: tell the system about the work</h3>
<p>If a deploy pages someone, the deploy process is missing a step. Pause the affected monitors for the length of the work, with an automatic resume time so a forgotten pause does not become a blind spot for weeks. Pause only what the work touches. And do not use a pause as a cure for a monitor that is merely noisy; that hides the problem and the visibility with it. <a href="/features/maintenance-windows">Maintenance windows</a> cover the mechanics.</p>
<h3>Unactionable pages: change the recipient or the channel</h3>
<p>A real problem that nobody can fix still deserves to be recorded, but it does not deserve a 3 a.m. phone call. Move these to a quiet channel, or hold them to working hours. A branch of the policy for "important but not urgent" next to a branch for "wake someone" is usually enough. A <a href="/features/schedule-gates">schedule gate</a> is one way to hold the first branch to the working day while the critical one stays open around the clock.</p>
<h3>Duplicates: one incident, one message per destination</h3>
<p>Count how many messages a single outage generated. If an incident that lasted ten minutes produced thirty notifications, the design is repeating itself. Make each step fire once per incident by default. Add a cooldown to services that flap, so each transition does not produce its own message. Repeats are fine when they are deliberate, for example a page that keeps going until somebody acknowledges it. That is noise you chose, and it ends when a person responds. The distinction between the two is covered on the <a href="/features/alert-noise-control">alert noise control</a> page.</p>
<h2>Check the routing, not just the thresholds</h2>
<p>Some of the worst fatigue comes from alerts that are correct but sent to everyone. A page that goes to the whole team teaches nine people to assume that someone else has it. Send the first alert to one person, and widen only if they do not respond. That is what <a href="/blog/escalation-policy-without-waking-everyone">an escalation policy that does not wake the whole team</a> is for. Pair it with a rota so that "one person" is a named, current answer; see <a href="/features/on-call-scheduling">on-call scheduling</a>.</p>
<h2>Where Hesklo fits</h2>
<p>In Hesklo the confirmation threshold and timeout are set per monitor, notify steps carry a cooldown and fire once per incident unless you ask otherwise, and pausing a monitor can resume itself at a time you set. The point of the audit is that you can apply every fix above with any tool that has these controls.</p>
<h2>The checklist</h2>
<ul>
<li>Write the page test down: wrong now, a human can act, cannot wait.</li>
<li>Label last month's pages as actionable, unactionable, planned, false or duplicate.</li>
<li>Compute the actionable share. Treat anything under half as a design defect.</li>
<li>False: raise the failure threshold, fix the timeout, look for a shared cause on the sending side.</li>
<li>Planned: pause the monitors for the work, with an auto-resume time.</li>
<li>Unactionable: move to a quiet channel or hold to working hours.</li>
<li>Duplicate: fire once per incident, add a cooldown for flapping services.</li>
<li>Route the first page to one person, not the whole team.</li>
<li>Repeat the audit in a month and compare the actionable share. If it did not improve, the changes were cosmetic.</li>
</ul>
<p>Do not aim for zero alerts. Aim for a page that always means something, so the day it arrives at 3 a.m. it gets read.</p>
]]></content:encoded>
  </item>
  <item>
    <title>A status page customers believe: what to publish and what to skip</title>
    <link>https://www.hesklo.com/blog/status-page-customers-believe</link>
    <guid isPermaLink="true">https://www.hesklo.com/blog/status-page-customers-believe</guid>
    <pubDate>Tue, 13 Oct 2026 00:00:00 +0000</pubDate>
    <description>A status page earns trust by being accurate, specific and boring. What to publish during an incident, what to leave out, and a template for updates.</description>
    <content:encoded><![CDATA[<p>Customers stop reading a status page the first time it says "All systems operational" while they are staring at an error. After that it is decoration, and they go back to opening tickets. Everything below is about avoiding that first lie.</p>

<h2>The page has one job</h2>
<p>A status page answers one question: "is it me or is it you?" It does not need to explain your architecture, apologise at length or show off your uptime. If a visitor can answer that question in five seconds, the page worked, and most of the tickets that would have followed never get written.</p>
<p>Two properties make that answer believable: it is <strong>current</strong> and it is <strong>specific</strong>. Most advice about wording is really advice about those two.</p>

<h2>Let the page follow the checks, not the people</h2>
<p>A page that someone has to remember to update is wrong exactly when it matters. During an incident the people who could update it are busy fixing the problem. The first update arrives late, the last one arrives after the fix has been quietly live for an hour, and in between the page says whatever it said before things broke.</p>
<p>The fix is to drive the component status from the same checks that page your team. Then "degraded" appears when your monitoring sees it, not when somebody gets a free minute. <a href="/features/status-pages">Hesklo status pages</a> work this way: the page reads live state from the same monitors that drive your alerts. Humans still write the explanation, but they do not have to be the source of truth for the colour.</p>
<p>That only holds if the underlying check is trustworthy. A monitor that flaps on single timeouts will paint your public page red for a minute at a time and teach customers to ignore it. The same <a href="/blog/fail-three-times-before-paging">confirmation logic that keeps pages quiet</a> keeps the status page honest.</p>

<h2>What to publish</h2>
<h3>Components customers recognise</h3>
<p>List things by what customers do, not by what you run. "Checkout", "Login", "API" and "Webhook delivery" are components. "payments-svc-eu-2" is not. Customers cannot judge whether an internal service name affects them, so they will assume the worst or ignore it.</p>
<p>Keep the list short. Five to ten components is plenty. Each one you add is another place the page can be wrong, and another row to read during an outage. Publishing is opt-in per monitor in Hesklo, so internal infrastructure stays private by default. That is the right default: expose a monitor only when a customer could say "that is the thing I use".</p>

<h3>The impact, in the customer's words</h3>
<p>Compare these two updates:</p>
<blockquote><p>We are investigating elevated error rates on a backend dependency.</p></blockquote>
<blockquote><p>Some customers cannot log in. Already signed-in sessions are unaffected. We are investigating.</p></blockquote>
<p>The second one lets a reader decide whether to wait or work around the problem. Say who is affected, what they see and what still works. If you do not know yet, say that too: "We do not yet know how many customers are affected" is more believable than a confident guess.</p>

<h3>A time and a next-update promise</h3>
<p>Put a timestamp on every update, with a timezone. End each one with when the next update will come, such as "Next update by 14:30 UTC", and then keep to it. If nothing has changed by then, post "no change, still investigating". Silence reads as abandonment, and a promised update that arrives is worth more than a vague "soon".</p>

<h3>Planned maintenance, ahead of time</h3>
<p>Announce planned work before it starts, with the window and what customers will notice. A monitor paused for a <a href="/features/maintenance-windows">maintenance window</a> should not look like an outage, and an announced window should not look like an unexplained gap.</p>

<h2>What to leave out</h2>
<ul>
  <li><strong>Guesses about root cause.</strong> "Looks like a database problem" becomes wrong half an hour later, and you will have to retract it in public. Describe symptoms until you are sure of the cause.</li>
  <li><strong>Blame.</strong> Naming a vendor, a deploy or a person in a live update helps nobody. Customers care about their impact, not your supply chain.</li>
  <li><strong>Internal jargon and hostnames.</strong> If a customer cannot act on a word, cut it.</li>
  <li><strong>Security details.</strong> Do not describe the exact mechanism of an attack or a vulnerability while it is open. Say what customers should do, and publish a fuller write-up later if appropriate.</li>
  <li><strong>Vanity numbers.</strong> A "99.99% uptime" badge next to a red component invites an argument you will lose.</li>
  <li><strong>Optimism.</strong> Do not write "should be resolved shortly" unless you can say what "shortly" is. An ETA you miss costs more trust than no ETA.</li>
</ul>

<h2>Use the stages, and make them boring</h2>
<p>A simple, fixed vocabulary is easier to trust than clever prose. A common sequence is:</p>
<ol>
  <li><strong>Investigating.</strong> We know something is wrong. We do not know why.</li>
  <li><strong>Identified.</strong> We know the cause and are working on a fix.</li>
  <li><strong>Monitoring.</strong> A fix is in place and we are watching it.</li>
  <li><strong>Resolved.</strong> Everything has been healthy for a stated period.</li>
</ol>
<p>Do not skip straight to "resolved" the moment a check goes green. Services often recover, relapse and recover again, especially after a restart or a cache flush. Waiting through a short "monitoring" stage costs you ten minutes of caution and saves you a second, embarrassing "we are investigating again".</p>

<h2>Be accurate about when it started</h2>
<p>Customers know when they first saw errors. If your page says the incident began at 14:10 and they were failing at 13:50, they conclude you either did not notice or are shaving the number. Where your detection was slower than the real start, say so: "Errors began at about 13:50 UTC; we were alerted at 14:10." It is an uncomfortable sentence and it is the one that makes the rest believable.</p>
<p>This is also a reason to keep monitoring honest about timing. The history of checks gives you a record of when the first failure was seen, which is better than reconstructing it from memory afterwards.</p>

<h2>An update template</h2>
<pre><code>[Stage] - [Component]: [what customers experience]

Impact: [who is affected, what still works]
Started: [time, timezone]
What we are doing: [one sentence, no speculation]
Next update: [time, timezone]
</code></pre>
<p>A filled-in example:</p>
<pre><code>Investigating - API: Requests are failing with 502 errors

Impact: Some API requests fail. The dashboard and webhooks are unaffected.
Started: 13:50 UTC
What we are doing: We are looking at recent changes and checking capacity.
Next update: 14:30 UTC
</code></pre>

<h2>Checklist</h2>
<ul>
  <li>Component colours come from monitors, not from someone's memory.</li>
  <li>Components are named after what customers do, and there are fewer than ten.</li>
  <li>Internal-only monitors are not published.</li>
  <li>Every update has a time, a stage, the impact and a next-update time.</li>
  <li>No root-cause guesses, no blame, no jargon.</li>
  <li>"Resolved" waits for a short monitoring period.</li>
  <li>The start time reflects when customers were first affected.</li>
  <li>Your page lives somewhere customers will find it, ideally on your own domain such as status.your-domain.com, and is linked from your app and support footer.</li>
</ul>
<p>None of this requires special tooling. A page that is accurate, specific and a little dull will outperform a polished one that is late. If you do want one wired to your monitors, see <a href="/features/status-pages">status pages</a> and <a href="/pricing">pricing</a> for which plans include them.</p>
]]></content:encoded>
  </item>
  <item>
    <title>One monitoring location is a guess: why you need a second</title>
    <link>https://www.hesklo.com/blog/second-vantage-point-monitoring</link>
    <guid isPermaLink="true">https://www.hesklo.com/blog/second-vantage-point-monitoring</guid>
    <pubDate>Fri, 09 Oct 2026 00:00:00 +0000</pubDate>
    <description>A check from a single place cannot tell your outage from its own network trouble. How a second vantage point settles it, and the rules for combining two opinions.</description>
    <content:encoded><![CDATA[<p>When a check fails, it has told you exactly one thing: this request, from this machine, did not get the answer it expected. That is not the same as "the service is down", and a monitor that treats the two as equal will page you for faults that belong to the sender.</p>
<h2>What a single location can and cannot see</h2>
<p>Between the monitor and your service sit the monitor's own host, its uplink, its resolver, a chain of transit networks, and finally your edge. A failure can come from any link. From one location you cannot tell which.</p>
<ul>
<li><strong>The sender's trouble.</strong> A dead uplink, a broken resolver or a saturated link on the monitoring host fails every check at once, for every target.</li>
<li><strong>A bad path.</strong> One transit provider drops traffic toward your network. The service is fine for most users; the monitor happens to sit behind the broken route.</li>
<li><strong>A regional problem.</strong> A CDN node, a geo rule or a regional ISP fault breaks the service for one part of the world and not another.</li>
<li><strong>A real outage.</strong> The service is down for everyone.</li>
</ul>
<p>Only the last one deserves a page at 03:00. The other three look identical from where the monitor stands: a timeout or a refused connection.</p>
<h2>Retries help, but they share the vantage point</h2>
<p>Repeating the check, as covered in our post on why an uptime check should fail three times before it pages, removes random noise. It does nothing for a systematic cause, because the retry travels the same path from the same host. If the route is broken for ten minutes, ten retries fail for ten minutes. A second location is the cheapest way to add information that a retry cannot.</p>
<h2>Reading two opinions</h2>
<p>Once you have a second vantage point, you need a rule for combining the answers. There are four situations when location A and location B each report:</p>
<table>
<thead><tr><th>A says</th><th>B says</th><th>Likely meaning</th></tr></thead>
<tbody>
<tr><td>down</td><td>down</td><td>Real outage, or a fault close to the target. Page.</td></tr>
<tr><td>down</td><td>up</td><td>A's path or A itself. Do not page yet; keep watching.</td></tr>
<tr><td>up</td><td>down</td><td>B's path, or a regional problem. Worth a look, rarely a page.</td></tr>
<tr><td>up</td><td>up</td><td>Healthy.</td></tr>
</tbody>
</table>
<p>The interesting row is the second. Disagreement is information: it says the problem is not simply "the target is dead". Whether to page on it is a policy choice, and it depends on what the monitor protects.</p>
<h2>Choosing the policy</h2>
<h3>Require agreement to page</h3>
<p>Page only when both locations see the failure. This removes nearly all path-related false alerts. The cost is that a genuine outage affecting only one region does not page, and you must be comfortable with that. For a service with users everywhere, a partial outage in one region is real, but it is usually a different kind of incident from "the site is down", and it often deserves a lower urgency anyway.</p>
<h3>Page on either</h3>
<p>Page when any location fails. You miss nothing, and you page for every bad path on the internet. Use this only where a missed regional failure is truly expensive and the notification is cheap, such as a message to a chat channel rather than a phone call.</p>
<h3>Tier by agreement</h3>
<p>The most useful setup splits the two cases. Agreement goes to the page-a-human path. Disagreement goes to a quiet channel with the detail attached ("failed from A, reachable from B"). The first case wakes someone; the second gets read in the morning, and if it persists it becomes a ticket.</p>
<h2>Choose second locations carefully</h2>
<ul>
<li><strong>Different networks, not just different addresses.</strong> Two probes in the same datacentre share an uplink and fail together. The point is independent paths.</li>
<li><strong>Different regions.</strong> Spread across continents if your users are. A single extra probe in the same country catches little.</li>
<li><strong>Ask only when needed.</strong> You do not need continuous multi-site traffic to a healthy target. Running the second opinion only after the first location sees a failure gives you nearly the same protection at a fraction of the traffic.</li>
</ul>
<h2>Limits of the idea</h2>
<p>A second location is not proof. Two probes can both be blocked by the same firewall rule, a rate limit or a bot filter that treats monitoring traffic as hostile. If your target blocks the probes while serving real users, every location will say "down", and you will be paged about an outage that does not exist. Allow-list your monitors, and check what your WAF does with repeated requests from the same few sources.</p>
<p>The reverse also holds. A second location that can reach the target does not prove users can. If the page loads for a probe but a login flow is broken, only an assertion on the response, not a second location, will show it. Treat location and content as separate questions.</p>
<h2>Where Hesklo fits</h2>
<p>Hesklo checks from several locations across North America, Europe and Asia, and a failure seen in one region is re-run from another before it counts as an outage. That is the "require agreement" policy, built in. The mechanics are on the <a href="/features">features</a> page, and the check types it applies to are described under <a href="/features/http-monitoring">HTTP monitoring</a>. How to keep the remaining noise down is covered in <a href="/features/alert-noise-control">alert noise control</a>.</p>
<h2>A checklist</h2>
<ol>
<li>For each paging monitor, write down which of the four situations above you would want to be woken for.</li>
<li>Make sure at least one other location sits on a different network and in a different region.</li>
<li>Decide the policy for disagreement: ignore, quiet channel or page. Do not leave it as an accident of configuration.</li>
<li>Allow-list your monitoring traffic in any firewall, WAF or rate limiter.</li>
<li>Pair location checks with a content assertion, so "reachable" does not stand in for "working".</li>
<li>Send confirmed failures into an <a href="/features/escalation-policies">escalation policy</a>, not straight to the whole team.</li>
<li>After the next incident, look at what each location reported and when. That record is how you tune the policy.</li>
</ol>
<p>To see the effect of vantage point yourself, run a URL through the <a href="/uptime-check">uptime check</a> tool, then compare it with a request from your own laptop. When the two disagree, you have found a path problem, which is exactly what a single monitoring location cannot show you.</p>
]]></content:encoded>
  </item>
  <item>
    <title>An escalation policy that doesn&apos;t wake the whole team</title>
    <link>https://www.hesklo.com/blog/escalation-policy-without-waking-everyone</link>
    <guid isPermaLink="true">https://www.hesklo.com/blog/escalation-policy-without-waking-everyone</guid>
    <pubDate>Tue, 06 Oct 2026 00:00:00 +0000</pubDate>
    <description>Page one person first, wait, then widen. How to design tiers, timings and a last resort so an alert always reaches someone without paging everybody.</description>
    <content:encoded><![CDATA[<p>The simplest escalation policy is "alert everyone". It never misses, and it teaches a whole team to sleep through their phones. A better policy pages one person, gives them a fair window to respond, and only then widens the circle.</p>
<h2>Start from the failure you are protecting against</h2>
<p>An escalation policy exists to answer one question: what happens if the person we paged does not respond? They may be asleep, in a tunnel, on a flight, or have muted the channel. The policy is the plan for that case. Everything else is routing.</p>
<p>Two opposite mistakes are common:</p>
<ul>
<li><strong>Too wide.</strong> Everyone is paged at once. Each person assumes someone else will pick it up, and the cost of every incident is multiplied by the size of the team.</li>
<li><strong>Too narrow.</strong> One person is paged once. If that page fails, the outage runs until a customer complains.</li>
</ul>
<p>The fix for both is the same: time. Widen the audience only as the clock runs.</p>
<h2>Build it in tiers</h2>
<p>A workable shape for a small team has three tiers.</p>
<ol>
<li><strong>Tier 1: the person on call.</strong> One named human, reached by the channel most likely to interrupt them. Chat is fine for a daytime warning; for a night-time outage it should be something that rings, such as a push notification, SMS or a phone call.</li>
<li><strong>Tier 2: a backup.</strong> A second person, paged only if tier 1 has not acknowledged within the window. They are not a co-owner. They are insurance against tier 1 being unreachable.</li>
<li><strong>Tier 3: a last resort.</strong> A team lead, a shared channel, or a ticket. This tier is rarely reached. Its job is that the alert is never <em>only</em> in somebody's pocket.</li>
</ol>
<p>Three tiers is usually enough. Every extra tier adds delay and another place for the policy to be wrong.</p>
<h2>Choose the waits deliberately</h2>
<p>The wait between tiers is a trade between speed and noise. Too short, and you page the backup while tier 1 is still reaching for the phone. Too long, and a dead pager costs you the better part of an hour.</p>
<p>A reasonable starting point for a customer-facing service is 5 to 10 minutes before tier 2 and another 10 to 15 before tier 3. Work out the worst case explicitly. If detection takes 3 minutes, tier 2 waits 10 and tier 3 waits 15, then a completely unanswered page reaches the last resort after 28 minutes. Ask whether your business can live with that number. If not, shorten the waits. If it can, resist the urge to shorten them anyway.</p>
<p>Internal tools, batch jobs and staging environments can have far longer waits, or no escalation at all. The policy should match what an hour of downtime costs for <em>that</em> service.</p>
<h2>Acknowledgement is what stops the clock</h2>
<p>Without an acknowledgement signal, a policy cannot tell "tier 1 is handling it" from "tier 1 never saw it". It can only keep escalating on a timer, which pages the backup even when tier 1 is already working the problem. That is exactly the noise you were trying to avoid.</p>
<p>So the alert itself needs a way to say "I have this". The cheapest version is a link in the alert message. One tap, no login, no hunting for the incident. Once acknowledged, the escalation stops. If nobody taps it, the next tier fires. Silence should mean "nobody has seen this", never "someone probably has".</p>
<h2>Repeat until somebody answers</h2>
<p>A single page is easy to miss. If nothing acknowledges, repeat the page to the current tier at an interval, say every 5 minutes, before moving on. Repetition is the cheapest escalation there is: it costs no extra people.</p>
<p>Put a limit on it. A page repeated forever to a phone that is switched off is just noise in the log. Repeat a few times, then move to the next tier.</p>
<h2>Keep quiet hours honest</h2>
<p>Not every alert deserves a 3 a.m. phone call. Decide ahead of time which alerts do:</p>
<ul>
<li><strong>Page at any hour:</strong> the customer-facing service is down, a payment path is failing, data is at risk.</li>
<li><strong>Hold until morning:</strong> a certificate with three weeks left, a slow-but-working endpoint, an internal dashboard.</li>
</ul>
<p>Mixing the two on one policy is how a team ends up muting everything. Give the urgent and the non-urgent separate paths, and send the non-urgent ones to a place that is read at working hours.</p>
<h2>Follow the rota, not a name</h2>
<p>Hard-coding "Alex" as tier 1 works until Alex goes on holiday. A policy that points at a role, <em>whoever is on call right now</em>, survives people changing. That means the rota needs to be a real, current thing and not a wiki page nobody updates. Keep it small: one person on duty at a time, no overlaps, and a clear fallback when the slot is empty. Test the fallback, because it is the one that runs on the worst night.</p>
<h2>One policy per service, not one for the company</h2>
<p>A global policy is simple to explain and wrong for most services. The checkout API and the internal reporting job should not share a ruleset. Let each monitor carry its own policy, and start new monitors from a copy of a sensible default rather than from nothing.</p>
<h2>How this looks in Hesklo</h2>
<p>In Hesklo each monitor has its own canvas, and the escalation policy is what you draw on it: a down port connected to notify steps, with waits, branches and repeats between them. Email steps can follow an <a href="/features/on-call-scheduling">on-call rota</a>, and an <a href="/features/incident-acknowledgement">acknowledge link</a> in the email stops the escalation. The <a href="/features/escalation-policies">escalation policies page</a> shows the building blocks. None of the design above depends on it, though; the same three tiers work in any tool.</p>
<h2>A checklist to review your policy</h2>
<ol>
<li>Can you name tier 1, tier 2 and tier 3 for every customer-facing service right now?</li>
<li>Is the worst-case time from detection to last resort written down, and acceptable?</li>
<li>Does acknowledging an alert actually stop the escalation?</li>
<li>Is repetition capped?</li>
<li>Do urgent and non-urgent alerts take different paths?</li>
<li>Does the policy follow a rota rather than a hard-coded name?</li>
<li>Have you triggered a test alert end to end in the last quarter, including the tier 2 hop?</li>
</ol>
<p>That last question matters most. An escalation policy you have never run is a hypothesis. Run it on a quiet afternoon, with a teammate pretending to be asleep, and fix whatever turns out to be wrong. Related reading: <a href="/features/alert-noise-control">controlling alert noise</a> keeps tier 1 from being paged for things that were never outages.</p>
]]></content:encoded>
  </item>
  <item>
    <title>What an expired TLS certificate breaks, and how to get warned early</title>
    <link>https://www.hesklo.com/blog/expiring-tls-certificate-warning</link>
    <guid isPermaLink="true">https://www.hesklo.com/blog/expiring-tls-certificate-warning</guid>
    <pubDate>Fri, 02 Oct 2026 00:00:00 +0000</pubDate>
    <description>An expired certificate is a full outage, not a degradation. Where renewal fails, which clients break first, and how to set a warning that leaves time to fix it.</description>
    <content:encoded><![CDATA[<p>An expired TLS certificate does not slow a service down. Browsers show a full-page warning, API clients refuse to connect, and your own health checks fail, all at the same second. The fix is usually small, but it has to happen after the outage starts unless something warned you earlier.</p>
<h2>What actually breaks</h2>
<p>A certificate carries a "not after" timestamp. Once the clock passes it, a client doing normal validation must reject the connection. There is no grace period and no partial failure.</p>
<ul>
<li><strong>Browsers</strong> block the page behind an interstitial. Most visitors leave; a few click through, and you should not want that habit in your users either.</li>
<li><strong>API clients and SDKs</strong> fail the request with a verification error. Well-written ones do not retry their way around it, because that would defeat the point of TLS.</li>
<li><strong>Webhooks you send and receive</strong> stop. The receiving side drops your deliveries, and your calls to a partner with a lapsed certificate fail in your code, not theirs.</li>
<li><strong>Internal service-to-service calls</strong> break wherever mutual TLS or a private CA is in use, often far from anything you think of as "the website".</li>
<li><strong>Mobile apps</strong> that pin a certificate or its chain can fail until a new app version ships, which no server-side fix can speed up.</li>
</ul>
<p>The common thread is that the failure is total and immediate. Nothing degrades gracefully, so there is no early symptom to notice.</p>
<h2>Why it still happens when renewal is automated</h2>
<p>Automatic issuance has made expiry rarer, and it has also changed who gets hurt. Certificates from ACME-based authorities are short-lived (Let's Encrypt issues 90-day certificates), and the industry is moving to shorter maximum lifetimes still. Shorter lifetimes mean more renewals, so a renewal path that works 99% of the time fails more often in absolute terms. These are the failures we see described again and again:</p>
<ul>
<li><strong>The renewal job stops running.</strong> A cron entry lost in a server rebuild, a timer that was never enabled, a container that no longer ships the client. The job cannot report its own absence.</li>
<li><strong>Renewal runs but cannot validate.</strong> A DNS record was moved, a firewall rule now blocks the HTTP-01 challenge, an API token for the DNS provider expired.</li>
<li><strong>Renewal succeeds but nothing reloads.</strong> A new certificate is on disk, and the web server or load balancer is still serving the old one from memory.</li>
<li><strong>The wrong certificate is served somewhere.</strong> One node in a pool, one CDN edge, or one rarely used hostname still has the old file.</li>
<li><strong>Someone else owns it.</strong> A vendor endpoint, a partner API or an appliance with a manually installed certificate, where you cannot see the renewal process at all.</li>
</ul>
<p>Notice that in the third and fourth cases the certificate on disk is fine. Only a check against the live endpoint, doing the same handshake a client does, sees what clients see.</p>
<h2>Check the served certificate, not the file</h2>
<p>The most reliable signal is the expiry date of the certificate that the endpoint presents right now. You can read it yourself:</p>
<pre><code>echo | openssl s_client -connect example.com:443 -servername example.com 2&gt;/dev/null \
  | openssl x509 -noout -enddate
</code></pre>
<p>Two details matter. Pass <code>-servername</code>, or a server hosting several sites may hand you a different site's default certificate through SNI. And check every hostname that matters, not only the apex: <code>www</code>, <code>api</code> and the status or login hostnames are usually separate certificates or at least separate configuration.</p>
<p>A one-off command proves nothing about next month, so the real task is to run the same check on a schedule and alert on the number of days remaining.</p>
<h2>Choosing a warning threshold</h2>
<p>The threshold has to be longer than the longest delay between "someone is told" and "a new certificate is live and reloaded". Work it out from your own process rather than copying a number.</p>
<ul>
<li><strong>Fully automated, 90-day certificates.</strong> Renewal normally happens around 30 days before expiry, so a healthy certificate should never be seen with fewer than about 30 days left. A warning at 21 days means "the renewal that should have happened did not". That leaves three weeks, plenty for a person to look.</li>
<li><strong>Manual or ticketed renewal.</strong> Add the time to get a purchase approved, validation completed and a change window. Two to four weeks is a common floor; use more if renewals need a vendor.</li>
<li><strong>Certificates you do not control.</strong> Warn early, to someone who can contact the owner, because your fix is an email, not a command.</li>
</ul>
<p>Use two stages. The first is a low-urgency note, such as a ticket or a chat message, with weeks of slack. The second, a few days before expiry, should be a real page, because by then the automatic path has clearly failed and the cost of waiting is an outage. A single alert at seven days tends to be seen on a Friday and fixed on a Monday that never comes.</p>
<h2>Who should receive it</h2>
<p>Expiry warnings are the classic alert that nobody owns. They fire rarely, so people forget where they go. Send the early stage somewhere with an owner, a named person or a team queue, and make sure that destination survives staff changes. Avoid a shared mailbox that nobody reads: the warning is only useful if a human reads it within days.</p>
<p>The late stage should follow your normal escalation path, since it is now an incident in waiting. If your on-call process can be reached for an outage, it can be reached for "this will be an outage on Thursday".</p>
<h2>Doing this in Hesklo</h2>
<p>Every HTTPS monitor in Hesklo tracks its certificate expiry as part of the normal check, so there is no separate monitor to create. A branch node can test whether the certificate expires within a number of days you choose and route the flow to whichever notification you want. That makes the two-stage setup above two branches on one monitor. The details are on the <a href="/features/ssl-monitoring">SSL monitoring page</a>, and the canvas that holds the branches is described under <a href="/features/escalation-policies">escalation policies</a>.</p>
<h2>A checklist</h2>
<ol>
<li>List every hostname clients connect to, including vendor and partner endpoints you depend on.</li>
<li>Check the served certificate for each one, with SNI, not the file on disk.</li>
<li>Find out how each renews and who owns it. Write the owner down.</li>
<li>Set a first warning longer than your slowest renewal path, and a second a few days before expiry.</li>
<li>Route the first to a named owner and the second to whoever is on call.</li>
<li>After any change to DNS, load balancers, CDNs or the renewal client, confirm the served expiry date moved.</li>
<li>Once a year, deliberately let a low-risk test certificate approach your warning threshold and confirm the alert arrives.</li>
</ol>
<p>The last item is the one most teams skip. An alert that has never fired is an assumption, and an expiry warning is exactly the kind you rarely see in practice. Test it while nothing depends on the answer.</p>
]]></content:encoded>
  </item>
  <item>
    <title>Why an uptime check should fail three times before it pages</title>
    <link>https://www.hesklo.com/blog/fail-three-times-before-paging</link>
    <guid isPermaLink="true">https://www.hesklo.com/blog/fail-three-times-before-paging</guid>
    <pubDate>Fri, 02 Oct 2026 00:00:00 +0000</pubDate>
    <description>One failed check is a weak signal. Here is the arithmetic behind confirmation thresholds, what they cost in detection time, and how to pick a number.</description>
    <content:encoded><![CDATA[<p>A single failed HTTP check is not an outage. It is one request that did not come back in time, and the cause may be the target, the network in between, or the machine that sent it. Paging a person on that one data point is the most common way a monitoring setup teaches a team to ignore it.</p>
<h2>The arithmetic of a false alert</h2>
<p>Suppose a healthy service has a 1% chance that any single check fails for reasons unrelated to the service: a dropped packet, a resolver hiccup, a slow TLS handshake. That figure is invented for illustration, but the shape of the result does not depend on it.</p>
<ul>
<li>Checking every minute gives 1,440 checks a day. At 1% you expect about 14 spurious failures a day.</li>
<li>If one failure pages, that is about 14 pages a day for a service that never went down.</li>
<li>If you require 3 failures in a row, and the failures are independent, the chance of a false trigger per check window is 0.01 &times; 0.01 &times; 0.01, which is one in a million. Across a day that is roughly 0.001 false pages.</li>
</ul>
<p>Independence is the assumption doing the work. A real outage fails every check, so three in a row happens with certainty. A random blip rarely repeats three times, so it does not. Confirmation exploits that asymmetry: it costs almost nothing against real outages and removes almost all of the noise.</p>
<h2>What it costs: detection time</h2>
<p>The price is delay. With a check every 60 seconds and a threshold of 3, an outage that starts just after a passing check is confirmed after roughly three further checks, so expect between two and three minutes before anything fires. With a 5-minute interval the same threshold means up to 15 minutes.</p>
<p>That gives you a simple formula to reason with:</p>
<pre><code>worst-case detection delay = interval &times; threshold
</code></pre>
<p>Decide the delay you can live with first, then split it between interval and threshold. A 3-minute budget could be a 1-minute interval with 3 failures, or a 90-second interval with 2. The first is usually better, because more frequent samples give you a finer picture of what happened.</p>
<h2>Failures that are not independent</h2>
<p>The arithmetic breaks when failures are correlated. Three consecutive failures caused by one cause count as one event, not three. Common cases:</p>
<ul>
<li><strong>A slow dependency.</strong> If your timeout is 5 seconds and the endpoint takes 6 during a nightly job, every check in that window fails. Confirmation does not help; the timeout is simply set too tight.</li>
<li><strong>A deploy.</strong> A rolling restart can fail several checks in a row. That may be a real, short outage, or it may be planned work that should not page at all.</li>
<li><strong>A fault on the sending side.</strong> If the monitoring host loses its uplink or its resolver, all of its checks fail together and for as long as the fault lasts.</li>
</ul>
<p>Confirmation handles random noise. It does not handle systematic causes, which need a different fix: a better timeout, a maintenance window, or a second place to check from.</p>
<h2>Choosing the number</h2>
<p>There is no universal threshold, but a few rules of thumb hold up.</p>
<ol>
<li><strong>Start at 3 for anything that pages a human.</strong> Two failures still leaves room for a bad moment on one path. Three is a reasonable default for a check every minute.</li>
<li><strong>Use 1 only where a miss is more expensive than a false alert,</strong> such as a payment endpoint with a very short interval and a cheap, non-waking notification channel.</li>
<li><strong>Scale with interval.</strong> A 10-minute interval with threshold 3 means half an hour of blindness. Lower the threshold or shorten the interval.</li>
<li><strong>Separate the channels.</strong> A first failure can go to a log or a quiet chat channel while only the confirmed failure pages someone. The early signal is still recorded; it just does not wake anyone.</li>
</ol>
<h2>Set the timeout deliberately</h2>
<p>Most spurious failures are timeouts, not errors. A connection refused or a 503 is a definite answer. A timeout only says that nothing arrived in time, which a slow path or a busy sender produces as readily as a dead server. Set the timeout from the endpoint's real behaviour: look at its slow tail, add margin, and let the confirmation threshold absorb the rest. A timeout copied from a default is a frequent source of noise.</p>
<h2>Do not stop at confirmation</h2>
<p>Confirmation decides when a monitor is down. It does not decide how often you are told afterwards. A service that flaps between up and down every few minutes will re-trigger a fresh set of failures each time, so pair the threshold with a cooldown on the notification step. Otherwise a marginal service sends a message per transition, which is the same fatigue by another route.</p>
<p>Also keep planned work out of the picture. Pausing a monitor for the length of a deploy is cleaner than tuning a threshold to tolerate it, and it keeps the threshold honest for the cases it is meant for.</p>
<h2>Where Hesklo fits</h2>
<p>In Hesklo the failure threshold is set per monitor, alongside its own timeout and interval, and notify steps carry a flap-dampening cooldown. The details are on the <a href="/features/alert-noise-control">alert noise control</a> page, and the check itself is described under <a href="/features/http-monitoring">HTTP monitoring</a>. Pausing for planned work is covered by <a href="/features/maintenance-windows">maintenance windows</a>.</p>
<h2>A checklist for one monitor</h2>
<ul>
<li>Write down the longest detection delay you accept. Work out interval &times; threshold.</li>
<li>Set the timeout from the endpoint's observed slow tail, not a default.</li>
<li>Use a threshold of 3 for anything that wakes a person; justify any lower value.</li>
<li>Add a cooldown so a flapping service does not repeat itself.</li>
<li>Pause the monitor for planned work instead of absorbing it with a higher threshold.</li>
<li>Route the confirmed failure into your <a href="/features/escalation-policies">escalation policy</a>; send unconfirmed signals somewhere quiet.</li>
<li>Review a month later: for every page, was it real? If not, find the cause rather than raising the number again.</li>
</ul>
<p>You can try the idea without setting anything up: run a one-off request with the <a href="/uptime-check">uptime check</a> tool, then run it again a minute later. If the answers differ, you have just seen why one sample is not enough.</p>
]]></content:encoded>
  </item>
</channel>
</rss>
