The ticket always reads the same way. Two links, same bandwidth, same protocol, same metric, both up. One is running at 400 Mbps and the other is doing 30. Somebody escalates it as a load-balancing fault, and it is not one.
The misconception is that equal-cost multipath means packets alternate between links. It does not. The routing table installs multiple next-hops, but CEF picks one of them per flow and keeps that flow there for as long as the path exists. A single TCP session will never use both links. Scale that up across a tiered topology where every router hashes the same fields with the same algorithm and you get polarization: entire layers agreeing on the same side and leaving the other side idle. This article proves the per-flow behaviour on real output, then explains the polarization mechanism that follows from it. For the wider context, start with how a router decides where to send a packet.
Everything captured below came from CML using iol-xe nodes running IOS XE 17.18.2, on 2026-07-20.
What the router installs when two paths tie
Equal-cost multipath needs three things to line up: the same routing protocol, the same administrative distance, and the same metric. Miss any one of them and you get a single best path, because the router ranks the source before it ever compares metrics. When they do line up, the prefix is installed with more than one Routing Descriptor Block.
R1# show ip route 8.8.8.0
Routing entry for 8.8.8.0/24
Known via "ospf 1", distance 110, metric 11, type intra area
Routing Descriptor Blocks:
10.0.13.2, from 3.3.3.3, via Ethernet0/1
Route metric is 11, traffic share count is 1
* 10.0.12.2, from 2.2.2.2, via Ethernet0/0
Route metric is 11, traffic share count is 1Two descriptor blocks, both Route metric is 11, both traffic share count is 1. That share count is the ratio in which the router intends to distribute traffic, and 1 to 1 is what equal-cost gives you. The asterisk marks the next-hop used for a packet the router itself originates, which is worth knowing because it makes engineers think one path is preferred. It is not. If you are unsure which fields in that output mean what, reading the anatomy of a show ip route entry is worth the detour.
The RIB is only half the story. Forwarding happens in CEF, so check the FIB too:
R1# show ip cef 8.8.8.0 255.255.255.0
8.8.8.0/24
nexthop 10.0.12.2 Ethernet0/0
nexthop 10.0.13.2 Ethernet0/1Two descriptor blocks in the RIB plus two next-hops in the FIB is the definitive answer to "is this prefix load-sharing?". If the RIB shows two and CEF shows one, you have a FIB problem, not a routing problem, and that is a completely different investigation.
CEF picks a path per flow, not per packet
Here is the command that ends most of these arguments. show ip cef exact-route takes a source and a destination and tells you exactly which physical interface that pair will use, right now, deterministically:
R1# show ip cef exact-route 1.1.1.1 8.8.8.10
1.1.1.1 -> 8.8.8.10 => IP adj out of Ethernet0/1, addr 10.0.13.2One source, one destination, one interface. Ethernet0/0 is installed, available, and completely irrelevant to this flow. Run the command again and you get the same answer, because CEF is not choosing at random, it is hashing the source and destination address and using the result to index into a load-sharing table. Same input, same output, every time.
That is the mechanism behind the ticket. Your 400 Mbps backup job is one source talking to one destination, so it is one hash result, so it is one link. Adding a second 1 Gbps circuit did not make that transfer faster and never could. Only more flows can use more links.
Layer 4 ports are not in the hash unless you explicitly add them with ip cef load-sharing algorithm include-ports source destination, and platform support for that varies. This matters more than it sounds: if a host pair opens sixteen parallel TCP sessions to move data faster, all sixteen still hash identically under the default algorithm, because the hashed fields are the same for all of them. Adding ports is what turns those sixteen sessions into sixteen independent hash results.
Per-packet exists and you almost certainly do not want it
IOS will do true round-robin if you ask it to, per interface, with ip load-sharing per-packet. It gives you genuinely even byte distribution across the links. It also delivers packets out of order whenever the two paths have even slightly different latency, which they always do.
Out-of-order delivery is not cosmetic. TCP treats three duplicate ACKs as a loss signal and halves its congestion window, so a link pair that is dropping nothing still looks like a lossy path to every session crossing it. Add that per-packet forwarding is frequently punted rather than done in hardware, and the feature costs you throughput to buy a statistic nobody needs. The one defensible case is very low speed parallel links carrying traffic that does not care about ordering. Outside of that, leave it alone.
Hashing distributes flows, not bytes
This is the part that gets skipped, and it is the reason "fixing" the hash so often changes nothing. CEF distributes flows evenly. It has no idea how many bytes each flow will carry.
With two links and four flows, a perfect hash puts two flows on each side. If one of those four is a database replication stream and the other three are SSH sessions, your links are 99 percent uneven and the load balancing is working correctly. You need a large number of similarly-sized flows before hash distribution starts to look like even utilisation, and plenty of real links carry a handful of elephant flows and a long tail of nothing.
So before you tune hash algorithms, count your flows. If the answer is "six, and one of them is the backup", no algorithm will help you. The fix is at the application or the design layer, not in CEF.
Polarization: when every router makes the same decision
Now scale that deterministic hash across a tiered topology. An access router hashes a flow and sends it left. The flow arrives at a distribution router that has its own pair of equal-cost paths and runs the same hash function over the same source and destination fields. Nothing about the packet changed in transit, so the hash produces the same relative result and the distribution layer sends it left again.
Each downstream router therefore only ever sees the subset of flows that hashed to one side, and half of its own uplinks go unused. That is CEF polarization. It is not a bug in the hash, it is what happens when every device in a path runs an identical deterministic function on identical input, which was exactly the behaviour of the original fixed CEF hashing algorithm.
The fix is to make the input or the function differ per device. Modern IOS and IOS XE default to the universal algorithm, which mixes a router-specific unique ID into the hash so that two routers presented with the same flow do not necessarily reach the same conclusion:
R1(config)# ip cef load-sharing algorithm universalYou can append a hex ID to that command to set the seed explicitly, which guarantees adjacent layers differ rather than trusting them to differ by accident. Other options exist depending on platform, including tunnel for topologies where the outer header is nearly identical on every flow, and include-ports to widen the hash input. The principle is the same in all of them: vary either the algorithm or the fields per layer so the same flow does not get the same answer twice.
If this pattern feels familiar, it should. The way an EtherChannel picks a member link is the same problem one layer down. A bundle hashes frames to members, a single flow rides a single member, and a bundle with a non-power-of-two member count distributes hash buckets unevenly before any traffic arrives. Diagnose both the same way: count flows first, look at the hash input second.
When the paths are not equal
Everything above assumes the metrics tie. Two knobs change that.
maximum-paths caps how many equal-cost paths the protocol will install. Check what yours is set to with show ip protocols | include maximum path, because the default varies by platform and release. Setting it to 1 disables ECMP for that protocol entirely, which is a useful diagnostic in itself:
R1(config)# router ospf 1
R1(config-router)# maximum-paths 1
R1# show ip route 8.8.8.0
Routing entry for 8.8.8.0/24
Known via "ospf 1", distance 110, metric 11, type intra area
Routing Descriptor Blocks:
* 10.0.12.2, from 2.2.2.2, via Ethernet0/0 <-- only ONE path now
R1# show ip cef 8.8.8.0 255.255.255.0
8.8.8.0/24
nexthop 10.0.12.2 Ethernet0/0One descriptor block, one CEF next-hop. The second path is still in the OSPF database and still valid, it just is not installed. Nothing about the topology changed, only the RIB install limit.
The second knob is unequal-cost sharing, and this is where the protocol matters. OSPF, IS-IS and BGP install equal-cost paths only. EIGRP can install paths with different metrics using variance, subject to the feasibility condition, and it adjusts the traffic share counts to match the metric ratio. That is a protocol feature layered on top of everything described here, and it is covered separately in how to share traffic across paths with different metrics. Keep the two ideas apart: variance decides which paths get installed and in what ratio, while everything in this article is platform-level forwarding behaviour that happens after the install and applies identically no matter which protocol put the paths there. Set variance and the polarization mechanism does not go away, it just operates on unevenly weighted buckets.
What this was captured on
R2 and R3 each advertise the same prefix into OSPF area 0 at identical cost, so R1 has no reason to prefer either. That symmetry is what makes the exact-route result meaningful: the flow is not landing on Ethernet0/1 because that path is better, it is landing there because the hash said so.
Gotchas
- A loopback advertised into OSPF appears as a /32, not as the subnet you configured. If you build this lab and your
8.8.8.0/24never shows up, that is why. Putip ospf network point-to-pointon the loopback and OSPF advertises the real mask. - A /30 has exactly two host addresses. The first build of this lab put R3 on 10.0.13.3/30, which is the broadcast address. No adjacency formed, only one path was installed, and the whole thing looked like an ECMP failure. It was an addressing typo. Before you debug load sharing, confirm both neighbours are actually up.
- The asterisk in
show ip routeis not a preference marker. With two equal descriptor blocks it just tags which next-hop the router itself would use for locally originated traffic. Engineers regularly read it as "this is the active path" and conclude ECMP is broken. - Do not test load sharing with ping from the router. Router-sourced traffic does not necessarily follow the CEF path a transit flow would take. Use
show ip cef exact-routewith the real source and destination instead. - Interface counters lie about load sharing. Uneven byte counts across two links are consistent with a healthy hash and a small number of flows. Counters tell you nothing until you know the flow count.
Key Takeaways
- More than one Routing Descriptor Block plus more than one CEF next-hop for the same prefix is the definitive test for whether a prefix is load sharing at all.
- CEF hashes each flow to exactly one path, by default over source and destination IP.
show ip cef exact-routeproves it and settles the "why is my transfer not using both links" question in one command. - Per-packet load sharing gives even bytes and causes reordering, which TCP reads as loss. Reach for it almost never.
- Hashing distributes flows, not bytes. A handful of large flows will never balance evenly no matter which algorithm you choose.
- Polarization happens when every router in a tier runs the same hash over the same fields and reaches the same conclusion. Vary the algorithm, the seed, or the fields per layer to break it.
maximum-pathscontrols how many equal paths get installed and disables ECMP at 1. Unequal-cost sharing is an EIGRP-only feature and sits on top of, not instead of, the forwarding behaviour described here.
The pattern worth carrying away is that load sharing decisions happen in two separate places: the routing protocol decides which paths get installed, and CEF decides which installed path each flow uses. Almost every load-balancing complaint is really a question about the second one. For the rest of the path a packet takes from route lookup to egress, the complete IP routing reference covers the surrounding pieces.