Every team eventually asks the same question at the worst possible time: "Wait — how long has the site been down?" Usually the answer is "since a customer emailed us," which is not a monitoring strategy.
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.
The 20-line version
At its core, uptime monitoring is one question asked on a loop: did the endpoint respond the way I expected? curl answers it and cron provides the loop.
#!/usr/bin/env bash
# uptime-check.sh — minimal uptime monitor
set -euo pipefail
URL="https://example.com"
TIMEOUT=10
code=$(curl --silent --show-error --location \
--max-time "$TIMEOUT" \
--output /dev/null \
--write-out '%{http_code}' \
"$URL" || echo "000")
if [[ "$code" =~ ^(2|3)[0-9][0-9]$ ]]; then
echo "UP $URL ($code)"
else
echo "DOWN $URL ($code)" >&2
# trigger an alert here
fi
Wire it to cron and you have a monitor:
* * * * * /opt/monitoring/uptime-check.sh >> /var/log/uptime.log 2>&1
That's a genuine, working uptime check. But "working in the happy path" and "trustworthy at 3 a.m." are two very different bars.
Making it resistant to false positives
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 think you're covered.
Three fixes matter most:
1. Retry before you alert. A single failed request is noise; three failures in a row is a signal. Never page a human on one bad sample.
check() {
curl --silent --location --max-time "$TIMEOUT" \
--output /dev/null --write-out '%{http_code}' "$1" || echo "000"
}
fails=0
for _ in 1 2 3; do
code=$(check "$URL")
[[ "$code" =~ ^(2|3)[0-9][0-9]$ ]] && { fails=0; break; }
((fails++)); sleep 5
done
(( fails >= 3 )) && echo "DOWN $URL ($code)" >&2
2. Validate the algorithm and the destination, not just "did it connect." A 200 OK 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.
3. Set explicit timeouts. Without --max-time, a hung connection hangs your whole check. A monitor that blocks is a monitor that lies.
Don't forget the certificate
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:
host="example.com"
expiry=$(echo | openssl s_client -servername "$host" -connect "$host:443" 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
days_left=$(( ( $(date -d "$expiry" +%s) - $(date +%s) ) / 86400 ))
(( days_left < 14 )) && echo "SSL $host expires in $days_left days" >&2
Fourteen days is a sane warning window — enough lead time to renew without a fire drill.
Where the DIY script quietly breaks down
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:
- One vantage point. Your cron job runs from one machine. If that 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.
- Who watches the watcher? 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.
- No history. "Is it up right now" 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.
-
Alert routing and dedup. Real incidents need escalation, on-call schedules, and grouping so one outage doesn't fire 200 identical messages. That's a system, not a
>> curlto a webhook. - Status communication. When you are down, users want a status page, not silence. Building and hosting one is its own project.
When to stop scripting and use a hosted monitor
The honest rule of thumb: keep the script while monitoring is a convenience; move to a dedicated monitor once uptime is a promise you make to someone else. The moment there's a customer, an SLA, or an on-call rotation, the properties above stop being nice-to-haves.
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, enterno.io 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.
Whichever way you go, the underlying discipline is identical, so it's worth internalizing:
TL;DR checklist
- ✅ Check on a schedule (cron or hosted).
- ✅ Retry N times before alerting — kill false positives.
- ✅ Assert on real behavior (health endpoint / body content), not just a
200. - ✅ Always set explicit timeouts.
- ✅ Monitor TLS-certificate expiry (warn ~14 days out).
- ✅ Check from more than one location once real users depend on you.
- ✅ Make sure something other than the failing server is doing the watching.
- ✅ Keep history and a status page once uptime becomes a commitment.
Start with the 20 lines. Graduate when the stakes — not the ego — say it's time.
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.
Top comments (0)