You’ve got a server with two, four, maybe six NICs. Right now, one of them is plugged in and doing all the work. The rest are dark, sitting in a chassis that cost real money. That’s embarrassing.
Linux has had rock-solid bonding support since the 2.4 kernel era. It’s mature, well-documented in theory, and absolutely brutal in practice when you misconfigure the switch side and spend three hours wondering why traffic drops to zero the moment you enable LACP. This article skips the fluff and shows you working recipes — two of them, for the two modes that actually matter in production: 802.3ad LACP and active-backup. Both include bridge integration for KVM hypervisors.
We’ll use systemd-networkd for the configs because it’s what ships with modern Debian/Ubuntu and doesn’t require installing anything extra. I’ll note where Netplan equivalents diverge.
Know Your Modes Before You Touch the Switch
The Linux bonding driver supports seven modes (0–6). You only need to care about two in most environments:
Mode 1 — active-backup: One slave carries traffic. If it goes down, another slave takes over. The switch needs zero special config — it just sees one MAC address. Recovery is seamless, latency spike is ~1–2 seconds. Bandwidth is capped at a single link. Perfect for edge servers, remote sites, or anywhere you don’t control the switch.
Mode 4 — 802.3ad (LACP): The switch and the Linux host negotiate link aggregation using LACP frames. Both links carry traffic. The kernel hashes flows across slaves. Bandwidth scales (with caveats). Requires the switch to have LACP enabled on the port-channel. If LACP isn’t running on both sides, you get one link that works and one that does nothing — silently.
There’s also mode 2 (balance-xor) and mode 6 (balance-alb) which don’t need switch cooperation but have narrower use cases. Mode 0 (balance-rr) round-robins packets and breaks TCP because it delivers out of order — avoid it unless your application is UDP bulk transfer on a dumb switch.
Prerequisites
- Linux kernel 3.10+ (anything modern is fine)
ifenslavepackage on Debian-based systems:apt install ifenslave- Switch ports configured for LACP (mode 4 only)
- Root access
- Two or more physical NICs — let’s call them
enp2s0andenp3s0
Identify your NICs first:
ip link show
# or for hardware details:
lshw -class network -short
Write down the actual interface names. Don’t assume eth0 exists — modern kernels use predictable naming and your names will differ.
Recipe 1: LACP (802.3ad) with a Linux Bridge
This is the KVM hypervisor setup: two NICs bonded via LACP, a bridge on top of that bond, VMs connect to the bridge. Maximum throughput on the host side, redundancy on the wire.
Switch Side First
Before touching Linux, go configure the switch. On Cisco IOS:
interface range GigabitEthernet0/1 - 2
channel-group 1 mode active
channel-protocol lacp
!
interface Port-channel1
switchport mode access
switchport access vlan 100
On a Juniper EX series:
set interfaces ae0 aggregated-ether-options lacp active
set interfaces ge-0/0/0 ether-options 802.3ad ae0
set interfaces ge-0/0/1 ether-options 802.3ad ae0
The Linux side uses mode active by default — it sends LACP frames and expects the switch to respond. If you set the switch to mode passive, you’ll need at least one side active, and Linux defaults to active, so you’re fine.
systemd-networkd Configuration
Step 1 — enslave the physical NICs
Create /etc/systemd/network/10-bond-slave-1.network:
[Match]
Name=enp2s0
[Network]
Bond=bond0
Create /etc/systemd/network/11-bond-slave-2.network:
[Match]
Name=enp3s0
[Network]
Bond=bond0
Step 2 — define the bond device
Create /etc/systemd/network/20-bond0.netdev:
[NetDev]
Name=bond0
Kind=bond
[Bond]
Mode=802.3ad
TransmitHashPolicy=layer3+4
LACPTransmitRate=fast
MIIMonitorSec=100ms
UpDelaySec=200ms
DownDelaySec=200ms
TransmitHashPolicy=layer3+4 hashes on IP+port pairs, which gives much better flow distribution than the default layer2. Without this, all traffic between the same pair of IPs goes down one slave regardless of how many links you have.
LACPTransmitRate=fast makes LACP PDUs send every second instead of every 30 seconds. You’ll detect link failures in ~3 seconds instead of ~90 seconds.
Create /etc/systemd/network/21-bond0.network:
[Match]
Name=bond0
[Network]
Bridge=br0
# No IP here — the bridge gets the IP
Step 3 — define the bridge
Create /etc/systemd/network/30-br0.netdev:
[NetDev]
Name=br0
Kind=bridge
[Bridge]
STP=yes
ForwardDelaySec=4
STP is worth having even on a simple two-port bridge. Without it, a VM with a misconfigured bridge port can cause a loop.
Create /etc/systemd/network/31-br0.network:
[Match]
Name=br0
[Network]
Address=192.168.1.10/24
Gateway=192.168.1.1
DNS=1.1.1.1
[Link]
MTUBytes=9000
MTU 9000 only if your entire path supports jumbo frames — switch ports, upstream routers, all of it. If you’re not sure, leave it at 1500.
Step 4 — apply
systemctl restart systemd-networkd
# Check status:
networkctl status bond0
networkctl status br0
Verify LACP is Actually Negotiated
cat /proc/net/bonding/bond0
You want to see this under each slave:
MII Status: up
Link Failure Count: 0
Permanent HW addr: ...
Slave queue ID: 0
Aggregator ID: 1
Actor Churn State: none
Partner Churn State: none
Actor Churned Count: 0
Partner Churned Count: 0
If you see Partner Churned State: churning — the switch isn’t responding to LACP frames. Double-check the switch config. If MII Status is up on both but only one is in the aggregation — the LACP system priority or key mismatches; check Aggregator ID values.
Recipe 2: Active-Backup with a Bridge
No switch cooperation required. Works on consumer switches, cloud VMs with SR-IOV, or anywhere you just want failover without the LACP negotiation dance.
systemd-networkd Configuration
Slave configs — same as above, just point them at bond0.
/etc/systemd/network/10-bond-slave-1.network and 11-bond-slave-2.network — identical to Recipe 1.
Bond device — /etc/systemd/network/20-bond0.netdev:
[NetDev]
Name=bond0
Kind=bond
[Bond]
Mode=active-backup
PrimaryReselectPolicy=always
MIIMonitorSec=100ms
UpDelaySec=200ms
DownDelaySec=200ms
PrimaryReselectPolicy=always makes the bond return to the primary slave as soon as it comes back up. Without this, the bond might stay on the backup even after the primary recovers, which matters if the primary has a faster link or is on a preferred switch.
To set a preferred primary slave, add:
PrimarySlave=enp2s0
Bond network and bridge configs — identical to Recipe 1.
Verify Failover Works
Check current active slave:
cat /proc/net/bonding/bond0 | grep "Currently Active Slave"
Force a failover by bringing down the active interface:
ip link set enp2s0 down
cat /proc/net/bonding/bond0 | grep "Currently Active Slave"
# Should show enp3s0 now
ip link set enp2s0 up
# With PrimaryReselectPolicy=always, it will flip back automatically
Measure the actual failover time with a continuous ping:
ping -i 0.1 192.168.1.1
You should see at most 2–3 dropped packets with MIIMonitorSec=100ms. If you see 20+ drops, your UpDelaySec/DownDelaySec is too high, or the kernel is waiting for ARP convergence.
Gotchas That Will Wreck Your Day
LACP and the switch decide your bond size, not you. If the switch puts your ports into different LACP aggregation groups (wrong key, different speeds, different duplex), you’ll get two bonds of one slave each. Both will appear up and bonded to the driver, but LACP negotiation failed. Only cat /proc/net/bonding/bond0 will tell you the truth.
The MAC address matters for the bridge. When you put a bond under a bridge, the bridge inherits the bond’s MAC. The bond takes the MAC of its first-enslaved slave. If slaves join in a different order after a reboot (and they will, because systemd starts units in parallel), the bridge MAC changes, your DHCP reservation breaks, and your firewall rules based on MAC go dark. Fix: set a stable MAC on the bond explicitly.
In your bond0.netdev:
[NetDev]
Name=bond0
Kind=bond
MACAddress=52:54:00:ab:cd:ef
[Bond]
...
Pick a locally administered MAC (second nibble must be 2, 6, A, or E) that you own.
VLAN offload and bonding don’t always mix. Some drivers disable hardware VLAN offload when enslaved. Check with ethtool -k enp2s0 | grep vlan. If you’re using 802.1q VLANs on top of the bond, unexpected CPU spikes under load can mean offload silently disabled. Add this to your bond netdev:
[NetDev]
...
[Link]
ReceiveChecksumOffload=yes
TransmitChecksumOffload=yes
GenericSegmentationOffload=yes
KVM and the bridge: don’t set hairpin carelessly. If you enable hairpin mode on bridge ports without understanding what it does, VMs on the same host will loop frames back through the bridge rather than taking the fast local path. Leave hairpin off unless you’re doing specific VLAN tricks.
Don’t run DHCP on the bond interface if the bridge exists. Exactly one layer gets an IP. If it’s a plain bonded server (no VMs), the bond gets the IP. If it’s a hypervisor with a bridge, the bridge gets the IP and the bond has no IP. Running DHCP on both will give you split-brain routing and intermittent reachability that’s miserable to debug.
balance-alb and balance-tlb break with bridges. Modes 5 and 6 use ARP manipulation that doesn’t work when the bond is under a bridge because the bridge terminates ARP before it reaches the bonding driver. Only use mode 1 (active-backup) or mode 4 (802.3ad) with bridges. Modes 2 and 3 technically work but aren’t worth the trouble.
Production-Ready Additions
VLAN-aware bridge for KVM
If you want VMs in different VLANs, make the bridge VLAN-aware instead of creating separate bridges per VLAN. Add to br0.netdev:
[Bridge]
VLANAware=yes
DefaultPVID=1
Then set the VLAN on each VM tap interface via libvirt or directly:
bridge vlan add vid 200 dev vnet0
This scales dramatically better than 8 separate bridges for 8 VLANs.
Monitoring with a simple script
Stick this in cron or a systemd timer — it pages you when the active slave changes or a slave drops:
#!/bin/bash
# /usr/local/bin/bond-check.sh
BOND=/proc/net/bonding/bond0
ALERT_EMAIL="[email protected]"
active=$(grep "Currently Active Slave" $BOND | awk '{print $NF}')
up_count=$(grep -c "MII Status: up" $BOND)
total=$(grep -c "Slave Interface" $BOND)
if [[ "$up_count" -lt "$total" ]]; then
echo "Bond degraded: $up_count/$total slaves up. Active: $active" \
| mail -s "ALERT: bond0 degraded on $(hostname)" "$ALERT_EMAIL"
fi
Jumbo frames — all or nothing
If you enable jumbo frames, you must set MTU=9000 consistently on:
- The physical NICs (via
[Link] MTUBytes=9000in their.networkfiles) - The bond
- The bridge
- Every VM’s virtio NIC
- The switch ports end-to-end
Missing any one layer causes fragmentation or silent drops. Verify with:
ping -M do -s 8972 192.168.1.1
# 8972 + 28 bytes header = 9000
If that drops and regular pings work, you have a jumbo frame gap somewhere in the path.
ethtool persistent settings
Some NICs revert wake-on-LAN or pause frames after enslavement. Force them off to prevent the NIC from buffering under load instead of dropping:
# In /etc/systemd/network/10-bond-slave-1.network
[Link]
WakeOnLan=off
For pause frames, you still need an ethtool hook or a post-up script — systemd-networkd doesn’t expose this directly yet:
ethtool -A enp2s0 rx off tx off
Quick Reference: Check Everything at Once
# Full bond status
cat /proc/net/bonding/bond0
# Bridge forwarding table
bridge fwd show br0
# Which VMs are on which bridge port
bridge link show
# Is LACP negotiated? (look for "LACP rate: fast" and both slaves "in sync")
cat /proc/net/bonding/bond0 | grep -A5 "Slave Interface"
# Live traffic per slave
watch -n1 'cat /proc/net/dev | grep "enp[23]"'
# networkctl gives you a clean summary
networkctl status bond0 br0
Choosing Between the Two
Pick active-backup when: you don’t control the switch, you’re on a cloud host with SRIOV, you want dead-simple failover with zero negotiation risk, or you’re setting up a remote server you can’t afford to misconfigure.
Pick LACP when: you control the switch, you need actual aggregate throughput (remember: a single TCP flow still uses one slave regardless of mode), and you’re willing to do the switch-side config properly.
Both modes with a bridge are production-proven setups for KVM hypervisors. The config files above are the same ones running on real hardware right now. The only thing between you and a working setup is getting the interface names right and — for LACP — actually configuring the switch before you touch Linux.