ECMP Routing in CDN Networks: Equal-Cost Multi-Path Traffic Distribution

Back in 2019 we brought a new cache expansion online in Ashburn and watched our spine-leaf fabric melt down within 20 minutes. We had 400G of capacity across four equal-cost paths, but one link was pegged at 98% while the other three sat under 30%. It wasn't a capacity problem — it was an ECMP routing problem. If you've run any serious CDN networking at scale, you've seen this exact scenario where "equal-cost" doesn't mean equally used.

How ECMP Hashing Actually Distributes Traffic in a Clos Fabric

Most engineers think ECMP routing is simple: you have four paths with the same cost, so the router just round-robins packets. It doesn't. That would destroy TCP performance with massive reordering. Instead, every modern platform does flow-based hashing. The router takes fields from the packet header — typically the 5-tuple of source IP, destination IP, protocol, source port, and destination port — runs them through a hash algorithm, and uses the result to pin that entire flow to a single next-hop. That flow stickiness is what keeps packets in order, but it’s also what makes traffic distribution so unpredictable in CDN environments.

I've spent the last decade tuning this on everything from Juniper MX to Arista 7508Rs, and the hash inputs matter more than most people realize. In a CDN, your traffic isn't enterprise traffic. You're dealing with millions of long-lived TCP connections serving large objects, plus a huge volume of short-lived HTTP/2 and HTTP/3 flows that are often QUIC-encrypted. If you're only hashing on the traditional 5-tuple, QUIC flows can look identical from a hashing perspective if they're all hitting the same VIP. We've had to enable additional entropy fields like ingress port and even MPLS labels in some fabrics to get decent distribution. It's a stark contrast to MPLS in CDN Backbone Networks: Label Switching for Predictable Performance, where path selection is explicitly signaled rather than hashed.

The other gotcha is how BGP advertises those equal costs. You don't get ECMP for free. You have to explicitly enable BGP multipath and ensure your IGP metrics are truly equal. I once spent a whole night troubleshooting a PoP where two paths had a 10-point OSPF metric difference because someone left a legacy interface cost on a loopback. The routers did exactly what we told them — they preferred the lower cost and ignored ECMP entirely. You can verify what the control plane actually sees versus what the forwarding plane does with the hardware hash.

# Arista EOS - Verify ECMP and hash fields on a leaf
show ip route 198.51.100.0/24
# Should show 4 equal-cost next-hops with same metric

show platform jericho hash fields
# Displays configured hash seed and fields: src-ip, dst-ip, l4-ports

# Resilient ECMP configuration - avoids full rehash on link failure
hardware forwarding ecmp resilient
ip load-sharing hash seed 0x4A7F
ip load-sharing fields ip src-ip dst-ip src-port dst-port ingress-interface ipv6 flow-label

# Monitor actual distribution in hardware
show counters ecmp member utilization | grep -A 4 Port-Channel100

Hash Polarization and Flow Collisions: Why Your Links Aren't Balanced

Here's the failure mode that'll keep you up at night: hash polarization. It happens when multiple layers of your network make the same hash decision. Imagine traffic entering your CDN edge router, getting hashed onto link 2 of 4. Then it hits your spine layer, which runs the exact same hash algorithm with the same seed — and it hashes that same flow onto link 2 again. Suddenly, instead of spreading traffic across 16 unique end-to-end paths in your Clos, you're only using 4. I saw this take down a whole availability zone in Chicago where two tiers of Broadcom Trident-based switches were using identical default seeds. We were seeing 60% packet loss on one spine while the others were idle.

The fix isn't complex, but you've got to be deliberate about it. Every ECMP group at each tier needs a different hash seed or offset. Modern platforms call this resilient hashing or universal hashing. It ensures that even if the 5-tuple is the same, the hash result differs per device. The other half of the problem is flow entropy — or the lack of it. CDN traffic distribution suffers when you have elephant flows. A single 100 Gbps cache-fill from origin or a large video segment fetch can be a single flow that pins to one link for minutes. No amount of hashing magic will split a single flow. That's where you have to start thinking beyond pure ECMP routing and consider flowlet switching or packet spraying for specific traffic classes, though that comes with its own reordering risks we documented extensively when reviewing HTTP Working Group Specifications for cache interactions.

For multi-path routing to work well, you also need to understand your multipath width. Most vendors support 32-way or 64-way ECMP, but your hash quality degrades as you add more paths if you don't have enough entropy. We typically limit ourselves to 16-way at the leaf-spine layer and use hierarchical ECMP for anything larger. If you need deterministic traffic engineering for high-priority origin traffic, ECMP alone won't cut it — you'll want to look at Segment Routing for CDN Traffic Engineering: Source-Routed Path Selection to explicitly steer those flows without relying on hash luck. For general eyeball traffic, though, well-tuned ECMP is still far more scalable.

! Juniper MX - Mitigating polarization with unique seeds per layer
set forwarding-options enhanced-hash-key family inet seed 0x1A2B3C4D
set forwarding-options enhanced-hash-key family inet hash-field ip-protocol
set forwarding-options enhanced-hash-key family inet hash-field source-port
set forwarding-options enhanced-hash-key family inet hash-field destination-port

! Enable resilient hashing - only flows on failed link are rehashed
set routing-options forwarding-table ecmp fast-reroute
set routing-options forwarding-table load-balance resilient-hash

! Verify polarization isn't occurring
show route forwarding-table destination 203.0.113.0/24 extensive | match "next-hop|weight"
monitor interface traffic | match "ae[0-9].*bps"  # Watch for imbalance in real-time

! Linux host ECMP for anycast cache servers
ip route add 192.0.2.0/24 nexthop via 10.0.1.1 weight 1 nexthop via 10.0.2.1 weight 1 nexthop via 10.0.3.1 weight 1

Benchmarking ECMP Distribution With Real CDN Workloads

You can't tune what you don't measure, and `show interface` isn't enough. We learned to instrument ECMP at the flow level, not just the link level. The key metrics are link utilization standard deviation, flow entropy, and the dreaded rehash rate when a member fails. A healthy CDN networking fabric should keep link utilization within 10-15% of the mean across all members under steady state. If you're seeing one link 40% hotter than the others, you've got a hash problem, not a capacity problem. We also track the number of flows per member — with millions of active flows, the distribution should be statistically even. If it's not, you're likely hitting a collision pattern with your VIP structure.

What surprised us most when we rebuilt our European fabric was how much virtualization impacted hashing. When we moved some of our L4 load balancers to virtual appliances on Network Function Virtualization in CDN Infrastructure: Virtual Appliances at Scale, the inner packet's entropy was hidden behind an outer VXLAN or GRE header. The leaf switches were hashing on the outer header only, so all traffic from a single hypervisor looked like one or two flows. We had to enable inner-header hashing and ensure the vSwitches were setting entropy labels correctly. Guidance from web.dev Articles on connection coalescing also helped us understand why HTTP/3 flows were bunching — they reuse the same 5-tuple more aggressively than HTTP/1.1.

Below is what we measured in our Amsterdam PoP before and after we fixed our ECMP tuning. We changed three things: set unique hash seeds per layer, enabled resilient hashing, and added ingress-interface and IPv6 flow-label to the hash fields. No hardware was added. The improvement in tail latency alone justified the maintenance window.

Metric (Per Leaf-Spine Group, 8x100G) Before Tuning After Tuning Change
Mean Link Utilization (Peak) 68.4% 61.2% -10.5% more headroom
Utilization Std. Deviation 22.7% 6.3% 72% reduction in imbalance
Flows per ECMP Member (1M flows) 118k - 182k (54% skew) 123k - 128k (4% skew) Near-perfect distribution
Packet Reordering on Member Failure 1.8% of flows rehashed 0.12% of flows rehashed 15x less disruption
P99 Cache Node RTT (intra-PoP) 1.84 ms 0.92 ms 50% lower tail latency

Implementing Resilient ECMP on Production CDN Switches

Default ECMP behavior will burn you the first time a spine link flaps at 2 a.m. Most vendor OS images ship with a simple modulo-N hash — take the 5-tuple hash, mod it by the number of next-hops, and you're done. It’s fast, but it’s brittle. When one of your 16 ECMP next-hops goes down, that modulo operation reshuffles nearly every flow. We’ve measured 87-94% flow remapping on standard hash during a single link failure, which means thousands of TCP sessions getting re-hashed to a different path, out-of-order delivery, and a 400-600ms latency spike while your video clients re-buffer.

What you actually want in a CDN fabric is resilient hashing. Instead of rehashing everything, it only remaps flows that were using the failed next-hop; the other 15/16ths stay pinned. On Arista EOS that’s a one-line profile, on Juniper it’s `resilient-ecmp` under `forwarding-options`, and on Cumulus Linux / FRR you’ll toggle `resilient-hash` in the ECMP config. Pair that with a deterministic seed per tier — don’t use the same hash seed on your leaf and spine or you’ll just recreate polarization downstream. You’ll also want to be explicit about your hash fields. For CDN traffic, IP + ports isn’t always enough; we include `mpls-label` and `inner-ipv4` for tunneled cache-fill traffic, otherwise your 100 Gbps of origin fetch collapses onto two links.

Before you push this live, lab it with your actual flow distribution. Don’t trust `show load-balance`. Inject a recorded pcap with 500k+ flows and watch the histogram. If you’re evolving toward a more dynamic fabric, Software-Defined Networking for CDN: Programmable Traffic Control is worth a read — once you’ve got resilient hashing stable, you can let a controller tune WECMP weights instead of doing it by hand.

! Arista EOS - Resilient ECMP with 5-tuple + entropy
load-balance profile CDN-RESILIENT-HASH
   fields ip src-ip dst-ip src-port dst-port protocol
   fields entropy-label  ! for tunneled PoP-to-PoP traffic
   hash-seed 1984        ! unique per tier: leaf=1984, spine=4242
   resilience resilient-hash
   buckets 512           ! larger table = less remapping on failure

router general
   ecmp 64
   load-balance profile CDN-RESILIENT-HASH

! Verify - look for < 7% remaps on single next-hop failure
! show load-balance profile CDN-RESILIENT-HASH buckets detail
! show ip route 203.0.113.0/24 detail | grep Hash

Diagnosing Polarization, Elephant Flows, and Silent Imbalances

You won’t see ECMP imbalance in your 5-minute SNMP averages. You’ll see it as microbursts — one spine link sitting at 68% average but hitting 97% for 50ms bursts every second, while its neighbor idles at 31%. That’s classic hash polarization. It happens when two tiers use the same hash function and seed, so a flow hashed to link 3 at the leaf gets hashed to link 3 again at the spine. We caught this in a 32-leaf Clos where just two hash seeds caused a 2.3:1 skew between spine uplinks under real video traffic. The fix wasn’t more capacity; it was per-tier seeds plus including the ingress interface ID in the hash, which broke the correlation and brought the ratio down to 1.08:1.

The other killer is elephant flows. ECMP is per-flow, not per-packet, so a single 40 Gbps 4K origin pull or a large cache-fill between PoPs can pin a link. Standard hashing won’t save you. We now run sFlow at 1:4096 on all fabric links and alert if any single 5-tuple exceeds 8% of link capacity for more than 30 seconds. When that fires, we steer it. Weighted ECMP (WECMP) lets you bias away from hot links without a full reconvergence, and for newer fabrics we’re piloting flowlet switching — it cuts elephant flows into 100-200ms flowlets that can be re-balanced without reordering. If you’re rethinking how your PoPs interconnect to begin with, review CDN Backbone Topology Design: Ring, Mesh, and Hierarchical Architectures and CDN PoP Interconnection Design: Building Reliable Edge-to-Core Links — your ECMP strategy has to match your topology, a 4-way mesh needs very different weighting than a 16-way Clos.

Finally, don’t trust that ECMP is actually forwarding where you think it is. Silent blackholing happens when BGP advertises an ECMP next-hop that’s no longer in the FIB due to a stale ARP or a down member. We run a lightweight prober that sends Paris traceroute variants with varying UDP source ports every 10 seconds across each ECMP group. If a specific hash bucket stops responding but the link stays Up, you’ve found a broken member that BFD missed. We’ve caught three forwarders in the last year that were dropping 1/16th of traffic — that’s 6.25% loss that never showed up as a link-down event.

Frequently Asked Questions

How many ECMP next-hops can we actually use in a CDN fabric?

Most modern merchant silicon (Trident3, Tomahawk3/4) supports 64-way ECMP in hardware, and some Jericho2 platforms go to 128 or even 512. But don’t max it out just because you can. We keep production Clos fabrics at 16-way or 32-way. Beyond 32, the hash table gets sparse with realistic flow counts (under 200k concurrent flows per PoP), and you’ll see 15-20% utilization variance even with a perfect hash. Stick to 16-way leaf-to-spine and use WECMP if you need finer traffic engineering.

Should we hash on the 5-tuple or include additional fields for CDN traffic?

Start with the classic 5-tuple: src/dst IP, src/dst port, protocol. For most edge traffic that’s enough. You should add fields when you’re tunneling — GRE, MPLS, or VXLAN-encapsulated cache-fill between PoPs will look like a single flow if you only hash outer headers. Include inner IP and entropy labels. And if you’re seeing poor distribution with QUIC (which encrypts everything above UDP), you’ll want to include IPv6 flow-label or UDP source port entropy from the client side.

Does ECMP work well with anycast for CDN edge PoPs?

Yes, but they’re orthogonal layers. Anycast gets the user to the *nearest* PoP via BGP, ECMP distributes traffic *inside* that PoP’s fabric. The gotcha is that ECMP can break anycast session affinity if you ECMP across PoPs without consistent hashing. Keep your ECMP domain local to the PoP fabric. If you’re load-balancing across PoPs for origin, use consistent hashing or Maglev-style rings at the L7/L4 load balancer instead — ECMP alone will reshuffle users on any path change.

How do we test ECMP distribution before going live?

Don’t rely on iperf with 10 flows. Replay a real traffic capture — at least 1M flows sampled from your busiest PoP — through a tool like TRex or Ostinato with the same hashing profile. Check the bucket histogram: you want a standard deviation under 4% across members for uniform link speeds. Then flap a link. With resilient hashing you should see less than 1/(N) flows remapped. If you see 50%+ remaps, your resilient hashing isn’t active or your bucket size is too small.

Related Articles

References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker