Routing Loop Troubleshooting: Detect, Break, and Prevent Them

A traceroute whose hop addresses repeat until the TTL runs out is a routing loop, and nothing else. Captured on CML with IOS XE 17.18.2: the symptom signature, why TTL keeps a routing loop from behaving like a Layer 2 loop, the four causes ranked by real frequency, and the Null0 discard route...

Terminal showing a traceroute alternating between two router addresses, the signature of a routing loop

A routing loop is one of the few faults where the symptom hands you the diagnosis. Traffic to one prefix dies, everything else on the box is fine, and a traceroute comes back with the same two or three addresses repeating down the whole column until it gives up at hop 30. That pattern is not ambiguous. Two routers each believe the other is the way to the destination, and your packets are passed back and forth between them until their TTL runs out.

Every block of output below came off a two-router CML lab running Cisco IOS XE 17.18.2 on iol-xe nodes, break baked into the startup config: R1 and R2 back to back on 10.0.12.0/30, each holding a static for 172.16.99.0/24 pointing at the other. For the machinery underneath, the guide to how a router picks a route and forwards on it covers the routing table behaviour this article assumes.

The Symptom Signature

Three things point at a loop, and the first is worth more than the other two put together.

The traceroute address column repeats. This is the tell. Here is the lab, broken:

R1# traceroute 172.16.99.1 numeric timeout 1 probe 1
Tracing the route to 172.16.99.1
  1 10.0.12.2 3 msec
  2 10.0.12.1 2 msec
  3 10.0.12.2 2 msec
  4 10.0.12.1 2 msec
  ... (alternating 10.0.12.2 / 10.0.12.1) ...
 29 10.0.12.2 13 msec
 30 10.0.12.1 13 msec

Two addresses, alternating, all the way to the maximum TTL. Note numeric and probe 1: reverse DNS on thirty hops turns a four second traceroute into a long wait, and one probe per hop keeps the address column a single entry wide so the repeat is obvious at a glance.

You get TTL exceeded replies from a router, not the destination. A user reporting "TTL expired in transit" is reporting a loop until proven otherwise, and the source address on that ICMP message is one of the looping routers.

A link sits at high utilisation carrying nothing legitimate. Each packet crosses the looping link many times before it dies, so the loop is a traffic multiplier. A host starting at TTL 128 gets roughly sixty round trips per packet, and a trickle of traffic to a dead prefix presents as tens of megabits between the two routers.

Why the Network Does Not Melt (and a Layer 2 Loop Does)

People conflate routing loops with switching loops. They behave nothing alike, and the difference is one field in the IPv4 header.

Every router that forwards a packet decrements its TTL by one, and a router that decrements it to zero discards the packet and sends an ICMP Time Exceeded message back to the source. A looping packet therefore has a hard, bounded lifetime and the damage is linear: one packet per second entering a two-router loop at TTL 255 becomes roughly 127 packets per second on that link. A fixed multiplier that never grows. An Ethernet frame has no TTL field, so a broadcast entering a Layer 2 loop is replicated at every switch on every port, each copy replicated again, and the segment saturates in seconds. That is why STP exists and why nothing equivalent is needed at Layer 3.

The same mechanism is what traceroute exploits deliberately. It sends probes with TTL 1, then 2, then 3, and reads the source address of each Time Exceeded reply. When the routing table is looping, traceroute is printing the routers in the cycle in order, over and over. The alternating column is not an artefact of the tool. It is a faithful map of the path.

What This Was Captured On

PlatformCML, 2 x iol-xe nodes, Cisco IOS XE 17.18.2
LinkR1 to R2 back to back, 10.0.12.0/30
Target prefix172.16.99.0/24, which exists nowhere in the lab
The breakR1 static to 10.0.12.2, R2 static to 10.0.12.1
Capture methodOn-box EEM running traceroute, output to syslog on R1

Finding Where the Loop Closes

The traceroute gives you the members of the cycle. Now find the one routing table entry that is wrong. Walk the routers it named and ask each the same question:

show ip route 172.16.99.1
show ip cef 172.16.99.1

The loop closes at the pair where A's entry names B as the next hop and B's names A. On a longer cycle, look for the router whose next hop points backwards along the path the packet arrived on. Check show ip cef as well as show ip route: the RIB is what the control plane decided, the FIB is what the linecard is doing, and the rare disagreement is the one worth catching.

Then read that entry's route code and administrative distance, which name the cause: a static shows as S, a redistributed route as external (O E2, D EX), and a summary as a shorter prefix than the one you asked for.

The Four Causes, in Order of Frequency

1. Mutual redistribution without filtering

By a wide margin the most common cause in production, and the only one here that turns up in networks nobody misconfigured on purpose. It needs two ingredients: two routing protocols, and two or more routers redistributing between them in both directions.

A prefix native to OSPF is redistributed into EIGRP at router A, crosses the EIGRP domain to router B, and is redistributed back into OSPF as an external. Some router now has a choice between the real route and the round-trip copy, and if the copy wins on metric or administrative distance, it starts forwarding toward the EIGRP domain for a prefix that lives in OSPF.

The structural fact worth holding on to: a single redistribution point cannot loop, because a route has nowhere to come back from. Two or more can. Anyone adding a second redistribution router to a single-point design is one route-map away from this, which is why what breaks when two protocols redistribute into each other is worth reading first.

2. A static route whose next hop routes the packet back

The lab's loop, and the branch and edge classic. You point a static at a next hop, that next hop has no more specific route for the destination, so it falls back on its default, and its default points at you.

The production version usually involves an internet edge. Your default points at the ISP, the ISP has a static for your address block pointing back at you, and somebody assigns a prefix inside that block that is not routed internally. Traffic to it goes out, comes straight back, and you trade the packet until TTL runs out. Nobody configured a loop and both statics are individually correct. Recursive statics earn the same suspicion: your router checks it can resolve the next hop and installs the route, and whether that next hop knows what to do with the destination is not something it can verify.

3. Administrative distance manipulation

Administrative distance is locally significant. That single sentence is the whole cause. Change the AD of a route to make one box prefer the path you want and you have changed one router's opinion and nobody else's. If the neighbour still prefers the old path, the two now disagree about direction, and traffic caught between them loops.

Floating statics are the usual vehicle, along with per-protocol distance commands used during a migration. The migration case is worse, because it is applied router by router across a maintenance window and the loop lives for as long as the network is half changed. If you are going to touch AD, get clear on how a router decides which routing source to trust, and change it symmetrically or not at all.

4. Summarisation hiding a more specific route

You advertise 10.1.0.0/16 upstream, but only 10.1.1.0/24 and 10.1.2.0/24 exist behind you. Traffic to 10.1.9.9 is drawn to you by the summary, you have no matching specific, so you fall back on your default, which points at the router that just sent you the packet. It sends it back because of your summary.

This one surprises people, because summarisation is supposed to be good practice and nothing in the config looks wrong. Being fluent in how longest prefix match picks the winning entry helps, since the failure is entirely a consequence of the summary being the longest match available for an address with no real home.

Breaking the Loop

The fix is to make one router stop forwarding the packet back. In the lab, repoint R2's static at Null0, a discard route:

R2(config)# no ip route 172.16.99.0 255.255.255.0 10.0.12.1
R2(config)#  ip route 172.16.99.0 255.255.255.0 Null0

Traceroute from R1 straight afterwards:

R1# traceroute 172.16.99.1 numeric timeout 1 probe 1
Tracing the route to 172.16.99.1
  1 10.0.12.2 3 msec
  2 10.0.12.2 !H          <-- !H = destination host unreachable (R2 dropped it), loop gone

Two lines instead of thirty. Be precise about what the !H proves: the packet reached R2 and R2 dropped it rather than forwarding it back. An unreachable code is a healthy result for a prefix that genuinely does not exist. (You may see !N instead, depending on platform; either means dropped.)

Treat the Null0 as stopping the bleeding, not as the fix. It ends the CPU and bandwidth damage in one command, but the reason the two routers disagreed is still in the config.

Preventing the Next One

Tag routes on redistribution and deny the tag on the way back. This is the answer to cause number one and the only one that scales. Set a tag when you inject routes into a protocol, then match and deny that tag on every route-map redistributing the other way. A route that originated in your OSPF domain cannot get back in, however many redistribution points you add later, and you never maintain a prefix list. The mechanics are in stopping redistributed routes from being re-injected using tags.

Filter explicitly at the redistribution point when tags are not available. Prefix lists on the route-map work, but every new subnet is a change to every filter, and the first one somebody forgets brings the loop back. Use them only where the prefix set is static.

Install a discard route for every summary you advertise. A Null0 for the aggregate means traffic to non-existent specifics inside it is dropped by you instead of bounced upstream. EIGRP and OSPF do this when you use their own summarisation commands; summaries built by advertising a static or by redistribution do not get one.

Keep redistribution to one point where the topology allows. One boundary cannot loop, and when redundancy forces you to two, the tagging goes in with the second router, not after the first outage. For a dual-homed site that could receive its own routes back from the core, tagging routes with the site they came from applies the same idea per site rather than per protocol.

Common Mistakes and Gotchas

  • Assuming the pattern is always two alternating addresses. A three or four router loop cycles through three or four addresses in sequence. Look for repetition, not specifically for A/B/A/B.
  • Only testing one direction. Loops are frequently asymmetric. Traffic one way loops, the return path is clean, and the far end insists the link is fine because their pings work. Traceroute from both ends.
  • Treating an unreachable as a failure. After the fix the traceroute still does not reach the destination, because the destination does not exist. !H at hop 2 is the success condition here.
  • Leaving the Null0 in and closing the ticket. The discard route makes the symptom vanish completely, which is why it gets forgotten. Six months later somebody deploys that prefix for real and it is unreachable for reasons nobody can find.

Key Takeaways

  • A traceroute whose hop addresses repeat in a cycle up to the maximum TTL is a routing loop. There is no other common explanation.
  • TTL is why a routing loop degrades a link instead of destroying a network. Ethernet frames have no equivalent field, which is why a switching loop is far worse.
  • Find it with show ip route and show ip cef for the destination on each router the traceroute named, looking for the pair pointing at each other.
  • Ranked by real frequency: mutual redistribution without filtering, a static whose next hop routes the packet back, asymmetric AD changes, and a summary with no matching specific.
  • A Null0 discard route breaks the loop immediately and !H proves the packet is dropped rather than forwarded, but it is triage, not a fix.
  • Prevent the next one with route tags on redistribution, filtering at the boundary, and a discard route behind every summary you advertise.

Loops are one of the small set of faults where the diagnostic is faster than the theory. For the rest of the material on route selection and forwarding behaviour underneath this one, start from the routing troubleshooting and configuration hub.

Read next