2026年11月13日

2026年11月13日

Simple Uptime Monitoring Script with curl: cron and Slack Alerts

This article is a localized edition of the Japanese original. Read the Japanese original

To find out quickly when a website stops responding, you run "uptime monitoring": access the page at regular intervals and check the result. You do not need a dedicated monitoring service for basic checks; curl, a shell script and cron are enough.

In this article you will build a script that works like this:

  • It requests the given URL with curl and checks that the HTTP status is 200 and, optionally, that the page contains a specific string
  • On failure it waits a few seconds and retries, and only treats the site as "down" after consecutive failures
  • It notifies you only when the status changes from healthy to down and from down to recovered (so you do not get the same alert every minute)

The script was tested as of October 2026 on Debian 13 (bash 5.2, curl 8.14.1) for these cases: healthy, HTTP 404, name resolution failure, connection refused, and missing keyword.

Installing curl

Most Linux distributions ship with curl. If curl --version does not print a version, install it.

# Debian / Ubuntu
sudo apt install curl

# RHEL / AlmaLinux / Rocky Linux
sudo dnf install curl

Try the command first

The heart of the script is the following curl command. It discards the page body and prints only the HTTP status.

curl -sS -o /dev/null -w '%{http_code}\n' --connect-timeout 5 -m 10 https://example.com/
Option Meaning
-s Hide the progress meter
-S Still show error messages even with -s
-o /dev/null Discard the response body
-w '%{http_code}\n' Print the HTTP status when the transfer finishes
--connect-timeout 5 Maximum time for connecting (seconds)
-m 10 Maximum time for the whole operation (seconds), so you do not wait forever when no response comes back

To see response times as well, add timing variables to -w.

curl -sS -o /dev/null -w 'code=%{http_code} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n' https://example.com/

The old version of this article used --head (a HEAD request), but some servers and plugins answer HEAD and GET differently. To check what visitors actually get, this article uses a normal GET.

Reading curl exit codes

When the connection fails, -w '%{http_code}' prints 000 and curl itself exits with a non-zero code. These exit codes help you narrow down the cause:

Exit code Meaning
0 The transfer succeeded (check the HTTP status separately)
6 Could not resolve the host name (DNS problems, expired domain, and so on)
7 Could not connect to the server (web server stopped, firewall, and so on)
22 With -f (--fail), the HTTP status was 400 or higher
28 Timed out
35 An error occurred in the TLS (SSL) handshake
60 The server certificate could not be verified (expired, misconfigured, and so on)

Create the monitoring script

Save the following as /usr/local/bin/site-check.sh.

#!/bin/bash
# Check a site with curl and notify only when it goes down and when it recovers
set -u

URL="${1:-https://example.com/}"   # URL to monitor (from the argument, otherwise this value)
EXPECT_CODE="200"                  # HTTP status treated as healthy
KEYWORD=""                         # Text the page should contain (empty = do not check)
TIMEOUT=10                         # Maximum seconds for one check
RETRIES=3                          # Consecutive failures before it counts as down
SLACK_WEBHOOK_URL=""               # Slack Incoming Webhook URL (empty = no notification)

# File that keeps the previous result per URL (results do not mix when you monitor several URLs)
STATE_FILE="/var/tmp/site-check.$(printf '%s' "$URL" | md5sum | cut -c1-12).state"

# If the previous check is still running, do nothing this time (prevents overlapping runs for the same URL)
exec 9> "$STATE_FILE.lock"
flock -n 9 || exit 0

log() {
  echo "$(date '+%F %T') $*"
}

notify() {
  log "$1"
  if [ -n "$SLACK_WEBHOOK_URL" ]; then
    curl -sS -m 10 -X POST -H 'Content-type: application/json' \
      --data "{\"text\":\"$1\"}" "$SLACK_WEBHOOK_URL" > /dev/null
  fi
}

check_once() {
  local body code rc
  body=$(mktemp)
  code=$(curl -sS -o "$body" -w '%{http_code}' --connect-timeout 5 -m "$TIMEOUT" "$URL" 2>/dev/null)
  rc=$?
  if [ "$rc" -ne 0 ]; then
    REASON="curl exit $rc"
  elif [ "$code" != "$EXPECT_CODE" ]; then
    REASON="HTTP $code"
  elif [ -n "$KEYWORD" ] && ! grep -qF -- "$KEYWORD" "$body"; then
    REASON="keyword not found"
  else
    REASON=""
  fi
  rm -f "$body"
  [ -z "$REASON" ]
}

status="NG"
for i in $(seq 1 "$RETRIES"); do
  if check_once; then
    status="OK"
    break
  fi
  log "retry $i/$RETRIES: $URL ($REASON)"
  [ "$i" -lt "$RETRIES" ] && sleep 5
done

prev=$(cat "$STATE_FILE" 2>/dev/null || echo "OK")
echo "$status" > "$STATE_FILE"

if [ "$status" = "NG" ] && [ "$prev" = "OK" ]; then
  notify "[DOWN] $URL ($REASON)"
elif [ "$status" = "OK" ] && [ "$prev" = "NG" ]; then
  notify "[RECOVERED] $URL"
fi

[ "$status" = "OK" ]

How the script works

  • check_once performs one check. It looks at the curl exit code, the HTTP status and, if KEYWORD is set, the text on the page, in that order, and puts the reason for any problem in REASON
  • On failure it waits 5 seconds and retries; after RETRIES consecutive failures the result is "NG". This reduces alerts caused by brief network glitches
  • It saves the previous result in STATE_FILE and sends [DOWN] only on a change from OK to NG, and [RECOVERED] only on a change from NG to OK
  • With flock, if the previous check of the same URL is still running, the script exits without doing anything. Even when run every minute, checks do not overlap
  • When the site is down the script exits with code 1, so other scripts can use the result

Set KEYWORD to text that appears on the healthy page (the site name, footer text and so on). This makes it easier to catch database connection error pages or defaced pages that still return status 200.

Sending alerts to Slack

To notify Slack, create an Incoming Webhook in Slack and put the issued URL in SLACK_WEBHOOK_URL. Anyone who knows the webhook URL can post to that channel, so make sure other users cannot read the script file.

The message is turned into JSON by wrapping the URL and the reason in ". If you monitor a URL that contains ", the JSON breaks, so you would need to change how the message is built.

Make it executable and test it

sudo chmod 700 /usr/local/bin/site-check.sh
sudo /usr/local/bin/site-check.sh https://example.com/
echo $?

If the site is healthy, nothing is printed and the exit code is 0. If you give it a page that does not exist, it prints the retry log and [DOWN] like this:

2026-10-29 17:18:18 retry 1/3: https://example.com/nonexistent-zzz (HTTP 404)
2026-10-29 17:18:23 retry 2/3: https://example.com/nonexistent-zzz (HTTP 404)
2026-10-29 17:18:28 retry 3/3: https://example.com/nonexistent-zzz (HTTP 404)
2026-10-29 17:18:28 [DOWN] https://example.com/nonexistent-zzz (HTTP 404)

If you run it again in the same state, the retry log appears but [DOWN] does not. When the URL is healthy again, [RECOVERED] is printed.

Run it every minute with cron

Open root's crontab with sudo crontab -e and add the following lines. Write one line per URL to monitor several sites.

* * * * * /usr/local/bin/site-check.sh https://example.com/ >> /var/log/site-check.log 2>&1
* * * * * /usr/local/bin/site-check.sh https://example.org/ >> /var/log/site-check.log 2>&1

With these settings one check can take up to about 45 seconds (a 10-second timeout × 3 attempts + 5-second waits × 2). Even if you raise TIMEOUT or RETRIES and a run goes past one minute, flock keeps checks of the same URL from overlapping.

Check the log like this:

tail -f /var/log/site-check.log

The log keeps growing, so add it to logrotate if you use the script long term.

Things to watch out for

  • Where the monitor runs: if the script runs on the same server it monitors, it cannot alert you when the server itself goes down or the network is cut. Monitor from another server, or combine it with an external monitoring service
  • Request frequency: requests every minute also show up in the monitored site's access log. If you want to exclude them from analytics, setting a recognizable User-Agent with curl's -A option makes them easy to tell apart
  • Redirects: if a site redirects from http:// to https:// and you monitor the http:// URL, the status will be 301 or similar. Specify the final URL, or add -L to curl to follow the redirect
  • Certificates: an expired certificate is detected through exit code 60 and similar, but this script cannot warn you in advance that a certificate is about to expire

Summary

  • Get the HTTP status with curl -sS -o /dev/null -w '%{http_code}', and cap the waiting time with --connect-timeout and -m
  • When the connection fails, use the curl exit code (6, 7, 28, 60 and so on) to narrow down the cause
  • Retry and remember the previous state so you are notified only when the site goes down and when it recovers
  • Prevent overlapping runs with flock and run the script every minute with cron
  • Monitoring from the same server cannot detect a full server outage, so combine it with another server or an external service

Related articles:

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲