Link-State Routing and Dijkstra's Algorithm

Imagine an internet service provider running hundreds of routers across a continent. Any single fiber cut must trigger a network-wide correction within a second or two. This must happen without any router ever seeing more than local hardware alerts. This is not a hypothetical. A stale route can black-hole traffic for millions of users. A slow-converging protocol can loop packets forever, exhausting their time-to-live field. Link-state routing is the classical answer to this problem. It underlies the intra-domain routing that keeps enterprise and carrier networks alive today. The core insight is deceptively simple. If every router in a network can obtain a complete, identical map of the topology, then each router can independently compute the best route to every destination. It does this using a well-understood graph algorithm. This decouples the distribution problem (getting the map to everyone reliably) from the computation problem (finding shortest paths on that map). Link-state routing solves distribution with a controlled flooding protocol. It solves computation with Edsger Dijkstra's shortest-path algorithm, applied locally by each router to its own copy of the graph. This article builds the two halves precisely. First, it explains how link-state advertisements are flooded so that every router converges on the same topology database. It also explains why this process terminates correctly despite unreliable links and out-of-order delivery. Second, it walks through Dijkstra's algorithm step by step. It proves why the greedy strategy is optimal for non-negative edge weights. It derives its running time under different data structures. The article closes by tracing this machinery into real systems. These include the OSPF and IS-IS protocols that run the internet's backbones. It contrasts these with the alternative distance-vector philosophy. By the end, you should be able to hand-simulate the flooding of a link-state packet across a small network. You should be able to hand-trace Dijkstra's algorithm to build a shortest-path tree. You should be able to state and justify its complexity under an array versus a min-heap implementation. Finally, you should be able to explain precisely why link-state convergence is fast and loop-free compared to distance-vector alternatives.

Key takeaways
  • Link-state routing gives every router a full copy of the network graph via flooding, then each router runs Dijkstra's Algorithm locally to compute shortest paths.
  • Flooding uses sequence numbers and reliable per-link acknowledgment so link-state packets (LSPs) reach all routers exactly once in the steady state, despite retransmission and duplication.
  • Dijkstra's algorithm greedily grows a set of "settled" nodes in strictly increasing distance order, which is provably correct only when all edge weights are non-negative.
  • With a binary min-heap, Dijkstra runs in O((V+E)log⁡V)O((V+E)\log V) time; with a simple array it runs in O(V2)O(V^2), better for dense graphs.
  • Because every router computes from an identical, consistent database, link-state routing avoids the persistent routing loops that plague naive Distance-Vector Routing and Bellman-Ford designs.
  • OSPF and IS-IS are the production link-state protocols; both flood LSPs inside areas and run Dijkstra to build a shortest-path tree rooted at each router itself.
The core insight

Give every router the same map of the network, and let each one independently run Dijkstra's algorithm to find its own shortest paths.

The idea

A link-state routing algorithm is built from two logically separate mechanisms: topology dissemination and path computation. Every router first constructs a link-state packet (LSP), sometimes called a link-state advertisement, describing itself: its identifier, the identifiers of its directly attached neighbors, and the cost (weight) of each attached link, together with a monotonically increasing sequence number. Every router then floods its LSP to all other routers in the domain using a reliable flooding protocol, so that in steady state every router holds an identical link-state database (LSDB) — a complete, weighted graph of the network. Once a router has the full graph, it treats itself as the source vertex and runs Dijkstra's shortest-path algorithm on the LSDB to compute a shortest-path tree rooted at itself, giving the least-cost path (and hence the next hop) to every other router. Because all routers compute from the same graph, and because Dijkstra's algorithm is deterministic given a graph and a source, all routers agree — modulo tie-breaking rules — on a globally consistent set of paths, which is precisely what prevents persistent forwarding loops. This is fundamentally different from distance-vector routing, where each router knows only its neighbors' reported distances, not the topology itself, and computes paths via a distributed, iterative relaxation (Bellman–Ford in disguise). Link-state trades a larger amount of flooded state — the entire topology, held by every router — for faster, more predictable convergence and freedom from the "count-to-infinity" failure mode. The two philosophies (link-state vs. distance-vector) are the two archetypal answers to the routing problem, which itself is precisely the shortest-path problem on the graph abstraction of a network, where routers are vertices and links are weighted edges.

How it works

Link-state routing operates in two phases: flooding to build the LSDB, then per-router shortest-path computation.

Phase 1 — Reliable flooding

Each router periodically (or on a topology change) generates an LSP describing its local link state. Flooding must guarantee that every router in the domain eventually receives every router's latest LSP exactly once, even though the underlying links can drop, duplicate, or reorder packets. This is achieved with:

  • A sequence number in each LSP, incremented every time the originating router issues a new one. Routers discard any LSP whose sequence number is not newer than the copy already stored.
  • An age field (time-to-live) that decrements over time or hops, so that stale LSPs from routers that have crashed or been removed eventually get purged from every LSDB even without an explicit withdrawal.
  • Flood-and-acknowledge on every link: when a router receives a new LSP on one interface, it immediately forwards ("floods") a copy out every other interface, and acknowledges receipt back to the sender. This guarantees an LSP originating anywhere reaches every router, since the network graph is connected.

``` function ON_RECEIVE_LSP(lsp, incoming_link): stored = LSDB.lookup(lsp.origin) if stored is None or lsp.seq_num > stored.seq_num: LSDB.update(lsp.origin, lsp) # accept newer information for link in ROUTER.links: if link != incoming_link: SEND(lsp, link) # flood outward, not back ACK(lsp, incoming_link) SCHEDULE_DIJKSTRA() # topology changed, recompute else if lsp.seq_num == stored.seq_num: ACK(lsp, incoming_link) # duplicate, just acknowledge else:

# lsp is older than what we have — tell the sender the truth SEND(stored, incoming_link) ``` This flood terminates: each router forwards a given (origin, sequence-number) pair at most once per interface, because the second time it arrives the stored copy is no longer strictly older. On a graph with VV routers and EE bidirectional links, the total number of LSP transmissions for one flooded update is O(E)O(E), since every link is traversed a bounded number of times in each direction.

Phase 2 — Dijkstra's shortest-path computation

Once a router's LSDB stabilizes, it runs Dijkstra's algorithm with itself as the source vertex ss. The algorithm maintains a set SS of vertices whose shortest distance from ss is already finalized ("settled"), and a tentative distance array d[⋅]d[\cdot] for all others. ``` function DIJKSTRA(Graph G, source s): for each vertex v in G: d[v] = INFINITY prev[v] = UNDEFINED d[s] = 0 S = empty set # settled vertices Q = min-priority-queue of all vertices, keyed by d[] while Q is not empty: u = Q.extract_min() # vertex with smallest tentative distance if u in S: continue # stale entry, already settled add u to S for each edge (u, v) with weight w(u, v): if v not in S and d[u] + w(u, v) < d[v]: d[v] = d[u] + w(u, v) prev[v] = u Q.decrease_key(v, d[v]) # or insert a new entry, if lazy deletion return d, prev # d[v] = shortest distance, prev[v] = predecessor ``` At termination, `prev[]` encodes a shortest-path tree rooted at `s`; the router walks this tree back one hop from each destination to itself to determine the correct next hop, which is the only thing it actually needs to populate its forwarding table.

Complexity & correctness

Flooding complexity. Let VV be the number of routers and EE the number of bidirectional links. Each router's LSP is forwarded across every link it did not arrive on, and duplicate-suppression via sequence numbers guarantees each link carries a bounded number of copies (in practice one per direction, plus retransmissions on loss). Total message complexity for propagating one LSP update is O(E)O(E). Since each router stores one LSP per origin, the space required for the LSDB is O(V+E)O(V + E) per router — the entire topology, replicated at every router. This is the structural price of link-state routing: it trades memory and flooding bandwidth for computational independence and fast convergence.

Dijkstra's time complexity. The algorithm performs exactly VV `extract_min` operations (each vertex is settled once) and, across the whole run, at most EE relaxation steps, since each edge is examined exactly once, from its tail endpoint when that endpoint is settled.

  • With an unsorted array holding tentative distances, `extract_min` costs O(V)O(V) (linear scan) and `decrease_key` costs O(1)O(1). Total: O(V⋅V+E⋅1)=O(V2)O(V \cdot V + E \cdot 1) = O(V^2).
  • With a binary min-heap, `extract_min` costs O(log⁡V)O(\log V) and `decrease_key` costs O(log⁡V)O(\log V) (or, with lazy deletion and re-insertion, an `insert` at O(log⁡V)O(\log V), tolerating stale duplicate entries by skipping them, as the pseudocode above does). Total: O((V+E)log⁡V)O((V + E)\log V).
  • With a Fibonacci heap, `decrease_key` is O(1)O(1) amortized and `extract_min` is O(log⁡V)O(\log V) amortized, giving O(E+Vlog⁡V)O(E + V\log V) — the asymptotically optimal bound for comparison-based Dijkstra, though the large constant factors make it rarely used in practice.

For router-scale graphs (hundreds to low thousands of vertices, sparse, E=O(V)E = O(V)), the binary-heap bound O((V+E)log⁡V)≈O(Vlog⁡V)O((V+E)\log V) \approx O(V \log V) is what real implementations use; the O(V2)O(V^2) array version is only preferable when the graph is dense (EE close to V2V^2), which router topologies never are.

Why the greedy strategy is correct. Dijkstra's correctness rests on one crucial precondition: all edge weights are non-negative. The proof is by induction on the order in which vertices are added to the settled set SS. Invariant: whenever a vertex uu is extracted from the priority queue and added to SS, its tentative distance d[u]d[u] equals the true shortest-path distance δ(s,u)\delta(s, u). Base case: the source ss is settled first with d[s]=0=δ(s,s)d[s] = 0 = \delta(s,s), trivially correct. Inductive step: suppose the invariant holds for every vertex already in SS. Let uu be the next vertex extracted, so d[u]d[u] is minimal among all vertices outside SS. Consider any path from ss to uu. It must leave SS at some point, crossing an edge (x,y)(x, y) where x∈Sx \in S and y∉Sy \notin S. Because edge weights are non-negative, the distance to yy along this path is at most the distance to uu along it, and by the time xx was settled, d[y]d[y] was already updated to at most δ(s,x)+w(x,y)\delta(s, x) + w(x,y) via relaxation. Since uu was chosen as the minimum tentative-distance vertex remaining, d[u]≤d[y]≤d[u] \le d[y] \le (length of any path through the boundary) ≤\le (length of the path to uu). Since this holds for every path to uu, in particular the true shortest one, d[u]≤δ(s,u)d[u] \le \delta(s,u). But d[u]d[u] is always an upper bound on δ(s,u)\delta(s,u) by construction (it is the cost of some actual path found via relaxation), so d[u]=δ(s,u)d[u] = \delta(s,u), completing the induction. This argument fails with negative edge weights: a vertex could be settled with a tentative distance that a later, negative-weight edge would have improved, but by then it is no longer reconsidered. Network link costs (OSPF metrics, IS-IS metrics) are configured to be strictly positive precisely so that this precondition always holds.

Why link-state avoids routing loops. Because every router computes shortest paths from an identical LSDB using a deterministic algorithm, the derived next-hop choices are mutually consistent: if router A's shortest path to destination Z goes through B, then B's own shortest path to Z (computed independently on the same graph) cannot loop back through A, since that would contradict A's path being shortest. Transient loops can still occur for a few milliseconds while different routers hold momentarily different, not-yet-converged LSDBs, but these self-heal as soon as flooding completes — unlike distance-vector protocols, where inconsistent partial information can cause the persistent count-to-infinity problem.

Worked example

Consider the five-router network below (undirected, weighted), and trace Dijkstra's algorithm from source AA. Edges: AA–BB (4), AA–CC (1), CC–BB (2), CC–DD (5), BB–EE (1), DD–EE (3), DD–BB (8).

StepExtracted (settled)d[A]d[A]d[B]d[B]d[C]d[C]d[D]d[D]d[E]d[E]Relaxations performed
init—0∞∞∞∞—
1A (0)041∞∞d[B] ⁣= ⁣4d[B]\!=\!4 via A, d[C] ⁣= ⁣1d[C]\!=\!1 via A
2C (1)0316∞d[B] ⁣= ⁣min⁡(4,1+2)=3d[B]\!=\!\min(4, 1+2)=3 via C, d[D] ⁣= ⁣1+5=6d[D]\!=\!1+5=6 via C
3B (3)03164d[D] ⁣= ⁣min⁡(6,3+8)=6d[D]\!=\!\min(6, 3+8)=6 unchanged, d[E] ⁣= ⁣3+1=4d[E]\!=\!3+1=4 via B
4E (4)03164d[D] ⁣= ⁣min⁡(6,4+3)=6d[D]\!=\!\min(6, 4+3)=6 unchanged (tie, no improvement)
5D (6)03164no outgoing edges left to relax

The final shortest-path tree rooted at AA: A ⁣→ ⁣CA\!\to\!C (1), A ⁣→ ⁣C ⁣→ ⁣BA\!\to\!C\!\to\!B (3), A ⁣→ ⁣C ⁣→ ⁣B ⁣→ ⁣EA\!\to\!C\!\to\!B\!\to\!E (4), A ⁣→ ⁣C ⁣→ ⁣DA\!\to\!C\!\to\!D (6). Router AA's forwarding table now reads: to reach BB, CC, DD, or EE, send first to CC (the next hop shared by every one of those routes, since CC is the sole neighbor of AA on all of them here) — a small but real illustration of how a multi-hop shortest-path tree collapses into simple one-hop forwarding decisions at the source. Notice the tie at step 4: both the direct path AA–CC–DD (cost 6) and the alternate AA–CC–BB–EE–DD (cost 1+2+1+3=71+2+1+3=7) were considered, and Dijkstra correctly rejected the longer one — the greedy invariant guarantees this without ever having to reconsider a settled vertex.

Common pitfalls & traps

PitfallWhy it happensConsequence
Assuming Dijkstra works with negative weightsThe greedy settle-once invariant relies on non-negative weights (see proof)Silently wrong shortest paths; use Bellman–Ford instead if negatives are possible
Re-processing a stale queue entry as if it were currentLazy `decrease_key` (re-insert instead of true decrease) leaves old entries in the queueMust check `if u in S: continue` or distances get corrupted by outdated relaxations
Treating link-state cost as hop count onlyReal metrics (bandwidth-based, delay-based) matter for path qualitySuboptimal paths chosen even though the algorithm itself is correct
Forgetting LSP aging / sequence-number wraparoundOnly comparing sequence numbers without handling wraparound or age-based expiryStale topology entries for crashed routers persist indefinitely, causing black holes
Not synchronizing LSDBs before computing routesRunning Dijkstra mid-flood, before all LSPs have arrivedTransient inconsistent forwarding tables, brief micro-loops during convergence
Ignoring flooding storms on unstable linksA flapping link keeps generating new LSPs, retriggering full-network floodsExcessive CPU/bandwidth use; mitigated with LSP generation throttling ("exponential backoff")

Pioneers & history

Link-state routing and Dijkstra's algorithm come from two different, later-merged, threads: theoretical graph algorithms, and practical internet engineering under the ARPANET program.

Origins · 1 minEdsger W. Dijkstra and how this was discovered
EW
Edsger W. Dijkstra
1930–2002 · Dutch computer scientist, Mathematical Centre Amsterdam and University of Texas at Austin

Dijkstra devised his shortest-path algorithm in 1956, reportedly in about twenty minutes while sitting in a café, as a demonstration of what a simple computer should be able to do. He published it in 1959 in a short paper alongside his minimum spanning tree method. Beyond this algorithm, Dijkstra was a foundational figure in structured programming, formal program verification, and concurrent computing, and a fierce advocate for treating programming as a rigorous mathematical discipline rather than a craft.

History
1956
— Edsger Dijkstra conceives the shortest-path algorithm that bears his name.
1959
— Dijkstra publishes the algorithm, establishing the greedy shortest-path method used ever since.
1979
— The ARPANET migrates from a distance-vector-style scheme to a link-state design after severe routing instabilities.
1990
— The OSPF (Open Shortest Path First) protocol is standardized, formalizing link-state flooding and Dijkstra computation for IP networks.
1990
s — IS-IS, originally designed for OSI networks, is adapted to carry IP routes and becomes the other dominant link-state protocol in large carrier backbones.

The ARPANET's original routing protocol used a distributed Bellman–Ford-style distance-vector scheme, which suffered severe oscillations and slow convergence under congestion and topology changes in the late 1970s. The 1979 redesign, led by engineers including John McQuillan, replaced it with a link-state approach: full topology flooding plus local Dijkstra computation, directly inspired by the theoretical shortest-path algorithm developed two decades earlier. This redesign became the direct ancestor of both OSPF and IS-IS, the two link-state protocols that dominate enterprise and carrier routing today.

Where this matters

Link-state routing runs the interior of almost every large IP network. OSPF (Open Shortest Path First) and IS-IS (Intermediate System to Intermediate System) are the two production link-state protocols; both flood LSPs (called Router-LSAs in OSPF, LSPs in IS-IS) within an administrative area, and both run Dijkstra locally at every router to build the shortest-path tree that populates the forwarding table. ISPs use IS-IS or OSPF as the "interior gateway protocol" that finds paths within their own network, while BGP handles routing between independently administered networks — a boundary that reflects the practical limit of how far full-topology flooding can scale before the LSDB becomes too large to flood and store everywhere. Beyond wide-area networking, the same idea appears in data-center fabrics (some use link-state-like designs at Layer 3), in wireless mesh routing protocols such as OLSR (Optimized Link State Routing), and conceptually in any distributed system where nodes need a globally consistent view before making a local decision — a pattern echoed in consensus and state-machine replication, though those solve a different (stronger) consistency problem. Dijkstra's algorithm itself, independent of networking, underlies GPS navigation systems, game AI pathfinding, and any application computing shortest paths on a weighted graph, making it one of the most reused algorithmic primitives in computer science.

Ask the tutor: Can you walk me through why Dijkstra's algorithm fails on a graph with a negative-weight edge, using a concrete 3-vertex counterexample?

Why does link-state routing need every router to store the entire network topology, instead of just its neighbors' distances?

Because each router computes shortest paths independently, it needs enough information to run a complete shortest-path algorithm on its own. Dijkstra's algorithm requires the full graph — all vertices and all weighted edges — not just local neighbor information, since a shortest path to a distant destination may need to be recomputed from scratch whenever any edge anywhere in the network changes. This is the structural tradeoff of link-state routing: it uses more memory and more flooding bandwidth than distance-vector routing, in exchange for computational independence, deterministic consistency across routers, and faster, more predictable convergence when the topology changes. Distance-vector protocols avoid storing the full topology but pay for it with slower convergence and loop-prone behavior, especially under link failures.

What happens if two routers receive LSPs for the same origin with the same sequence number but different content?

In a well-formed link-state protocol this should not happen, since only the originating router increments its own sequence number, and it does so exactly once per new LSP it issues. If duplicate sequence numbers with differing content do appear — due to a bug, a compromised router, or a wraparound bug — most implementations fall back to comparing an additional checksum or age field to break the tie, and flag the anomaly for operator attention. Real protocols such as OSPF include a checksum field in each LSA precisely as a second line of defense, and treat checksum mismatches as corruption requiring the LSA to be discarded and re-requested rather than blindly accepted.

Can Dijkstra's algorithm be adapted to handle negative edge weights at all?

Not directly — the greedy settle-once strategy fundamentally assumes weights are non-negative, as the correctness proof shows. If negative weights (but no negative cycles) are possible, the correct tool is the Bellman–Ford algorithm, which relaxes all edges repeatedly (V−1 times in the worst case) rather than settling vertices permanently after one visit, at the cost of a higher O(VE)O(VE) running time. Some specialized variants (like Johnson's algorithm) reweight the graph using Bellman–Ford first to eliminate negative weights, then run Dijkstra on the reweighted graph, which is useful for all-pairs shortest paths on sparse graphs but is not used in production network routing, where metrics are simply defined to be positive.

How quickly does link-state routing actually converge after a link fails?

Convergence time is the sum of three components: detection time (how fast the failure is noticed, often tens of milliseconds with modern fast-hello mechanisms), flooding time (proportional to network diameter, typically well under a second for the LSP to reach every router), and recomputation time (Dijkstra itself, which on a router-scale graph with a few thousand vertices completes in low milliseconds with a heap implementation). In practice, well-tuned OSPF or IS-IS deployments converge within roughly one second after a link failure, dramatically faster than classical distance-vector designs, which could take tens of seconds to minutes due to the count-to-infinity problem before route-poisoning and split-horizon mitigations were added.

Does every router really need to run its own copy of Dijkstra's algorithm, or could the computation be centralized?

In classical link-state protocols like OSPF and IS-IS, yes — every router runs Dijkstra locally, because the whole design goal is for routing decisions to survive the failure of any single device, including a centralized route computation server. Centralizing the computation is exactly the idea behind software-defined networking (SDN) controllers, which do compute paths centrally and push forwarding rules to switches, trading some resilience and per-router autonomy for centralized policy control and simpler debugging. Both approaches still use graph algorithms like Dijkstra internally; the difference is purely about where the computation happens, not what algorithm is used.

Why is the shortest path measured by "cost" rather than simply the number of hops?

A hop-count metric treats every link as equally good, which is unrealistic: a satellite link and a fiber link both count as "one hop" despite wildly different bandwidth and latency. Real link-state protocols assign each link a configurable cost, often inversely related to bandwidth (as OSPF does by default) or set manually by network operators to express policy preferences, such as avoiding an expensive backup link unless necessary. Dijkstra's algorithm is agnostic to what the weights represent; it simply requires them to be non-negative numbers, which is precisely why operators can encode arbitrary engineering intent into the metric without changing the underlying shortest-path computation.

What is the practical size limit before link-state flooding stops scaling well?

Flooding cost grows with the number of links, and every router must store and process the entire LSDB, so both memory and Dijkstra recomputation time grow with network size, though the O((V+E)log⁡V)O((V+E)\log V) Dijkstra cost itself remains modest even for networks with a few thousand routers. The real scaling limit in practice is flooding overhead and LSDB synchronization complexity across an unstable, large, flat topology, which is why large networks are partitioned into OSPF "areas" or IS-IS "levels": full-topology flooding happens only within each area, and a coarser summary is exchanged between areas, bounding the size of any single LSDB.

Before this

Where this leads