Shore Up
A night watchman on a tower flashing a signal lamp at several distant lighthouses one by one, a stopwatch in hand, ticking each one off on a clipboard as it flashes back.
Linux

Automate a Scheduled Reachability and Speed Test to Key Hosts

Ketan Aagja10 min read
No ratings yet

Before you run this

This guide builds a small bash script that pings a fixed list of hosts on a schedule, records latency and packet loss to a CSV file, and optionally measures throughput to a host you control. It is read-only from a system-configuration standpoint: it changes no firewall rule, route, or setting. It only sends test traffic and appends lines to a log file.

The one thing it does do that deserves care is generate traffic. A ping every few minutes is trivial, but a throughput test (iperf3 or a speedtest) can saturate a link for the duration of the run and consume real data on a metered or capped WAN. Do not schedule a full-throughput test every few minutes across a production circuit during business hours, and do not run one blind on a link you are billed for by the gigabyte.

Privileges: you do not need root to run this. On modern Linux ping works as an unprivileged user, and the iperf3 client needs no elevation. You only need sudo once to install the packages and to create the log directory under /var/log. Run the scheduled job as a normal user, not root.

Test first: read the script, then run it by hand against a single host and confirm the CSV line looks right before you add it to cron. Point it at a lab host or your own gateway first, not at a customer's box.

Nothing here is destructive and there is nothing irreversible — undoing it is deleting the cron entry and the files, covered at the end.

Assumptions

I'm writing this for Ubuntu 22.04 LTS, bash, and the classic user crontab as the scheduler. If you prefer systemd timers, that path exists (systemctl --user with a .timer and .service unit); I'm using cron because it's the boring, universal option. On RHEL/Alma the package names differ (iputils instead of iputils-ping) and cron is provided by cronie — otherwise the script is identical.

Install the tools

# ping is almost always already present; iperf3 and jq are for the optional speed test
sudo apt update
sudo apt install iputils-ping iperf3 jq

For an internet-facing speed test rather than host-to-host throughput, the official Ookla speedtest CLI is the standard choice. It installs from Ookla's own APT repository — follow the install steps on Ookla's official Speedtest CLI page rather than the speedtest-cli Python package, which is a different, unofficial tool. I'll keep the main script on ping + iperf3 and show where the speedtest slots in.

Create the log directory

Do this once with sudo so the script (running as your normal user) can write to it:

sudo mkdir -p /var/log/nettest
sudo chown "$USER" /var/log/nettest   # let the scheduling user write here

The script

Save this as /usr/local/bin/nettest.sh and chmod +x it. Replace the hosts in the HOSTS array with your own — internal servers, the default gateway, an upstream resolver, whatever "key" means for you.

#!/usr/bin/env bash
set -euo pipefail

# nettest.sh — scheduled reachability + latency check to a fixed host list.
# Read-only: it sends test traffic and appends to a CSV. It changes no config.

# --- configuration -------------------------------------------------------
HOSTS=("192.0.2.10" "gateway.example.com" "8.8.8.8")  # replace with YOUR hosts
PING_COUNT=5            # ICMP echoes per host
PING_TIMEOUT=2          # seconds to wait for each reply
LOGDIR="/var/log/nettest"
CSV="${LOGDIR}/reachability.csv"
# -------------------------------------------------------------------------

# write the header once, on first run
if [[ ! -f "$CSV" ]]; then
  echo "timestamp,host,loss_pct,rtt_avg_ms,status" >> "$CSV"
fi

ts="$(date --iso-8601=seconds)"

for host in "${HOSTS[@]}"; do
  # -c count, -W per-reply timeout in seconds (iputils ping on Linux)
  if out="$(ping -c "$PING_COUNT" -W "$PING_TIMEOUT" "$host" 2>/dev/null)"; then
    # e.g. "0% packet loss"  ->  0
    loss="$(printf '%s\n' "$out" | grep -oE '[0-9]+% packet loss' | grep -oE '[0-9]+' || true)"
    # summary line "rtt min/avg/max/mdev = 14.2/15.1/16.0/0.6 ms"; avg is field 5 when split on /
    rtt="$(printf '%s\n' "$out" | awk -F'/' '/rtt|round-trip/ {print $5}' || true)"
    echo "${ts},${host},${loss:-NA},${rtt:-NA},up" >> "$CSV"
  else
    # ping exits non-zero when the host is unreachable
    echo "${ts},${host},100,NA,down" >> "$CSV"
  fi
done

A note on the parsing: iputils ping prints its summary as either rtt min/avg/max/mdev = ... or round-trip min/avg/max = ... depending on version, which is why the awk pattern matches both. If your locale changes the wording of "packet loss", the loss field will fall back to NA — the CSV row is still written, so you never lose a data point.

Add the optional throughput test

Throughput to a specific host means running iperf3 -s on that host and pointing the client at it. This measures the path between the two machines, which is usually what you want for "is the link to the branch office slow", not "is the internet slow".

# Run on the far end first:   iperf3 -s
# Then on the tester (append this block to nettest.sh, adjusting the target):

TPUT_CSV="${LOGDIR}/throughput.csv"
[[ -f "$TPUT_CSV" ]] || echo "timestamp,host,mbps" >> "$TPUT_CSV"

# -t 10 = ten-second test; -J = JSON output for reliable parsing with jq
result="$(iperf3 -c iperf.example.com -t 10 -J 2>/dev/null || true)"
mbps="$(printf '%s' "$result" | jq -r '.end.sum_received.bits_per_second / 1000000' 2>/dev/null || echo NA)"
echo "$(date --iso-8601=seconds),iperf.example.com,${mbps}" >> "$TPUT_CSV"

The jq path .end.sum_received.bits_per_second is the received rate from iperf3's JSON output. Confirm the exact field against iperf3's own JSON if you extend this — the schema is documented in the iperf3 manual.

For an internet speed measurement instead, swap that block for the Ookla CLI. The first invocation needs a one-time licence acceptance; check the exact flags on Ookla's Speedtest CLI documentation before you script it, because they gate the non-interactive run and I don't want you guessing them into a cron job.

Schedule it with cron

Edit your user crontab (crontab -e) and add:

# reachability + latency every 15 minutes
*/15 * * * * /usr/local/bin/nettest.sh >> /var/log/nettest/cron.log 2>&1

Keep the frequent schedule for the ping-based reachability check. If you run a throughput test, put it on a much slower cadence — hourly at most, and preferably outside business hours — as its own line so it doesn't fire every 15 minutes:

# example: throughput once an hour, on the hour (only if you added that block or a second script)
0 * * * * /usr/local/bin/nettest-throughput.sh >> /var/log/nettest/cron.log 2>&1

Cron runs with a minimal PATH, so the absolute path to the script matters. The tools it calls (ping, iperf3, jq, date, awk) all live in standard system directories that cron's default PATH includes.

Verify it worked

Run it by hand once and look at the output:

/usr/local/bin/nettest.sh
column -s, -t < /var/log/nettest/reachability.csv | tail

You should see one row per host per run, with a numeric loss percentage and an average RTT for reachable hosts, and status=down for anything that didn't answer.

Confirm cron is actually firing after the first scheduled interval:

tail -f /var/log/nettest/cron.log      # should stay empty on success (script prints nothing)
grep CRON /var/log/syslog | tail       # shows cron dispatching the job

An empty cron.log is the good case here — the script is quiet on success, so anything appearing in it is an error worth reading.

Undo it

Nothing here modifies the system, so removal is clean:

crontab -e                              # delete the nettest lines, save
sudo rm -f /usr/local/bin/nettest.sh
sudo rm -rf /var/log/nettest            # this deletes your collected history — export the CSV first if you want it

If you only want to pause collection, comment out the crontab lines rather than deleting anything. The packages (iperf3, jq) can stay installed harmlessly, or come off with sudo apt remove if you'd rather.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.

Automate Cron Job Monitoring So a Silent Failure Pages You

This guide sets up a dead man's switch for a cron job: the job pings an external monitor every time it runs, and if that ping is late or reports a non-zero exit, the monitor pages you. It exists to catch the failure mode plain cron misses entirely — a job that silently never ran , or ran and failed on a box whose mail is broken.

8 min read

Automate Linux Network Bonding Health Checks and Alerts

A bond only earns its keep when a NIC or switch port dies and nobody notices — because the traffic kept flowing on the surviving link. The failure mode I care about is the silent one: you lose a slave, run degraded for three weeks, then lose the second one and take an outage that looks instant but was really two failures spread over a month. This guide sets up a small, read-only script on a systemd timer that watches your bond and shouts when a slave goes down.

9 min read

Build a Service Health Dashboard with Cron and Static HTML

This guide builds a small read-only status page. A bash script checks whether a list of systemd services are active and whether a few HTTP endpoints answer, then writes a plain HTML file that your web server already serves. Cron re-runs it every few minutes so the page stays current. Nothing here restarts, reconfigures, or stops any service — it only reads state and writes one HTML file.

10 min read

Automate SSL Certificate Expiry Monitoring Across Many Domains

This guide builds a small bash script that connects to a list of hostnames over TLS, reads the expiry date from each certificate, and emails you a summary of anything expiring within a threshold you choose (30 days by default). It is a read-only monitor : it makes outbound TLS connections and sends mail. It does not touch, renew, or modify any certificate, and it changes nothing on the servers it checks.

10 min read