<?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: Beamfar</title>
    <description>The latest articles on DEV Community by Beamfar (@beamfar).</description>
    <link>https://dev.to/beamfar</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%2F4244297%2F545e9bfa-8bfb-4f1c-b790-350c0a9825c3.png</url>
      <title>DEV Community: Beamfar</title>
      <link>https://dev.to/beamfar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/beamfar"/>
    <language>en</language>
    <item>
      <title>How to know your website is down before your customers do: a practical guide to uptime monitoring</title>
      <dc:creator>Beamfar</dc:creator>
      <pubDate>Sat, 14 Nov 2026 13:18:14 +0000</pubDate>
      <link>https://dev.to/beamfar/how-to-know-your-website-is-down-before-your-customers-do-a-practical-guide-to-uptime-monitoring-3jpk</link>
      <guid>https://dev.to/beamfar/how-to-know-your-website-is-down-before-your-customers-do-a-practical-guide-to-uptime-monitoring-3jpk</guid>
      <description>&lt;p&gt;The worst way to learn your website is down is from a customer: a support ticket, a social media post, a client asking why checkout is blank.&lt;/p&gt;

&lt;p&gt;This post covers what an uptime check should look at, why one failure is not an outage, alert fatigue, cron jobs, and a script you can run today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why customers notice first
&lt;/h2&gt;

&lt;p&gt;Your visitors use the site when you don't: at night, at weekends, from other countries and networks. Without a check of your own, they are your monitoring.&lt;/p&gt;

&lt;p&gt;When you look, you see a friendlier version: a cached page, a logged-in session, a DNS answer your resolver remembers. Many failures are partial, too: checkout returns a 500 while the home page loads, or &lt;code&gt;www&lt;/code&gt; works and the bare domain doesn't. Others come with a date: an expired certificate or domain.&lt;/p&gt;

&lt;p&gt;Monitoring has a delay as well: the wait for the next check, a confirmation step, alert delivery, and the time until someone looks. With one-minute checks that is usually a few minutes, not seconds. No setup guarantees you hear first. Aim for a short, known delay and an alert in front of someone who can act.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a real check should look at
&lt;/h2&gt;

&lt;p&gt;"The server answered" is not "a visitor can use the site". Check each layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DNS.&lt;/strong&gt; Does the name resolve, to the addresses you expect? A deleted record or a lapsed domain breaks everything below it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TCP and TLS.&lt;/strong&gt; Does the server accept connections, and does the certificate verify for this host name?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate expiry.&lt;/strong&gt; Warn weeks ahead. New public certificates last at most 200 days, and 100 from March 2027 (&lt;a href="https://cabforum.org/2025/04/11/ballot-sc081v3-introduce-schedule-of-reducing-validity-and-data-reuse-periods/" rel="noopener noreferrer"&gt;CA/Browser Forum&lt;/a&gt;), and Let's Encrypt &lt;a href="https://letsencrypt.org/2025/01/22/ending-expiration-emails/" rel="noopener noreferrer"&gt;stopped sending expiry emails&lt;/a&gt; in June 2025. If renewal breaks, your check may be the only warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain expiry.&lt;/strong&gt; Registrar reminders go to whoever registered the domain, maybe someone who left years ago.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirects.&lt;/strong&gt; Check where you land: still HTTPS, still your domain, not a loop. A redirect to a parked page ends in a 200.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status and content.&lt;/strong&gt; A 200 can still be an error page. Look for text that only appears when the page works, such as "Add to cart".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response time.&lt;/strong&gt; A page that takes 30 seconds is down for the person waiting. Use a timeout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the domain date, RDAP, the structured successor to WHOIS, answers in one line (rdap.org forwards the query to the right registry):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -sL https://rdap.org/domain/example.com &lt;span class="o"&gt;|&lt;/span&gt;
  jq -r &lt;span class="s1"&gt;'.events[] | select(.eventAction == "expiration") | .eventDate'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some registries, such as &lt;code&gt;.de&lt;/code&gt; and &lt;code&gt;.eu&lt;/code&gt;, publish no date, so an empty answer means "unknown", not "fine".&lt;/p&gt;

&lt;h2&gt;
  
  
  A DIY check with curl
&lt;/h2&gt;

&lt;p&gt;This script checks the status code, where redirects end, response time, an optional piece of text and the certificate's days left, all in one request. It needs bash and curl.&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Usage: ./check-site.sh URL ["text the page must contain"]&lt;/span&gt;
&lt;span class="c"&gt;# Exits 1 if anything needs a look, so cron can alert on it.&lt;/span&gt;
&lt;span class="kd"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C  &lt;span class="c"&gt;# English month names and dot decimals&lt;/span&gt;

&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"${1:?usage: check-site.sh URL [text]}"&lt;/span&gt;
&lt;span class="nv"&gt;must_contain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"${2:-}"&lt;/span&gt;
&lt;span class="nv"&gt;max_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5    &lt;span class="c"&gt;# slower than this is a problem&lt;/span&gt;
&lt;span class="nv"&gt;min_days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;14      &lt;span class="c"&gt;# warn below this many certificate days&lt;/span&gt;
&lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;mktemp&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;mktemp&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'rm -f "$body" "$trace"'&lt;/span&gt; EXIT
&lt;span class="nv"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
fail&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PROBLEM: $*"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# 1. One request: status, timings, redirects, 10 s limit.&lt;/span&gt;
&lt;span class="c"&gt;#    -v also logs the certificate of each TLS connection to $trace.&lt;/span&gt;
&lt;span class="nv"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%{http_code} %{time_namelookup} %{time_appconnect} %{time_total} %{num_redirects} %{url_effective}\n'&lt;/span&gt;
&lt;span class="nb"&gt;read&lt;/span&gt; -r code dns tls total hops final &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
  curl -sS -v -L --max-redirs 5 -m 10 -A &lt;span class="s1"&gt;'check-site/1.0'&lt;/span&gt; -o &lt;span class="s2"&gt;"$body"&lt;/span&gt; -w &lt;span class="s2"&gt;"$fmt"&lt;/span&gt; &lt;span class="s2"&gt;"$url"&lt;/span&gt; 2&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"$trace"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"status=$code dns=${dns}s tls=${tls}s total=${total}s redirects=$hops final=$final"&lt;/span&gt;
grep &lt;span class="s1"&gt;'^curl: ('&lt;/span&gt; &lt;span class="s2"&gt;"$trace"&lt;/span&gt;   &lt;span class="c"&gt;# curl's own error message, if any&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"$code"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 000 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; fail &lt;span class="s2"&gt;"no response"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; 1&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"$code"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 2* &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; fail &lt;span class="s2"&gt;"status $code"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"$final"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; https://* &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; fail &lt;span class="s2"&gt;"ended on $final"&lt;/span&gt;
awk -v &lt;span class="nv"&gt;t&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"$total"&lt;/span&gt; -v &lt;span class="nv"&gt;m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"$max_seconds"&lt;/span&gt; &lt;span class="s1"&gt;'BEGIN { exit !(t &amp;gt; m) }'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; fail &lt;span class="s2"&gt;"slow: ${total}s"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt; -n &lt;span class="s2"&gt;"$must_contain"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ! grep -qF -- &lt;span class="s2"&gt;"$must_contain"&lt;/span&gt; &lt;span class="s2"&gt;"$body"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  fail &lt;span class="s2"&gt;"page does not contain \"$must_contain\""&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# 2. Days left on the certificate of the last connection.&lt;/span&gt;
&lt;span class="nv"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;sed -n &lt;span class="s1"&gt;'s/.*expire date: //p'&lt;/span&gt; &lt;span class="s2"&gt;"$trace"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; tail -1&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt; -z &lt;span class="s2"&gt;"$end"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  fail &lt;span class="s2"&gt;"no certificate expiry found"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="c"&gt;# GNU date first, then macOS/BSD date&lt;/span&gt;
  &lt;span class="nv"&gt;end_s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;date -d &lt;span class="s2"&gt;"$end"&lt;/span&gt; +%s 2&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; date -j -f &lt;span class="s1"&gt;'%b %d %T %Y %Z'&lt;/span&gt; &lt;span class="s2"&gt;"$end"&lt;/span&gt; +%s&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;end_s - &lt;span class="nv"&gt;$(&lt;/span&gt;date +%s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; / 86400 &lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"certificate: $days days left (until $end)"&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt; days &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; min_days &lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; fail &lt;span class="s2"&gt;"certificate has $days days left"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="nv"&gt;$problems&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;curl verifies the certificate chain and host name by default, so a broken certificate fails the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./check-site.sh https://expired.badssl.com
status=000 dns=0.042882s tls=0.000000s total=0.558530s redirects=0 final=https://expired.badssl.com/
curl: (60) SSL certificate problem: certificate has expired
PROBLEM: no response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expiry date comes from curl's &lt;code&gt;-v&lt;/code&gt; log, which most Linux and macOS builds print. &lt;code&gt;openssl s_client&lt;/code&gt; works too, but it opens a second connection with no timeout of its own: in testing, a host with an unreachable IPv6 address stalled it for over a minute.&lt;/p&gt;

&lt;p&gt;Run it from cron on a machine that doesn't host the site, and alert only on failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*/5 * * * * /usr/local/bin/check-site.sh https://example.com "Add to cart" &amp;gt; /tmp/check.log 2&amp;gt;&amp;amp;1 || mail -s "example.com failed its check" you@example.com &amp;lt; /tmp/check.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One failed check from one place is not an outage
&lt;/h2&gt;

&lt;p&gt;The script checks from one place. A failed request only proves the path from the checker to the site failed: the checker's network, its DNS resolver, a firewall rate-limiting it, one unhealthy CDN node, a deploy restarting the server. Most of these are not an outage.&lt;/p&gt;

&lt;p&gt;Three common filters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Retry from the same place&lt;/strong&gt; a few seconds later. It removes blips, not a routing problem that outlasts the retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait for several failures in a row.&lt;/strong&gt; Less noise, more delay, still one point of view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirm from a second location&lt;/strong&gt; with an independent network path. Both fail: the site is very likely down. One fails: a regional problem, worth reading but not worth waking someone for.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trade-off: a strict two-location rule can hide an outage that only one region sees, so check from near your visitors and read the regional failures. For the script, re-running the check 30 seconds after a failure is a cheap filter. A real second location is the hard part to build yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alert fatigue is how real outages get missed
&lt;/h2&gt;

&lt;p&gt;If every blip sends a message, people mute the channel, and the real outage lands where nobody reads. False alarms teach people to ignore alerts. A few rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alert on confirmed failures, once per incident, plus one message on recovery.&lt;/li&gt;
&lt;li&gt;Keep urgent and planned apart: "site down" goes to chat or a phone, "certificate expires in 14 days" to email or a ticket.&lt;/li&gt;
&lt;li&gt;Pause alerts during planned maintenance.&lt;/li&gt;
&lt;li&gt;Send alerts where someone looks, agree who acts, and send a test alert after every change.&lt;/li&gt;
&lt;li&gt;Once a month, count the alerts that needed action. If few did, tune.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cron jobs and backups: alert on silence
&lt;/h2&gt;

&lt;p&gt;An outside check can't see a backup that stopped running. Cron fails quietly: the crontab didn't move to the new server, &lt;code&gt;PATH&lt;/code&gt; differs under cron, the disk filled up. Nothing runs, so nothing errors.&lt;/p&gt;

&lt;p&gt;A heartbeat turns this around. The job calls a URL when it succeeds, and the monitor alerts if the call doesn't arrive on schedule plus a grace time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 3 * * * /usr/local/bin/backup.sh &amp;amp;&amp;amp; curl -fsS -m 10 --retry 3 https://example.com/heartbeat/abc123 &amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; pings only after a successful backup. &lt;code&gt;-fsS&lt;/code&gt; fails on HTTP errors and stays quiet otherwise, &lt;code&gt;-m 10&lt;/code&gt; caps the time, &lt;code&gt;--retry 3&lt;/code&gt; rides out blips, and &lt;code&gt;&amp;gt; /dev/null&lt;/code&gt; keeps cron from mailing you on success. Set the grace time above your slowest normal run.&lt;/p&gt;

&lt;p&gt;Some services, Healthchecks.io among them, also accept the URL with &lt;code&gt;/fail&lt;/code&gt; appended, so a failed run alerts without waiting out the grace time.&lt;/p&gt;

&lt;p&gt;Your DIY checker is a cron job too, so give it a heartbeat. And a heartbeat only proves the job finished: test a restore now and then.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free options, and when DIY is enough
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;cron + curl&lt;/strong&gt;, as above: fine for a personal site or side project, if it runs away from the server it checks and you accept one location and no history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/louislam/uptime-kuma" rel="noopener noreferrer"&gt;Uptime Kuma&lt;/a&gt;&lt;/strong&gt;: open source (MIT), self-hosted, for example in Docker. HTTP, keyword, DNS and TCP checks, push monitors that work as heartbeats, status pages, 90+ notification services. It checks from wherever you host it, and keeping it running is your job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://uptimerobot.com/pricing/" rel="noopener noreferrer"&gt;UptimeRobot&lt;/a&gt; free tier&lt;/strong&gt;: hosted, 50 monitors every 5 minutes, HTTP, keyword, ping and port checks, commercial use allowed. Its pricing page lists SSL and domain expiry, heartbeats and multi-location checks as paid (November 2026).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://healthchecks.io/" rel="noopener noreferrer"&gt;Healthchecks.io&lt;/a&gt;&lt;/strong&gt;: built for cron jobs, free for 20 jobs, open source (BSD) if you self-host.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DIY is enough when downtime costs you little and someone technical reads the alerts. Go hosted when the site sells something, needs confirmation from more than one place, or a client wants the history.&lt;/p&gt;

&lt;p&gt;Whichever option you pick, set it up before the next outage. And if you found out about one in a memorable way, tell me in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was generated with AI. The commands and the script were tested before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>monitoring</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
