---
title: 'Routing Decision'
source: 'https://academia.sh/en/courses/switching-and-routing/routing-decision'
course: 'Switching and Routing'
language: en
updated: '2026-08-17T18:07:20+00:00'
license: 'CC BY-SA 4.0'
---

# Routing Decision

The routing decision is made by looking not at what the message says but at the device's own table; when the same table is read with two different rules, it diverges on 12 of the 256 addresses, and because the table is a copy of a shared reality, at the moment of the break 26 of the 40 packets reach their destination and 14 fall into a black hole.

Every decision covered so far was made **within the same local network**. A switch looked
up the frame's destination in its own address table; when it found an entry, it sent the
frame straight out that port, and when it did not, it flooded. The destination was in
every case a **direct neighbor**: inside the same broadcast domain, one hop away.

If the destination is on another network, this setup does not work. It is not a direct
neighbor, it has no port entry in the table, and flooding is not an option — a network of
networks cannot be flooded. The decision now has to answer not "which port" but **"whom
do I hand it to"**. This lesson's question is where that answer is read from.

## What the Decision Looks At

The decision a router makes is called the **routing decision**, and it has exactly one
output: the **next hop**. The packet's destination address is the input, one of the
neighboring nodes is the output. The router does not know the entire path to the
destination, and it does not need to; it knows only **one hop** and hands the packet
there. The rest of the path is the next node's problem.

The decision is not made by looking at the packet itself. The packet carries its
destination address and nothing else; it does not say which neighbor gets closer to that
destination. What says that is the device's own **routing table**, held in memory. The
table is a set of rows that maps destination-address prefixes to next hops:

```text
# taught transcript, not run

routing table — rows in the order they were written

  prefix      length   next hop
  1010            4    north
  10110           5    west
  101011          6    east
  (empty)         0    central
  10101100        8    local
  10100           5    south
```

This transcript is not a specification but a reading of a device's memory; it has not
been run and no numeric claim follows from it. The only thing to notice is the **order**
of the rows: they were added over time and are not sorted by length. The last row carries
a longer prefix than the first.

The empty-prefix row is special: its length is zero and it matches every address. This
row is the **default route**, and it covers whatever the table leaves out. Because of it,
the table never has to enumerate every address in the world.

## Longest Prefix Match

In the table above, an address can match more than one row. An address beginning with
`10101100` fits four of the five rows. Which one wins?

The rule is called **longest prefix match**: among the matching rows, the one with the
longest prefix is chosen. The reasoning is the amount of information. A longer prefix
covers a narrower set of addresses, and is therefore a **more precise** statement about
that set. A shorter prefix lumps a wide set together with a coarse approximation. When
two statements conflict, the precise one wins.

The real point here is this: **the rule is not written inside the table.** The table
carries only the rows; the rule that decides which row wins lives in the device's
specification. A device that reads the same set of rows with a different rule makes a
different decision. The most common wrong rule is to scan the rows in the order they were
written and take the **first match**.

## The Table Is a Copy

In the Linux Network Administration and Troubleshooting course, the routing table was
read as **a single machine's configuration**: a file, a command's output, a reality
belonging to that machine. Here, the table is not a configuration but **a copy of a
shared reality** — the network's topology is one, but every device keeps its own copy of
it and reads its decision from that copy.

As long as the copies agree, this distinction is invisible. It becomes visible the moment
the topology changes and the copies have not yet been updated. The state in which every
copy matches reality is called **convergence**; what is measured throughout the course is
not convergence itself but **the time spent not converged**.

During that time, a packet has three possible fates. **Reached**: it arrived at its
destination. **Black hole**: a node's table pointed to a neighbor that no longer exists,
and the packet died there. **Loop**: the tables sent the packet back and forth to each
other, and it circled until it burned through the hop limit.

The measured network has eight nodes. Seven links form a ring, and the eighth is a chord
that cuts across the ring. Because the chord short-circuits the two sides of the ring,
most tables route through it; when it breaks, a large number of rows are forced to change
their decision. Reality itself — the **oracle** — is known because we built the topology
ourselves: it is the first hop of the shortest path from each node to every destination.

The measurement's assumptions:

- **RT1** — The network has eight nodes and nine links; seven links form the ring, and
  the ninth is the chord that cuts across it. The oracle is known because we built the
  topology ourselves.
- **RT2** — The oracle is the first hop of the shortest path from each node to every
  destination, and it is not changed during the measurement. Reality is always the
  shortest path.
- **RT3** — The measured break is only the loss of the chord: one link drops, there is no
  node loss, and the network does not partition. After the break, every destination still
  has a path.
- **RT4** — Forty source–destination pairs are generated from a fixed seed. The same pair
  can come up more than once, and the measurement counts each recurrence as a separate
  packet.
- **RT5** — The hop limit is 12; a packet that burns through it is counted as a loop. In
  an eight-node network, the longest loop-free path is seven hops, well below the limit.
- **RT6** — In the prefix measurement, the address is eight bits wide and the **entire**
  space is scanned; no sample is chosen. Table rows stay in the order they were written.
- **RT7** — The set's resolution is the measurement band's floor: in a set of forty
  packets, the smallest measurable difference is **1/40 = 0.025**; in the address space,
  **1/256**.

## Same Table, Two Rules

First the rule itself is measured. The six-row table above is read with two different
rules, and the entire eight-bit address space is scanned.

```python
"""Longest prefix match: same table, two reading rules.

Rows stay in the order they were written, not sorted by length; the
entire eight-bit address space is scanned, no example is picked.
"""
TABLE = [("1010", "north"), ("10110", "west"), ("101011", "east"),
         ("", "central"), ("10101100", "local"), ("10100", "south")]
NODES = ("north", "south", "east", "west", "central", "local")
ADDRESSES = [format(a, "08b") for a in range(256)]


def longest_prefix(address):
    chosen = None
    for prefix, node in TABLE:
        if address.startswith(prefix) and (chosen is None or len(prefix) > len(chosen[0])):
            chosen = (prefix, node)
    return chosen


def first_match(address):
    for prefix, node in TABLE:
        if address.startswith(prefix):
            return (prefix, node)
    return None


diverging = [a for a in ADDRESSES if longest_prefix(a)[1] != first_match(a)[1]]
print(f"table rows {len(TABLE)} | address space {len(ADDRESSES)} | "
      f"addresses where the two rules diverge {len(diverging)}")
print()
print(f"{'next hop':>18s} {'longest prefix':>14s} {'first match':>12s}")
for node in NODES:
    print(f"{node:>18s} {sum(longest_prefix(a)[1] == node for a in ADDRESSES):14d}"
          f" {sum(first_match(a)[1] == node for a in ADDRESSES):12d}")
print()
for a in (diverging[0], diverging[8], diverging[11]):
    u, i = longest_prefix(a), first_match(a)
    print(f"  {a} -> longest prefix {u[1]} ({u[0]}), first match {i[1]} ({i[0]})")
```

```
table rows 6 | address space 256 | addresses where the two rules diverge 12

          next hop longest prefix  first match
             north              4           16
             south              8            0
              east              3            0
              west              8            8
           central            232          232
             local              1            0

  10100000 -> longest prefix south (10100), first match north (1010)
  10101100 -> longest prefix local (10101100), first match north (1010)
  10101111 -> longest prefix east (101011), first match north (1010)
```

The two rules diverge on **12** of the 256 addresses. The divergence runs one way: a
device reading by row order sends **16** addresses to `north`, one reading by longest
prefix sends **4**. Of the twelve addresses in between, eight should have gone to
`south`, three to `east`, one to `local`. The `central` column is **232** in both: the
default route remains the last resort in both rules, because for addresses that match no
longer prefix, it is the only match.

The **8** in the `west` column is also the same in both, and this is not a coincidence:
the prefix `10110` overlaps with no shorter prefix, and no other row in the table
contains it. **Divergence is born only from nested prefixes.** If the table had no
nesting, the two rules would give the same decision, and the rule would not need to be
written into the specification at all.

## Before and After the Break

Now the rule is held fixed and the **table** changes. Three generations are placed side
by side: tables converged with no break, tables that are still stale at the moment of the
break, and tables converged again after the break. The same forty packets are run through
all three between the same pairs.

```python
"""The table is a copy: same rule, three table generations.

Eight nodes, a chord that cuts the ring; the measured break is that
chord's loss.
"""
SEED = 20260810
NODES = ["a", "b", "c", "d", "e", "f", "g", "h"]
LINKS = [("a", "b"), ("b", "c"), ("c", "d"), ("d", "e"), ("e", "f"),
         ("f", "g"), ("g", "h"), ("h", "a"), ("b", "f")]
BROKEN = [b for b in LINKS if b != ("b", "f")]
HOP_LIMIT = 12


def generator(seed):
    d = seed % 2147483646 + 1

    def r(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def neighbors(links):
    k = {u: set() for u in NODES}
    for x, y in links:
        k[x].add(y)
        k[y].add(x)
    return k


def oracle(links):
    """The first hop of the shortest path from each node to each destination."""
    k, table = neighbors(links), {}
    for source in NODES:
        prev, frontier, seen = {}, [source], {source}
        while frontier:
            nxt = []
            for u in frontier:
                for v in sorted(k[u]):
                    if v not in seen:
                        seen.add(v)
                        prev[v] = u
                        nxt.append(v)
            frontier = nxt
        for dest in NODES:
            if dest == source or dest not in prev:
                continue
            step = dest
            while prev[step] != source:
                step = prev[step]
            table[(source, dest)] = step
    return table


def forward(source, dest, tables, links):
    """Runs the packet through the tables; returns its fate, hop count, and where it died."""
    k, u, crossed, hop = neighbors(links), source, [], 0
    while u != dest:
        if hop >= HOP_LIMIT:
            return "loop", hop, u
        nxt = tables.get((u, dest))
        if nxt is None or nxt not in k[u]:
            return "black hole", hop, u
        if (u, nxt) in crossed:
            return "loop", hop, u
        crossed.append((u, nxt))
        u, hop = nxt, hop + 1
    return "reached", hop, None


def pairs(count=40, seed=SEED):
    r, result = generator(seed), []
    while len(result) < count:
        x, y = NODES[r(8)], NODES[r(8)]
        if x != y:
            result.append((x, y))
    return result


def measure(tables, links, count=40):
    tally, hops = {"reached": 0, "loop": 0, "black hole": 0}, 0
    for x, y in pairs(count):
        fate, h, _ = forward(x, y, tables, links)
        tally[fate] += 1
        hops += h
    return tally, hops


old, new = oracle(LINKS), oracle(BROKEN)
print(f"nodes {len(NODES)} | links {len(LINKS)} | table rows {len(old)} | "
      f"measured pairs {len(pairs())} | hop limit {HOP_LIMIT}")
print()
print(f"{'table generation':<27s} {'reached':>7s} {'loop':>6s} {'black hole':>11s} {'hops':>5s}")
for name, tab, links in (("converged, no break", old, LINKS),
                          ("at break, tables stale", old, BROKEN),
                          ("after break, converged", new, BROKEN)):
    s, h = measure(tab, links)
    print(f"{name:<27s} {s['reached']:7d} {s['loop']:6d} {s['black hole']:11d} {h:5d}")
print()
changed = sum(1 for key in old if old[key] != new[key])
print(f"rows whose decision must change after the break: {changed}/{len(old)}")
died = {}
for x, y in pairs():
    fate, _, node = forward(x, y, old, BROKEN)
    if fate != "reached":
        died[node] = died.get(node, 0) + 1
print("node where the packet died:", dict(sorted(died.items())))
```

```
nodes 8 | links 9 | table rows 56 | measured pairs 40 | hop limit 12

table generation            reached   loop  black hole  hops
converged, no break              40      0           0    83
at break, tables stale           26      0          14    61
after break, converged           40      0           0    99

rows whose decision must change after the break: 11/56
node where the packet died: {'b': 6, 'f': 8}
```

## Reading the Break

The three rows say three separate things.

**In the converged network, all forty of the forty packets reach their destination**, in
a total of 83 hops. This is the state where the tables agree with reality, and it is the
measurement's baseline. The bottom row gives the same result after the break: again
**40/40**, but with **99** hops. Once the chord is gone, paths get longer; that is the
sixteen-hop difference. What the network loses is not connectivity but shortness — **no
packet is lost**.

The middle row is the time in between. While the tables still point at the chord, **26
packets reach their destination and 14 fall into a black hole.** That fourteen is a
measurable share: **0.350** of the set of forty packets, fourteen times the band's floor.

The total hop count dropping to **61** here looks like an improvement at first glance,
and that is exactly why it needs attention. Hops went down because a third of the packets
**did not continue on their way**: a packet that falls into a black hole dies immediately
and does not spend its remaining hops. **A low hop count here is not a success, it is the
signature of a loss.**

The last two rows say where the fault is. The number of rows whose decision must change
after the break is **11**; the table carries fifty-six rows, so the large majority of
rows stay correct. And yet fourteen packets die. The reason is that **every** dying
packet dies at one of two nodes: six at **b**, eight at **f**. These are the two ends of
the broken chord.

What follows from this is the course's axis. A packet's fate **does not depend on the
correctness of the table at the node it leaves from.** The source node's row can be
correct; the packet, along the way, passes through a node whose table is wrong and dies
there. Along the chain of decisions, it is the **oldest copy** that determines the
outcome.

## Summary

- The routing decision's input is the packet's destination address, its output is the
  next hop; the router knows not the whole path but only one hop.
- The decision is made by looking not at what the message says but at the device's
  **own table**; the rule that determines the decision lives in the specification, not
  the table.
- Longest prefix match chooses the narrowest set among the matching rows; a rule that
  reads by row order gives a different decision on **12** of the 256 addresses in the
  same table, and the divergence is born only from nested prefixes.
- The table is not a configuration but a copy of a shared reality; in a converged
  network, all forty of the forty packets reach their destination, in **83** hops before
  the break and **99** after.
- At the moment of the break, with stale tables, **26** packets reach their destination
  and **14** fall into a black hole; the drop in hops to **61** is not a gain, it is
  dying packets not spending hops.
- Only **11** of the fifty-six rows are wrong, but all fourteen dying packets die at the
  two ends of the broken link: what determines the fate is not the source's copy but the
  **oldest** copy along the path.

## Next Step

In this measurement, the tables were built by hand twice: once from the reality before
the break, once from the reality after it. Nobody made the transition in between — the
second-generation table was handed to the measurement already made. In a real network,
someone has to make that transition, and there are two options: an administrator writes
the table, or the devices tell each other. The next lesson puts these two options side by
side and asks: when does a hand-written table learn about a break, and what does the
black-hole count do over time?
