Reported symptoms
- Since a change window six days ago, about half the application servers
cannot reach the API on
svc01. The other half are fine. - It is completely consistent. The same hosts fail every time; the same hosts succeed every time. Retrying never helps and never hurts.
- The failing hosts are spread across three subnets and two VLANs. Working hosts are in the same subnets and VLANs. There is no pattern the network team can find in their ACLs.
svc01itself looks perfectly healthy: the service is listening, the bond is UP, andcurlfromsvc01to some peers works.- The firewall team confirms no rule changed. The network team confirms no ACL changed. Everyone is confident it is not them.
Evidence provided
$ ip link show bond0
5: bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT
link/ether 52:54:00:a1:b2:c3 brd ff:ff:ff:ff:ff:ff
$ cat /proc/net/bonding/bond0
Ethernet Channel Bonding Driver: v6.1.0
Bonding Mode: load balancing (xor)
Transmit Hash Policy: layer2 (0)
MII Status: up
MII Polling Interval (ms): 0
Up Delay (ms): 0
Down Delay (ms): 0
Slave Interface: eth0
MII Status: up
Speed: 10000 Mbps
Duplex: full
Link Failure Count: 0
Permanent HW addr: 52:54:00:a1:b2:c3
Slave Interface: eth1
MII Status: up
Speed: 10000 Mbps
Duplex: full
Link Failure Count: 0
Permanent HW addr: 52:54:00:a1:b2:c4
$ sudo ethtool eth0 | grep 'Link detected'
Link detected: yes
$ sudo ethtool eth1 | grep 'Link detected'
Link detected: no
$ ip -s link show eth1
7: eth1: <BROADCAST,MULTICAST,SLAVE,UP> mtu 1500 qdisc mq master bond0 state DOWN mode DEFAULT
RX: bytes packets errors dropped missed mcast
4182993021 8811204 0 0 0 0
TX: bytes packets errors dropped carrier collsns
91827364512 74119883 0 0 74119883 0
$ sudo ethtool -S eth1 | grep -iE 'carrier|link'
tx_carrier_errors: 74119883
link_down_events: 1
$ sudo dmesg -T | grep eth1
[Wed Aug 5 02:14:07 2026] ixgbe 0000:04:00.1 eth1: NIC Link is Down
$ cat /sys/class/net/bond0/bonding/mode /sys/class/net/bond0/bonding/xmit_hash_policy /sys/class/net/bond0/bonding/miimon
balance-xor 2
layer2 0
0
Work the evidence before reading on
Four observations, each of which is individually unremarkable and together are the whole answer:
/proc/net/bonding/bond0saysMII Status: upforeth1.ethtool eth1saysLink detected: no. Both are printed by the kernel. Both cannot be right.Link Failure Count: 0on a slave whose driver loggedNIC Link is Downsix days ago.ip -s link show eth1shows TX packets in the tens of millions climbing right now, RX frozen, and a carrier-error count equal to the TX packet count.MII Polling Interval (ms): 0.
Before continuing, answer: who is supposed to notice that a slave has lost carrier, and how often does the file above say they check?
Root cause
1. The bonding driver only knows what it asks
The bond does not receive a notification when a slave loses carrier and
act on it. It polls, on a timer, and miimon is that timer in
milliseconds. There is a second mechanism — the ARP monitor, configured
with arp_interval and arp_ip_target — which probes a peer address
instead.
With miimon=0 and no arp_interval, neither runs. The driver performs
no link monitoring whatsoever. It reports each slave’s state as it was
when the slave was enslaved, and it schedules transmit across every slave
in that list forever.
The kernel’s own bonding documentation is unusually direct about this:
it is critical that either miimon, or arp_interval together with
arp_ip_target, is specified — otherwise serious network degradation
will occur when a link fails. This scenario is that sentence happening.
2. Link Failure Count: 0 is not reassurance
That counter increments when the bond observes a slave transition. The bond observed nothing, so it counted nothing. The failure is recorded elsewhere:
ethtool eth1— the driver’s live query of the PHY:Link detected: noethtool -S eth1—link_down_events: 1dmesg— one line, six days agoip -s link show eth1—carriererrors equal to TX packets, because every single frame handed to that NIC is discarded for want of carrier
Four independent sources say the link is down. The one file everybody looks at says it is up.
3. Why exactly half the peers, consistently
balance-xor with xmit_hash_policy layer2 selects the transmitting
slave by XOR-ing the source and destination MAC addresses and taking the
result modulo the number of slaves. With two slaves, that is a coin flip
per destination MAC — but a deterministic one. A given peer always
hashes to the same slave.
So the fleet splits cleanly and permanently:
- Peers hashing to
eth0: perfect service. - Peers hashing to
eth1: every transmitted frame discarded at the NIC.
Because the hash is on MAC addresses and all off-subnet traffic goes via the router’s MAC, the split falls along lines that have nothing to do with subnet, VLAN or firewall zone — which is precisely why the network team could find no pattern.
4. Why it looks like a firewall rule
Ingress works. The switch has its own view of the link, its port to
eth1 is down, and it therefore forwards every frame to the host over
the live port. Requests arrive normally.
Replies go out through the bond, get hashed, and half of them are dropped inside the host before ever reaching a cable.
The result is a one-way path — traffic in, nothing out — for a fixed set of peers, unchanging over days. That is the exact signature of an ACL, and it is what everyone reasonably concluded.
Resolution
- Confirm the contradiction explicitly before changing anything, so the fix is aimed at the right thing. Compare
/proc/net/bonding/bond0againstethtoolfor every slave - Restore service by removing the dead slave. With one slave in the bond, every flow hashes to the survivor and the failing half recovers immediately:
- ``
echo -eth1 | sudo tee /sys/class/net/bond0/bonding/slaves`` - Enable link monitoring at runtime. This is the actual fix and it takes effect immediately:
- ``
echo 100 | sudo tee /sys/class/net/bond0/bonding/miimon cat /proc/net/bonding/bond0 | grep -i polling`` - Persist it. A sysfs write does not survive a reboot, and a bond that regresses to
miimon=0at the next maintenance window produces an incident nobody connects to this one. On netplan: - ``
network: bonds: bond0: interfaces: [eth0, eth1] parameters: mode: balance-xor mii-monitor-interval: 100`` - Or with NetworkManager:
- ``
sudo nmcli connection modify bond0 bond.options "mode=balance-xor,miimon=100"`` - Apply and re-read the live state, not the file you edited.
sudo netplan applyorsudo nmcli connection up bond0, then confirm the polling interval in/proc/net/bonding/bond0 - Fix the physical fault. Carrier down for six days is a cable, an optic, or a switch port — not a Linux problem. Get it repaired rather than working around it
- Re-enslave eth1 once the link is genuinely up, and confirm the bond now reports both slaves correctly:
- ``
echo +eth1 | sudo tee /sys/class/net/bond0/bonding/slaves`` - Audit every other bonded host in the fleet. This configuration came from a template, and templates are used more than once
Verification
- Monitoring is on.
grep -i "polling interval" /proc/net/bonding/bond0reports a non-zero value - The bond agrees with the hardware. For each slave, the
MII Statusin/proc/net/bonding/bond0matchessudo ethtool ethN | grep "Link detected". They disagreed for six days; agreement is the whole point - The bond actually detects a failure. This is the verification that can fail, and it is the only one that proves anything:
- ``
sudo ip link set eth1 down sleep 1 grep -A2 "Slave Interface: eth1" /proc/net/bonding/bond0 sudo ip link set eth1 up`` - The bond reports the transition.
sudo dmesg -T | tailshows a bonding line naming the slave, andLink Failure Countfor that slave has incremented. A count that stays at 0 through a deliberate down means monitoring is still not running - Traffic survives the failure. Run a continuous
pingfrom a peer that previously failed while you down the slave; it should continue uninterrupted or lose at most a packet or two - The previously broken peers work. Test from at least three of the hosts that were failing, not just one — with a hash-based mode, a single successful test may just be a peer that always hashed to the good slave
- It survives a reboot. Reboot the host, or re-apply the network configuration from scratch, and re-check the polling interval. This is where a runtime-only fix is exposed
- The fleet is clean. Confirm every bonded host reports a non-zero polling interval, rather than assuming the template was only used here
Prevention
- Assert
miimon(or the ARP monitor) on every bond, everywhere, as a configuration-management check. A bond without a monitor is not redundant hardware — it is two NICs sharing a hash function, and it fails worse than a single NIC would. - Monitor per-slave carrier, not the bond’s UP flag.
/sys/class/net/<slave>/carrierand theLink Failure Countper slave are the signals that matter; the master’s operstate is not. - Alert on a slave count below the expected number. This host ran for six days at half capacity with nothing firing.
- Alert on
tx_carrier_errorsfromethtool -S. A counter climbing by millions per day is a NIC transmitting into nothing, and it is unambiguous. - Drill failover. Down a slave on a schedule in a maintenance window, confirm the bond reacts, bring it back. Redundancy that has never been exercised is a hypothesis.
- Treat a “half the clients fail, consistently, with no ACL pattern” report as a load-balancing hash symptom until proven otherwise. Bonds, ECMP routes and LAGs all produce it, and it never looks like the network device it actually is.