Configuring ECMP Routing on Linux: Distributing Traffic Across Multiple Network Paths with ip route

Network tutorial - IT technology blog
Network tutorial - IT technology blog

I once ran into a situation where a production server was hitting a network bottleneck — CPU utilization was fine, disk IO was stable, but throughput was capped at 800Mbps despite having 2 NICs connected to 2 separate uplinks. The root cause: both links were active, but only one was actually carrying traffic while the other sat idle waiting for failover. That’s when I turned to ECMP.

Why a Single Default Route Isn’t Enough

When Linux has multiple uplinks, the system defaults to using only a single default route. The remaining route acts as a backup, only activated when the primary link fails. This leads to 3 real-world problems:

  • Bandwidth is limited to a single path — even with 2×1Gbps links, you can only use 1Gbps
  • When the primary link fails, it takes time for the system to detect the failure and switch to the backup link
  • Traffic patterns are uneven — some flows get congested while the other interface sits completely idle

ECMP (Equal-Cost Multi-Path) solves all 3 problems by letting the kernel distribute connections across multiple paths simultaneously, based on a hash of the packet header.

How ECMP Works on the Linux Kernel

Instead of selecting a single nexthop, the kernel computes a hash from the tuple (src IP, dst IP, src port, dst port, protocol) of each packet to determine the path. The same TCP connection always goes through the same nexthop — this is critical because if packets within the same connection take different paths, it causes out-of-order delivery and severely degrades throughput.

The default hash policy from kernel 3.6+ uses L3 (IP only). From kernel 4.4+, you can enable L4 hashing (IP + port) for more even distribution when there are many connections to the same destination.

Prerequisites: Check Kernel Version and Configure Hash Policy

The topology used in this article:

  • Server: 192.168.1.100 (eth0) and 192.168.2.100 (eth1)
  • Gateway ISP A: 192.168.1.1 via interface eth0
  • Gateway ISP B: 192.168.2.1 via interface eth1

When designing subnets for a lab, I often use toolcraft.app/en/tools/developer/ip-subnet-calculator — enter a CIDR and instantly get the network range, broadcast address, and available host count, no manual calculation needed.

# Check kernel version (requires >= 3.6)
uname -r

# View current hash policy (0 = L3, 1 = L4)
sysctl net.ipv4.fib_multipath_hash_policy

# Enable L4 hashing — better distribution, especially with many connections to the same IP
sysctl -w net.ipv4.fib_multipath_hash_policy=1

# Persist across reboots
echo "net.ipv4.fib_multipath_hash_policy=1" > /etc/sysctl.d/99-ecmp.conf
sysctl -p /etc/sysctl.d/99-ecmp.conf

Configuring ECMP with ip route

Adding a Basic ECMP Route

The nexthop syntax allows you to declare multiple paths in a single command:

# View current routes
ip route show

# Delete old default route
ip route del default

# Add ECMP route with 2 equal-weight nexthops
ip route add default \
  nexthop via 192.168.1.1 dev eth0 weight 1 \
  nexthop via 192.168.2.1 dev eth1 weight 1

Adjusting Weights Based on Actual Bandwidth

If ISP A has 100Mbps and ISP B has 200Mbps, use a 1:2 weight ratio to distribute traffic proportionally:

# Weight ratio 1:2 — ISP B receives twice the traffic of ISP A
ip route add default \
  nexthop via 192.168.1.1 dev eth0 weight 1 \
  nexthop via 192.168.2.1 dev eth1 weight 2

Persistent Configuration with Netplan (Ubuntu)

ip route commands are lost on reboot. On Ubuntu, use Netplan:

# /etc/netplan/01-ecmp.yaml
network:
  version: 2
  ethernets:
    eth0:
      addresses:
        - 192.168.1.100/24
    eth1:
      addresses:
        - 192.168.2.100/24
  routes:
    - to: default
      via: 192.168.1.1
      metric: 100
    - to: default
      via: 192.168.2.1
      metric: 100
netplan apply

Note: Netplan does not support the weight syntax directly — it creates 2 routes with the same metric and the kernel handles them as ECMP. If you need different weights, use a startup script:

#!/bin/bash
# /etc/rc.local or systemd ExecStartPost
ip route del default 2>/dev/null || true
ip route add default \
  nexthop via 192.168.1.1 dev eth0 weight 1 \
  nexthop via 192.168.2.1 dev eth1 weight 2
exit 0

Verification and Monitoring

Confirm ECMP Route is Active

# View routing table — output must show nexthop syntax
ip route show

# Expected output:
# default
#     nexthop via 192.168.1.1 dev eth0 weight 1
#     nexthop via 192.168.2.1 dev eth1 weight 2

Check Which Path Packets Take

# Use ip route get to see the nexthop for each destination
ip route get 8.8.8.8
ip route get 1.1.1.1
ip route get 208.67.222.222

# Run with multiple destinations — output will alternate between the 2 interfaces
# Example:
# 8.8.8.8 via 192.168.1.1 dev eth0 src 192.168.1.100
# 1.1.1.1 via 192.168.2.1 dev eth1 src 192.168.2.100

Real-time Bandwidth Monitoring per Interface

# Use ip -s to view packet/byte counts
watch -n 1 'ip -s link show eth0 && echo "---" && ip -s link show eth1'

# Or nload (requires installation)
nload eth0 eth1

# vnstat to view traffic by hour/day
vnstat -i eth0 -i eth1 --live

Real-world Issues: Asymmetric Routing and Failover

Asymmetric routing is the most common issue with multi-ISP ECMP. When packets from the server leave via eth0 but replies arrive via eth1 (due to different ISP routing), a stateful firewall will drop those packets because it never saw the initial SYN. Fix this with policy routing:

# Create 2 separate routing tables for each ISP
echo "100 isp1" >> /etc/iproute2/rt_tables
echo "200 isp2" >> /etc/iproute2/rt_tables

# Table isp1: all traffic leaving via eth0 replies back through eth0
ip route add default via 192.168.1.1 table isp1
ip route add 192.168.1.0/24 dev eth0 src 192.168.1.100 table isp1

# Table isp2: same for eth1
ip route add default via 192.168.2.1 table isp2
ip route add 192.168.2.0/24 dev eth1 src 192.168.2.100 table isp2

# Routing rules: use the corresponding table based on source IP
ip rule add from 192.168.1.100 table isp1
ip rule add from 192.168.2.100 table isp2

ECMP has no built-in health check. If a gateway goes down, the kernel will continue sending traffic to it. A simple monitor script to automatically remove the route when a gateway fails:

#!/bin/bash
# /usr/local/sbin/ecmp-monitor.sh — runs via cron every minute
GW1="192.168.1.1"; GW2="192.168.2.1"

ping -c 3 -W 2 $GW1 > /dev/null 2>&1; GW1_UP=$?
ping -c 3 -W 2 $GW2 > /dev/null 2>&1; GW2_UP=$?

if [ $GW1_UP -eq 0 ] && [ $GW2_UP -eq 0 ]; then
  # Both gateways up — ensure full ECMP
  ip route replace default \
    nexthop via $GW1 dev eth0 weight 1 \
    nexthop via $GW2 dev eth1 weight 2
elif [ $GW1_UP -ne 0 ]; then
  ip route replace default via $GW2 dev eth1
  logger "ECMP: Gateway 1 down, switched to single path via $GW2"
elif [ $GW2_UP -ne 0 ]; then
  ip route replace default via $GW1 dev eth0
  logger "ECMP: Gateway 2 down, switched to single path via $GW1"
fi
# Add to crontab
echo "* * * * * root /usr/local/sbin/ecmp-monitor.sh" > /etc/cron.d/ecmp-monitor
chmod +x /usr/local/sbin/ecmp-monitor.sh

ECMP is a lightweight and effective tool — no extra daemons or complex software needed, the kernel handles all the distribution. Combined with policy routing to prevent asymmetric routing and a simple monitor script, it covers most production use cases.

Share: