Loading learning content...
The count-to-infinity problem arises from a fundamental information gap: when router A tells router B "I can reach destination X," B doesn't know that A's path to X might go through B itself. If A learned the route from B, advertising it back makes no sense—B already has a better path.
Split horizon addresses this with an elegantly simple rule: never advertise a route back through the interface from which you learned it. If you learned about destination X from your east interface, don't advertise X out that same interface. After all, if your neighbor to the east told you about X, they clearly have a better path than going through you.
This simple rule prevents the most common form of routing loops—two-node loops where routers A and B inform each other about routes that ultimately depend on the link between them. Split horizon is enabled by default in virtually all distance vector protocol implementations and is considered mandatory for stable operation.
By the end of this page, you will understand the split horizon mechanism completely—how it prevents loops, the difference between simple split horizon and split horizon with poison reverse, implementation details, when it fails, and practical deployment considerations.
Let's formalize the split horizon rule and trace through how it prevents the two-node loop scenario we examined previously.
Formal Rule:
When sending routing updates out an interface, do not include routes that were learned from that same interface.
Intuition:
If you learned route X from neighbor B (via interface eth1), then B either:
In either case, advertising X back to B is useless—B already has a better path. Omitting this advertisement breaks the loop-formation cycle.
Worked Example:
Topology: A ——— B ——— C ——— X
Without Split Horizon:
With Split Horizon:
Split horizon is applied per-interface, not per-neighbor. On point-to-point links, this distinction doesn't matter. On multi-access networks (like Ethernet) with multiple routers, routes learned from any router on that interface aren't advertised back out that interface to anyone.
Let's trace through a complete network scenario to see split horizon in action, step by step.
Topology:
(eth0) (eth0) (eth0)
A ——————— B ——————— C ——————— X
(eth1) (eth1) (eth1) (destination)
Each link has cost 1. All routers run RIP with split horizon enabled.
Initial Convergence:
| Router | Destination | Metric | Next Hop | Interface |
|---|---|---|---|---|
| A | X | 3 | B | eth0 |
| A | B | 1 | B | eth0 |
| A | C | 2 | B | eth0 |
| B | X | 2 | C | eth0 |
| B | C | 1 | C | eth0 |
| B | A | 1 | A | eth1 |
| C | X | 1 | X | eth0 |
| C | B | 1 | B | eth1 |
| C | A | 2 | B | eth1 |
What Each Router Advertises (With Split Horizon):
Router A on interface eth0 (toward B):
Router B on interface eth0 (toward C):
Router B on interface eth1 (toward A):
Event: Link C-X Fails
Without Split Horizon:
With Split Horizon:
With split horizon, this failure scenario converges in just 2-3 triggered update cycles—a few seconds rather than the minutes required during count-to-infinity. The poison (metric 16) propagates cleanly from C to B to A without any confusion.
Implementing split horizon requires tracking which interface each route was learned from and filtering updates accordingly.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
from dataclasses import dataclassfrom typing import Dict, List, Optional, Setfrom enum import Enum class SplitHorizonMode(Enum): DISABLED = "disabled" SIMPLE = "simple" # Omit routes learned from interface POISON_REVERSE = "poison_reverse" # Advertise infinity for learned routes @dataclassclass RouteEntry: destination: str metric: int next_hop: str learned_interface: str # Critical for split horizon! @dataclassclass Interface: name: str neighbor: str cost: int class SplitHorizonRouter: """ Router implementation demonstrating split horizon behavior. """ INFINITY = 16 def __init__(self, router_id: str, interfaces: List[Interface], mode: SplitHorizonMode = SplitHorizonMode.SIMPLE): self.router_id = router_id self.interfaces = {iface.name: iface for iface in interfaces} self.mode = mode self.routing_table: Dict[str, RouteEntry] = {} # Add route to self self.routing_table[router_id] = RouteEntry( destination=router_id, metric=0, next_hop=router_id, learned_interface="local" ) def build_update_for_interface(self, interface_name: str) -> Dict[str, int]: """ Build routing update for a specific interface, applying split horizon. Args: interface_name: Name of the outgoing interface Returns: Dictionary of {destination: metric} to advertise """ update = {} for dest, route in self.routing_table.items(): if self.mode == SplitHorizonMode.DISABLED: # No split horizon: include all routes update[dest] = route.metric elif self.mode == SplitHorizonMode.SIMPLE: # Simple split horizon: omit routes learned from this interface if route.learned_interface != interface_name: update[dest] = route.metric # Routes learned from this interface are simply not included elif self.mode == SplitHorizonMode.POISON_REVERSE: # Split horizon with poison reverse if route.learned_interface == interface_name: # Advertise infinity for routes learned from this interface update[dest] = self.INFINITY else: update[dest] = route.metric return update def receive_update(self, source_interface: str, source_neighbor: str, distance_vector: Dict[str, int]) -> List[str]: """ Process received routing update. Args: source_interface: Interface update was received on source_neighbor: Neighbor that sent the update distance_vector: Received {destination: metric} pairs Returns: List of destinations that changed (for triggered updates) """ changed = [] link_cost = self.interfaces[source_interface].cost for dest, received_metric in distance_vector.items(): new_metric = min(received_metric + link_cost, self.INFINITY) if dest not in self.routing_table: if new_metric < self.INFINITY: self.routing_table[dest] = RouteEntry( destination=dest, metric=new_metric, next_hop=source_neighbor, learned_interface=source_interface ) changed.append(dest) else: existing = self.routing_table[dest] # Update if: better metric, or same next-hop with changed metric should_update = False if new_metric < existing.metric: should_update = True elif existing.next_hop == source_neighbor: # Must accept update from current next-hop should_update = (new_metric != existing.metric) if should_update: old_metric = existing.metric existing.metric = new_metric existing.next_hop = source_neighbor existing.learned_interface = source_interface changed.append(dest) print(f" [{self.router_id}] {dest}: {old_metric} -> {new_metric}") return changed def print_routing_table(self): """Display current routing table.""" print(f"\n=== {self.router_id} Routing Table (Mode: {self.mode.value}) ===") print(f"{'Dest':<8} {'Metric':<8} {'Next Hop':<10} {'Interface':<12}") print("-" * 40) for dest, route in sorted(self.routing_table.items()): metric_str = str(route.metric) if route.metric < self.INFINITY else "∞" print(f"{dest:<8} {metric_str:<8} {route.next_hop:<10} " f"{route.learned_interface:<12}") def print_outgoing_updates(self): """Show what would be advertised on each interface.""" print(f"\n=== {self.router_id} Outgoing Updates ===") for iface_name in self.interfaces: update = self.build_update_for_interface(iface_name) print(f" Interface {iface_name}:") if update: for dest, metric in update.items(): metric_str = str(metric) if metric < self.INFINITY else "∞" print(f" {dest}: {metric_str}") else: print(" (empty update)") def demo_split_horizon_comparison(): """Demonstrate difference between split horizon modes.""" print("=" * 60) print("SPLIT HORIZON MODE COMPARISON") print("=" * 60) # Same topology for each mode: A -- B -- C -- X modes = [ SplitHorizonMode.DISABLED, SplitHorizonMode.SIMPLE, SplitHorizonMode.POISON_REVERSE ] for mode in modes: print(f"\n{'='*60}") print(f"Mode: {mode.value.upper()}") print('='*60) # Create Router B router_b = SplitHorizonRouter( router_id="B", interfaces=[ Interface("eth0", "C", 1), Interface("eth1", "A", 1), ], mode=mode ) # B learns routes router_b.routing_table["X"] = RouteEntry("X", 2, "C", "eth0") router_b.routing_table["C"] = RouteEntry("C", 1, "C", "eth0") router_b.routing_table["A"] = RouteEntry("A", 1, "A", "eth1") router_b.print_routing_table() router_b.print_outgoing_updates() demo_split_horizon_comparison()Implementations typically track the learned interface rather than the specific neighbor. On point-to-point links this is equivalent. On multi-access networks (Ethernet), all routes learned from any router on that network segment are filtered—a conservative but safe approach.
Simple split horizon omits routes learned from an interface. Split horizon with poison reverse takes a more aggressive approach: instead of omitting, it explicitly advertises those routes with metric infinity.
The Difference:
| Scenario | Simple Split Horizon | Poison Reverse |
|---|---|---|
| Route X learned via eth0 | Don't advertise X on eth0 | Advertise X with cost ∞ on eth0 |
| Effect | Neighbor sees no route | Neighbor explicitly knows route is unavailable |
| Message size | Smaller (fewer entries) | Larger (includes poison entries) |
Why Poison Reverse Helps:
Consider a timing issue:
With simple split horizon, B receives A's (stale) advertisement before the failure propagates. B might briefly think A has an alternate path.
With poison reverse, A always sends "X, cost ∞" to B (since A learned X from B). Even if messages cross, B sees infinity and knows A doesn't have an independent path.
When to Use Poison Reverse:
RIP implementations typically enable split horizon by default. Poison reverse is often an optional configuration. Cisco IOS, for example, uses split horizon with poison reverse on most interface types but may disable it on certain interfaces like Frame Relay or NBMA networks.
Split horizon is not a complete solution to count-to-infinity. It prevents the most common loop scenario but fails in several important cases.
Limitation 1: Multi-Node Loops
Split horizon only breaks two-node loops. Consider a triangle:
A
/ \
/ \
B ——— C ——— X
If A learns X via B, and B learns X via C:
Limitation 2: NBMA (Non-Broadcast Multi-Access) Networks
On networks like Frame Relay where multiple routers connect through the same interface but can't all communicate directly:
Hub
/ | \
A B C (All via same Frame Relay interface on Hub)
Hub learns route X from A. With split horizon, Hub won't advertise X back out that interface—which means B and C never learn about X, even though they should.
Solution: Disable split horizon on NBMA interfaces, or use sub-interfaces with point-to-point links.
Limitation 3: Timing Windows
Even with split horizon, there's a brief window during topology changes where routers may have inconsistent views:
Triggered updates help but don't eliminate this window entirely.
| Scenario | Split Horizon Effective? | Notes |
|---|---|---|
| Two-node loop | Yes ✓ | Primary use case, very effective |
| Three-node loop | Partial | Reduces probability but doesn't prevent |
| Complex topology loops | Limited | Multi-path loops may still form |
| NBMA networks | Can cause problems | May need to be disabled |
| Parallel links | Yes ✓ | Per-interface rule handles correctly |
| Transient loops | Reduces duration | Still possible during convergence |
Always enable split horizon (unless NBMA issues require otherwise), but don't assume it solves all loop problems. Production networks need additional measures: hold-down timers, triggered updates, route poisoning, and—for critical networks—migration to link-state protocols that don't suffer from count-to-infinity.
Split horizon becomes more nuanced on multi-access networks (Ethernet, NBMA) where multiple routers share a single interface.
Ethernet Example:
┌──────────────────────────┐
│ Ethernet LAN │
│ ┌───┐ ┌───┐ ┌───┐ │
│ │ A │ │ B │ │ C │ │
│ └─┬─┘ └─┬─┘ └─┬─┘ │
└─────┴──────┴──────┴──────┘
│
▼
A, B, C all use eth0 to reach each other
Behavior:
NBMA Problem:
┌─────────────────────────────┐
│ NBMA Network (Frame Relay)│
│ ┌───┐ │
│ │Hub│ │
│ └─┬─┘ │
│ ┌────┼────┐ │
│ ┌─▼─┐┌─▼─┐┌─▼─┐ │
│ │ A ││ B ││ C │ │
│ └───┘└───┘└───┘ │
└─────────────────────────────┘
A can only reach B and C through Hub
Hub is the only fully-connected router
Here's the problem:
| Network Type | Default Split Horizon | Recommended Setting |
|---|---|---|
| Point-to-Point | Enabled | Keep enabled |
| Broadcast Multi-Access (Ethernet) | Enabled | Keep enabled |
| NBMA (Frame Relay, ATM) | Disabled by default (Cisco) | Disable or use sub-interfaces |
| Point-to-Multipoint | Varies | Usually enable |
| Tunnel interfaces | Enabled | Usually keep enabled |
Solutions for NBMA:
Disable split horizon: no ip split-horizon on the NBMA interface. Risk: loops become more likely.
Use point-to-point sub-interfaces: Create separate logical interfaces for each PVC. Each is then a simple point-to-point link.
Full mesh topology: If every router can directly reach every other router, split horizon isn't a problem.
Use a different protocol: OSPF handles NBMA networks with DR/BDR elections that don't suffer from split horizon issues.
Cisco IOS Configuration:
! Disable split horizon on Frame Relay interface
interface Serial0/0
encapsulation frame-relay
no ip split-horizon
! Or use sub-interfaces (preferred)
interface Serial0/0.1 point-to-point
ip address 10.1.1.1 255.255.255.252
frame-relay interface-dlci 100
Frame Relay and ATM are largely legacy technologies. Modern networks use Ethernet, MPLS, or IP tunnels where split horizon works normally. However, understanding NBMA issues remains relevant for certifications and occasional legacy network encounters.
Proper verification of split horizon behavior is essential for troubleshooting routing problems.
Cisco IOS Verification Commands:
! Check if split horizon is enabled on an interface
Router# show ip interface fastethernet0/0
...
Split horizon is enabled
...
! View RIP-specific interface settings
Router# show ip protocols
...
Outgoing update filter list for all interfaces is not set
Incoming update filter list for all interfaces is not set
...
Interface Send Recv Triggered RIP Key-chain
FastEthernet0/0 1 1 2
Serial0/0 1 1 2
...
! Debug RIP updates to see what's being sent
Router# debug ip rip
RIP: sending v2 update to 224.0.0.9 via FastEthernet0/0 (10.1.1.1)
RIP: build update entries
10.2.0.0/24 via 0.0.0.0, metric 1, tag 0
10.3.0.0/24 via 0.0.0.0, metric 2, tag 0
! Notice: 10.1.0.0/24 NOT included (learned via this interface)
Troubleshooting Split Horizon Issues:
Symptom: Routes not propagating
Possible causes:
Diagnosis:
Router# debug ip rip
! Watch for routes being built vs. actually sent
! Check if route appears in "build update entries" but not for specific interface
Symptom: Routing loops despite split horizon
Possible causes:
Diagnosis:
Router# show ip route
! Check next hops—if two routers point to each other, loop exists
Router# traceroute 10.0.0.1
! Watch for repeating patterns in hops
Router# debug ip rip events
! Monitor metric changes during convergence
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
def verify_split_horizon_behavior(router_updates: dict, routing_tables: dict) -> dict: """ Analyze update messages to verify split horizon is working. Args: router_updates: {router_id: {interface: {dest: metric}}} routing_tables: {router_id: {dest: RouteEntry}} Returns: Analysis results with any violations detected """ violations = [] correct_behaviors = [] for router_id, interfaces in router_updates.items(): table = routing_tables.get(router_id, {}) for interface, advertised in interfaces.items(): for dest, route in table.items(): learned_from = route.learned_interface if learned_from == interface: # Should NOT appear in update (simple split horizon) # Or should appear with infinity (poison reverse) if dest in advertised: if advertised[dest] < 16: # Not infinity violations.append({ 'router': router_id, 'interface': interface, 'destination': dest, 'issue': f"Route to {dest} learned via {interface} " f"but advertised with metric {advertised[dest]}", 'severity': 'ERROR' }) else: correct_behaviors.append({ 'router': router_id, 'interface': interface, 'destination': dest, 'behavior': 'Poison reverse (correct)' }) else: correct_behaviors.append({ 'router': router_id, 'interface': interface, 'destination': dest, 'behavior': 'Simple split horizon (correct)' }) return { 'violations': violations, 'correct_behaviors': correct_behaviors, 'summary': f"Found {len(violations)} violations, " f"{len(correct_behaviors)} correct behaviors" } def detect_potential_loops(routing_tables: dict) -> list: """ Detect potential routing loops from routing table inspection. """ loops = [] for router_id, table in routing_tables.items(): for dest, route in table.items(): next_hop = route.next_hop if next_hop in routing_tables: next_table = routing_tables[next_hop] if dest in next_table: next_route = next_table[dest] # Check if next hop points back to us if next_route.next_hop == router_id: loop = { 'destination': dest, 'routers': [router_id, next_hop], 'type': 'two-node loop' } # Avoid duplicates loop_key = tuple(sorted([router_id, next_hop]) + [dest]) if not any(l.get('_key') == loop_key for l in loops): loop['_key'] = loop_key loops.append(loop) return loops # Example usagefrom dataclasses import dataclass @dataclassclass RouteEntry: destination: str metric: int next_hop: str learned_interface: str # Simulated routing tablestables = { 'A': { 'X': RouteEntry('X', 3, 'B', 'eth0'), 'B': RouteEntry('B', 1, 'B', 'eth0'), }, 'B': { 'X': RouteEntry('X', 2, 'C', 'eth0'), 'A': RouteEntry('A', 1, 'A', 'eth1'), 'C': RouteEntry('C', 1, 'C', 'eth0'), }} # What each router advertises on each interfaceupdates = { 'A': { 'eth0': {'A': 0}, # Correct: X not included (learned from eth0) }, 'B': { 'eth0': {'A': 1, 'B': 0}, # Correct: X and C not included 'eth1': {'X': 2, 'C': 1, 'B': 0}, # Correct: A not included }} result = verify_split_horizon_behavior(updates, tables)print("Split Horizon Verification:")print(f" {result['summary']}")for v in result['violations']: print(f" VIOLATION: {v['issue']}")for c in result['correct_behaviors']: print(f" OK: {c['router']} on {c['interface']}: {c['behavior']}")When analyzing debug ip rip output, pay attention to which routes are built ('build update entries') versus what's actually sent on each interface. Routes that appear in the build but not in the interface-specific send are being filtered by split horizon or route filters.
We've thoroughly examined split horizon—the primary defense against routing loops in distance vector protocols. Let's consolidate our understanding:
What's Next: Poison Reverse
While we've introduced poison reverse as a split horizon variant, it deserves deeper examination as a standalone technique. The final page of this module explores poison reverse in full detail—its mechanism for accelerating loop detection, interaction with triggered updates, implementation patterns, and comparison with related techniques like route poisoning and hold-down timers.
You now have comprehensive understanding of split horizon—how it works, why it's effective against two-node loops, its limitations, and practical deployment considerations. This technique is fundamental to stable distance vector routing operation.