<?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: Enterno</title>
    <description>The latest articles on DEV Community by Enterno (@enterno).</description>
    <link>https://dev.to/enterno</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%2F4027011%2Fdb7754b2-9d7d-464d-8454-9a2d8f1a97f3.png</url>
      <title>DEV Community: Enterno</title>
      <link>https://dev.to/enterno</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/enterno"/>
    <language>en</language>
    <item>
      <title>Website Uptime Monitoring: From a 20-Line Bash Script to Production-Ready Alerts</title>
      <dc:creator>Enterno</dc:creator>
      <pubDate>Mon, 13 Jul 2026 12:27:54 +0000</pubDate>
      <link>https://dev.to/enterno/website-uptime-monitoring-from-a-20-line-bash-script-to-production-ready-alerts-36j9</link>
      <guid>https://dev.to/enterno/website-uptime-monitoring-from-a-20-line-bash-script-to-production-ready-alerts-36j9</guid>
      <description>&lt;p&gt;Every team eventually asks the same question at the worst possible time: &lt;em&gt;"Wait — how long has the site been down?"&lt;/em&gt; Usually the answer is "since a customer emailed us," which is not a monitoring strategy.&lt;/p&gt;

&lt;p&gt;You don't need a heavyweight observability stack to answer that question. You need something that checks your endpoints on a schedule and shouts when they break. Let's build that from scratch, make it robust enough to trust, and then be honest about where a hand-rolled script stops being enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 20-line version
&lt;/h2&gt;

&lt;p&gt;At its core, uptime monitoring is one question asked on a loop: &lt;em&gt;did the endpoint respond the way I expected?&lt;/em&gt; &lt;code&gt;curl&lt;/code&gt; answers it and &lt;code&gt;cron&lt;/code&gt; provides the loop.&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;# uptime-check.sh — minimal uptime monitor&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://example.com"&lt;/span&gt;
&lt;span class="nv"&gt;TIMEOUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10

&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--show-error&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nt"&gt;--max-time&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TIMEOUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nt"&gt;--output&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="nt"&gt;--write-out&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
            &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&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;"000"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^&lt;span class="o"&gt;(&lt;/span&gt;2|3&lt;span class="o"&gt;)[&lt;/span&gt;0-9][0-9]&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"UP   &lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"DOWN &lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="c"&gt;# trigger an alert here&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it to cron and you have a monitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* * * * * /opt/monitoring/uptime-check.sh &amp;gt;&amp;gt; /var/log/uptime.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a genuine, working uptime check. But "working in the happy path" and "trustworthy at 3 a.m." are two very different bars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it resistant to false positives
&lt;/h2&gt;

&lt;p&gt;The fastest way to kill a monitoring system is alert fatigue. If it cries wolf on every transient blip, people mute it — and then it's worse than having nothing, because you &lt;em&gt;think&lt;/em&gt; you're covered.&lt;/p&gt;

&lt;p&gt;Three fixes matter most:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Retry before you alert.&lt;/strong&gt; A single failed request is noise; three failures in a row is a signal. Never page a human on one bad sample.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;check&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  curl &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TIMEOUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="nt"&gt;--output&lt;/span&gt; /dev/null &lt;span class="nt"&gt;--write-out&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&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;"000"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;fails&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;for &lt;/span&gt;_ &lt;span class="k"&gt;in &lt;/span&gt;1 2 3&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;check &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^&lt;span class="o"&gt;(&lt;/span&gt;2|3&lt;span class="o"&gt;)[&lt;/span&gt;0-9][0-9]&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;fails&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;((&lt;/span&gt;fails++&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;5
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="o"&gt;((&lt;/span&gt; fails &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; 3 &lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"DOWN &lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Validate the algorithm and the destination, not just "did it connect."&lt;/strong&gt; A &lt;code&gt;200 OK&lt;/code&gt; that serves an error page is still down for your users. If you can, assert on a known string in the body or a health endpoint that touches the database, not just the front door.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Set explicit timeouts.&lt;/strong&gt; Without &lt;code&gt;--max-time&lt;/code&gt;, a hung connection hangs your whole check. A monitor that blocks is a monitor that lies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't forget the certificate
&lt;/h2&gt;

&lt;p&gt;Expired TLS certificates are self-inflicted outages, and they're completely predictable — which makes getting caught by one especially painful. Add a days-until-expiry check while you're here:&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;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"example.com"&lt;/span&gt;
&lt;span class="nv"&gt;expiry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; | openssl s_client &lt;span class="nt"&gt;-servername&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-connect&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;:443"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
         | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-enddate&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;days_left&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&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;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$expiry&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; +%s&lt;span class="si"&gt;)&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; +%s&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;

&lt;span class="o"&gt;((&lt;/span&gt; days_left &amp;lt; 14 &lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SSL &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt; expires in &lt;/span&gt;&lt;span class="nv"&gt;$days_left&lt;/span&gt;&lt;span class="s2"&gt; days"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fourteen days is a sane warning window — enough lead time to renew without a fire drill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the DIY script quietly breaks down
&lt;/h2&gt;

&lt;p&gt;The script above is honestly fine for a personal project or an internal tool. The problems show up as you scale, and most of them are structural — no amount of extra bash fixes them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One vantage point.&lt;/strong&gt; Your cron job runs from one machine. If &lt;em&gt;that&lt;/em&gt; box (or its network path) has the problem, you'll get false alarms; if the outage is regional, you'll miss real ones. Real availability needs checks from multiple locations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who watches the watcher?&lt;/strong&gt; If the server running your monitor goes down, your monitoring goes down with it — silently. The thing most likely to fail is the thing you're least likely to be alerted about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No history.&lt;/strong&gt; "Is it up &lt;em&gt;right now&lt;/em&gt;" is easy. "What was our uptime last month," "when did response time start creeping up," "show the customer an SLA report" — none of that lives in a log file you'll actually parse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert routing and dedup.&lt;/strong&gt; Real incidents need escalation, on-call schedules, and grouping so one outage doesn't fire 200 identical messages. That's a system, not a &lt;code&gt;&amp;gt;&amp;gt; curl&lt;/code&gt; to a webhook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status communication.&lt;/strong&gt; When you &lt;em&gt;are&lt;/em&gt; down, users want a status page, not silence. Building and hosting one is its own project.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to stop scripting and use a hosted monitor
&lt;/h2&gt;

&lt;p&gt;The honest rule of thumb: &lt;strong&gt;keep the script while monitoring is a convenience; move to a dedicated monitor once uptime is a promise you make to someone else.&lt;/strong&gt; The moment there's a customer, an SLA, or an on-call rotation, the properties above stop being nice-to-haves.&lt;/p&gt;

&lt;p&gt;A hosted uptime monitor exists precisely to own the parts that are tedious to build well: checks from independent regions, retry/dedup logic, incident history, alert routing across email/Telegram/Slack/webhooks, SSL-expiry tracking, and public status pages. If you'd rather not run and babysit that infrastructure yourself, &lt;a href="https://enterno.io" rel="noopener noreferrer"&gt;enterno.io&lt;/a&gt; is a straightforward option that covers HTTP/SSL/ping/DNS checks, multi-region monitoring, and status pages out of the box — the same checklist we just built by hand, minus the maintenance.&lt;/p&gt;

&lt;p&gt;Whichever way you go, the underlying discipline is identical, so it's worth internalizing:&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Check on a schedule (cron or hosted).&lt;/li&gt;
&lt;li&gt;✅ Retry N times before alerting — kill false positives.&lt;/li&gt;
&lt;li&gt;✅ Assert on real behavior (health endpoint / body content), not just a &lt;code&gt;200&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;✅ Always set explicit timeouts.&lt;/li&gt;
&lt;li&gt;✅ Monitor TLS-certificate expiry (warn ~14 days out).&lt;/li&gt;
&lt;li&gt;✅ Check from more than one location once real users depend on you.&lt;/li&gt;
&lt;li&gt;✅ Make sure &lt;em&gt;something other than the failing server&lt;/em&gt; is doing the watching.&lt;/li&gt;
&lt;li&gt;✅ Keep history and a status page once uptime becomes a commitment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with the 20 lines. Graduate when the stakes — not the ego — say it's time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What does your team use for uptime monitoring — a hand-rolled script, a hosted service, or a full observability stack? Curious where people draw the DIY line.&lt;/em&gt;&lt;/p&gt;

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