
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.
Before you run this
This is a read-only monitoring script. It parses the kernel's own bonding status file under /proc/net/bonding/, and if the bond or any of its slave interfaces reports a non-up MII status it writes to the system log and, optionally, sends an email. It does not change your bonding configuration, bring interfaces up or down, or touch NetworkManager. Nothing here is destructive to your network config.
Privileges: run the check as root via a systemd service. The /proc/net/bonding/* files are readable, but running as root keeps logging behaviour consistent and avoids surprises across distros. The script writes only to syslog and (if you configure it) sends mail.
Test first: read the script, then run it by hand on a non-production host — a lab box or a spare server with a bond configured — before you install the timer anywhere that matters. When you later want to prove the alert path works, the honest way to do it is to down one slave on a lab machine (covered at the end); do not rehearse a failure on a production bond, and never on the last healthy link.
Assumptions, held throughout:
- RHEL 9 / AlmaLinux 9 / Rocky 9, with
systemdandbash. - A bond already configured and named
bond0(mode active-backup or 802.3ad — the check works the same for both). - Bond status is exposed at
/proc/net/bonding/bond0, which is kernel-provided and identical on Debian/Ubuntu. The only real cross-distro difference is the mail client package name, noted below.
The status the kernel already gives you
Before automating, look at what you're parsing. As root:
cat /proc/net/bonding/bond0
You'll see a header block for the bond, then one block per slave. The lines that matter:
MII Status: up # this first one is the bond overall
...
Slave Interface: eth0
MII Status: up
...
Slave Interface: eth1
MII Status: down # a dead slave looks like this
Every check below is built on two literal strings the kernel prints: MII Status: and Slave Interface:. That's it — no invented fields.
The check script
Save this as /usr/local/sbin/bond-check.sh and chmod 750 it:
#!/usr/bin/env bash
set -euo pipefail
BOND="${1:-bond0}" # bond to check; pass another name as arg 1
PROC="/proc/net/bonding/${BOND}"
HOST="$(hostname -s)"
ALERT_EMAIL="${ALERT_EMAIL:-}" # set via the service unit; empty = log only
if [[ ! -r "$PROC" ]]; then
logger -t bond-check -p daemon.err "Bond ${BOND} not found at ${PROC}"
echo "ERROR: ${PROC} not found or not readable" >&2
exit 2
fi
# The FIRST "MII Status" line in the file is the bond's overall status.
bond_status="$(awk '/^MII Status:/ {print $3; exit}' "$PROC")"
# Walk each "Slave Interface:" block and pair it with its own MII Status line.
down_slaves=()
while read -r iface status; do
[[ "$status" == "up" ]] || down_slaves+=("$iface")
done < <(awk '
/^Slave Interface:/ { iface=$3 }
/^MII Status:/ && iface { print iface, $3; iface="" }
' "$PROC")
if [[ "$bond_status" != "up" || ${#down_slaves[@]} -gt 0 ]]; then
msg="Bond ${BOND} on ${HOST}: overall MII=${bond_status}"
(( ${#down_slaves[@]} > 0 )) && msg+="; DOWN slaves: ${down_slaves[*]}"
logger -t bond-check -p daemon.err "$msg"
if [[ -n "$ALERT_EMAIL" ]] && command -v mail >/dev/null 2>&1; then
printf '%s\n' "$msg" | mail -s "[bond alert] ${BOND} degraded on ${HOST}" "$ALERT_EMAIL"
fi
exit 1
fi
logger -t bond-check -p daemon.info "Bond ${BOND} healthy (all slaves up)"
exit 0
Two things worth calling out. The awk that lists slaves guards on && iface, so the bond's own leading MII Status line — which appears before any Slave Interface: — is never mistaken for a slave. And exit codes are meaningful: 0 healthy, 1 degraded, 2 bond missing.
Email is optional. The script only mails if you set ALERT_EMAIL and a mail command exists. On RHEL/Alma that's the mailx package (dnf install mailx), which provides mail. On Debian/Ubuntu it's bsd-mailx or mailutils. Configuring a working relay is out of scope here — if mail isn't set up, the script still logs, and you can wire the log to whatever you already use (rsyslog forwarding, journald, etc.).
Put it on a timer
Rather than cron, I use a systemd oneshot service plus a timer — it keeps the log tidy under journalctl -u and survives reboots cleanly.
/etc/systemd/system/bond-check.service:
[Unit]
Description=Check Linux bonding interface health
[Service]
Type=oneshot
# Optional: uncomment and set a real recipient to enable email alerts
#Environment=ALERT_EMAIL=alerts@example.com
ExecStart=/usr/local/sbin/bond-check.sh bond0
/etc/systemd/system/bond-check.timer:
[Unit]
Description=Run bond health check periodically
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min # check every 5 minutes after the last run
Unit=bond-check.service
[Install]
WantedBy=timers.target
Replace bond0 with your bond name and alerts@example.com with a real recipient. Then load and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now bond-check.timer
Verify it works
First, run the check by hand and read the output and exit code:
sudo /usr/local/sbin/bond-check.sh bond0 ; echo "exit=$?"
A healthy bond gives exit=0 and an info line in the log. Confirm the log entry:
journalctl -t bond-check --since "5 min ago"
Confirm the timer is scheduled and see when it next fires:
systemctl status bond-check.timer
systemctl list-timers bond-check.timer
To prove the alert path actually fires — on a lab machine only, and only on a bond that still has a second, healthy slave — down one slave and re-run the check:
sudo ip link set eth1 down # eth1 = a slave of your test bond
sudo /usr/local/sbin/bond-check.sh bond0 ; echo "exit=$?"
You should get exit=1, an error line in journalctl -t bond-check, and (if configured) the email. This is reversible — bring the slave back:
sudo ip link set eth1 up
Give it a few seconds, re-check /proc/net/bonding/bond0, and the next scheduled run should log healthy again. If the interface was managed by NetworkManager and doesn't rejoin cleanly, nmcli device connect eth1 (or a reboot of the lab box) restores it — but again, don't rehearse this on production.
Undoing it
Nothing here modifies your network config, so there's no bond state to revert. To remove the monitoring entirely:
sudo systemctl disable --now bond-check.timer
sudo rm /etc/systemd/system/bond-check.timer \
/etc/systemd/system/bond-check.service \
/usr/local/sbin/bond-check.sh
sudo systemctl daemon-reload
Where to take it next
This is the deliberately boring, standalone approach — a script and a timer, no dependencies beyond what's on the box. If you already run Prometheus, node_exporter exposes bonding metrics (interface up/down state per slave) and you can alert on those in Alertmanager instead of by email; that's the better fit once you have a metrics stack, and I'd point you to the node_exporter documentation for the exact metric names rather than guess them here. For the bonding driver's own options and the meaning of each field in /proc/net/bonding/, the canonical reference is the kernel's Linux Ethernet Bonding Driver HOWTO in the kernel documentation.
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.
