Runbook: Investigate Router CPU Saturation
1 · Prerequisites
Confirm every item is in place before any state change.
- CPU saturation — top, mpstat, softirq vs hardirq vs userspace, Linux scheduler
- Interrupt affinity — /proc/irq, RSS, NUMA, IRQBALANCE_BANNED_CPUS
- Packet drops — softnet_stat, per-NIC counters, drop reasons
- Crypto performance — AES-NI offload, IPsec throughput, kTLS, single-core bottleneck
- Route churn — FRR zebra CPU, route-flap, iBGP convergence, OSPF SPF
- Performance baselines — pbench, record baselines, alert on deviation, golden image
- Subsystem by subsystem — kernel, FRR, firewall, interface, application, isolate layer
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · The report is turned into a measurement and a start time. "CPU at 100%" is a number from a graph; what this runbook needs is which core, which class of work, and since when.
- · Established whether anything is actually being harmed: packet loss, latency, protocol adjacencies, or a queue. A router at 100% softirq that is forwarding at line rate with no drops is at capacity, not broken, and the response to those two states is different.
- · This router's performance baseline is to hand. Without a recorded normal, "30% CPU" cannot be distinguished from "30% CPU, which is triple what this box has ever done".
- · Checked whether a burst is in progress: a BGP peer reset, a full-table refresh, an OSPF flood, or an image upgrade. Transient convergence load must not be tuned as if it were steady state.
- · The load average is explicitly set aside as evidence. It is run-queue depth, not utilisation, and a load of 4 on a four-core router says nothing about whether the cores are busy.
- · Confirmed that the investigation itself will not make things worse: no
tcpdumpon a busy forwarding interface, no full SNMP walk, no verbose FRR debug on a control plane that is already behind. - · Out-of-band console reachable. A saturated control plane drops SSH sessions and refuses new ones, and it does so at exactly the moment you most need to be logged in.
- · Agreed with the incident owner that this runbook is read-only, and that any remediation leaves it and goes through a change with its own rollback.
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1Establish harm before attribution:
show ip bgp summaryandshow ip ospf neighborfor adjacency state,cat /proc/net/softnet_statfor dropped packets, and a latency probe from a real source. Saturation with no harm is a capacity finding, not an incident. - 2Run
mpstat -P ALL 1 5and read the per-core columns.%usrdominant means userspace - FRR or the management plane.%softdominant means the kernel packet path.%irqdominant means the NIC interrupt itself, which is rare and usually means a misbehaving device. - 3Read the per-core spread, not just the average. One core at 95% with the rest idle is a completely different fault from four cores at 70%, and the average hides exactly that distinction.
- 4For
%usrdominance, runtop -bn1 | head -30and name the process:zebra,bgpd,ospfd, an IPsec or telemetry daemon,sshd, orsnmpd. The name selects the next command; nothing before this point does. - 5For
zebraorbgpd, quantify the churn behind it:show ip route summaryfor table size,show ip bgp summaryfor message counters and peer state,show ip bgp flap-statisticsfor prefixes that keep moving. - 6For
%softdominance, decide single-core or all-core.cat /proc/net/softnet_statper CPU andcat /proc/interruptsshow whether one core is carrying the NIC alone. Single-core saturation is a distribution problem; all-core saturation is a capacity problem and no software knob fixes it. - 7Correlate with drops rather than inferring them: read
droppedandtime squeezeper CPU in/proc/net/softnet_stat, andethtool -S eth0for the driver's own counters. CPU percentage and packet loss decouple, in both directions. - 8If IPsec or WireGuard carries meaningful traffic, check AES-NI is present and the kernel module is loaded before blaming the CPU generally. Software AES is an order of magnitude slower and looks like a hardware sizing problem.
- 9Classify the saturation as transient, at-capacity, or faulty. Transient means a burst that will end - wait for it. At-capacity means the router is doing the work asked of it and the answer is hardware or less traffic. Faulty means something is consuming cycles it should not.
- 10Write the attribution down as a sentence naming the class, the process or queue, and the evidence: "softirq, single-core, eth1 IRQ pinned to CPU 0, softnet drops on CPU 0 only". That sentence is what makes the remediation obvious and reviewable.
- 11Stop here. Remediation - interrupt affinity, RSS, ring buffers, sysctl tuning, peer bounds, hardware - is a change with its own blast radius and its own rollback, and it belongs in a change window with the attribution attached.
- 12Re-baseline afterwards. A CPU incident that is closed without updating the recorded normal guarantees the next deviation is argued about instead of measured.
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓
mpstat -P ALL 1 5output is captured and attached, showing the per-core split of%usr,%sys,%softand%irqrather than a single aggregate figure - ✓The dominant class is named, and the naming is supported by a second, independent observation - a process from
top, an IRQ from/proc/interrupts, or a queue from/proc/net/softnet_stat - ✓Whether packets are being dropped is answered from counters, not inferred from the CPU figure:
droppedper CPU in/proc/net/softnet_statand the driver counters inethtool -S eth0 - ✓Routing adjacencies are confirmed intact -
show ip bgp summarypeers inEstablished,show ip ospf neighborinFull- or their loss is recorded as a consequence of the saturation - ✓The saturation is classified as transient, at-capacity, or faulty, and the classification is justified against this router's baseline rather than against a round number
- ✓For a transient burst, the CPU is observed returning to baseline once the burst completes, and the return is timed
- ✓No configuration was changed by this runbook; if anything was, it is listed with its rollback in the ticket
- ✓The baseline document is updated, or a task exists to update it
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶The investigation is read-only. Every command in the procedure reads a counter, a process table or a routing view, and none of them commit.
- ↶If
ethtool -Lorethtool -Xwas used to redistribute queues during the incident, restore the previous queue count and indirection table. These are runtime-only, so a reboot also restores them - which is a rollback and a trap at the same time, because the change silently disappears at the next reload. - ↶If an IRQ mask was written to
/proc/irq/, restore the original mask. The kernel resets IRQ affinity at boot, so this too vanishes on reload and must not be relied on as a fix. - ↶If a sysctl was set with
sysctl -w, restore the previous value. The running value and the configured value have now diverged, and that divergence outlives your shell. - ↶If a process was reniced or constrained, remove the constraint. De-prioritising a routing daemon to make a graph look better trades convergence time for CPU headroom, and nothing in the routing protocol knows that trade was made.
- ↶If FRR was restarted, that is not a rollback - every session dropped and re-established, and the table was rebuilt. Record it as an action taken, with the time, because the convergence that follows will show up in everyone else's graphs.
- ↶Any runtime tuning that is worth keeping must be re-made as configuration in a change window. A fix that only exists until the next reboot is a fix that will be missing on the day the router reboots unexpectedly.
6 · Escalation
When the runbook isn't enough, contact:
- · Escalate to network engineering when the attribution is
bgpdorzebradriven by a peer: bounding what a peer can send is a policy change on the peering relationship, not an operator action during an incident - · Escalate to whoever owns the peer when the churn arrives from their side. The local router is reporting their instability accurately and no local tuning changes that.
- · Escalate to capacity planning when all cores are saturated in softirq at the offered traffic rate. That is the router doing its job at the edge of what it can do; the remedies are hardware, offload, or less traffic, and none of them are configuration.
- · Escalate to security when the load is inbound traffic this router should not be receiving. Control-plane saturation from unsolicited traffic is a control-plane protection problem, not a performance-tuning problem.
- · Escalate to the monitoring owner when the saturation is a management-plane artefact - an SNMP walk storm or a telemetry exporter in a restart loop. The fix is in their system, not on the router.
- · Escalate before restarting FRR or rebooting the router. Both clear the symptom, both destroy the evidence, and both cost a full reconvergence that will be visible across the estate.
- · Hold rather than force: a router at high CPU that is forwarding correctly, holding its adjacencies and dropping nothing can be left alone under observation, with an owner and a review time, while the capacity conversation happens properly.
“The router’s CPU is at 100%” is not a finding. It is three completely different findings that a single graph cannot tell apart, and the fix for each lives in a different subsystem.
A VyOS router spends cycles in three classes of work. Userspace is
where the FRR daemons run — zebra maintaining the RIB and pushing it
into the kernel FIB, bgpd and ospfd handling protocol messages —
along with SSH, SNMP and any telemetry exporter. Softirq is where
the kernel processes packets that the NIC has already delivered into
memory: routing lookups, firewall, NAT, and the encryption pass for
tunnelled traffic. Hardirq is the NIC’s own interrupt handler,
which on a modern driver does almost nothing.
The classes are not interchangeable, and confusing them is expensive
in a specific way: it sends you to tune the wrong subsystem, which
takes an hour and occasionally makes things worse. Tuning BGP timers
when the real problem is a NIC interrupt pinned to one core changes
nothing except the configuration. Raising netdev_max_backlog when a
peer is sending a full table changes nothing except your confidence.
So this runbook is deliberately read-only. It ends with a written attribution, not with a change, because the attribution is the part that is hard and the change is the part that has a blast radius.
When to use this runbook
- A monitoring alert on router CPU has fired.
- Convergence after a topology change is taking noticeably longer than this router’s baseline.
- SSH to the router is slow or intermittent, or new sessions are refused.
- Throughput has fallen short of what the link and the hardware should deliver, with no obvious link fault.
When not to use it
- Packet loss with idle CPU. Drops and utilisation decouple.
Packets can be dropped at the ring, at the per-CPU backlog, or at a
socket while the CPU has headroom. That is a drop investigation, and
it starts at the counters rather than at
top. - A single flapping session or prefix. That is route instability. It can raise CPU as a side effect, and treating the side effect wastes the window.
- A planned upgrade or a known burst. Convergence load during a full-table refresh is the router doing its job. Wait for it.
- “The router feels slow” with no measurement. Get one first. Half the reports that arrive as CPU problems are DNS, an asymmetric path, or an application.
Blast radius
Reading is free — except that on a saturated router, some diagnostics are not.
tcpdump on a busy forwarding interface adds a copy of every matching
packet to the softirq path you are already investigating, and can turn
a router that was coping into one that is not. Verbose FRR debugging
adds work to a control plane that is already behind. A full SNMP walk
against a router whose management plane is the thing saturating is a
diagnostic that reproduces the fault.
Use the cheap observers — mpstat, top, pidstat, the /proc
counters — and reach for packet capture only after the class of work
is already known, and preferably on a switch SPAN port rather than on
the router.
Inputs
- The alert, with its threshold and the time it fired.
- This router’s recorded baseline: normal per-core
%usrand%soft, normal route count, normal throughput. - What changed in the estate in the preceding hour — commits, peer resets, upgrades, new traffic.
- Whether this router terminates IPsec or WireGuard, and roughly how much.
- The interface layout: which NIC carries the transit, how many queues it has, how many cores the router has.
Step 1: Is anything actually being harmed?
Attribution is the second question. The first is whether this matters at all, because a router can be legitimately busy.
show ip bgp summary
show ip ospf neighbor
show ip route summary
show interfacescat /proc/net/softnet_stat
ethtool -S eth1 | grep -Ei 'drop|miss|error|discard'
cat /sys/class/net/eth1/statistics/rx_droppedThree outcomes, and they diverge sharply:
- Busy, no harm. Adjacencies up, drop counters flat, latency normal. The router is at capacity for the work it is being given. This is a capacity conversation with an owner and a timescale — not an incident, and not something to tune under pressure.
- Busy, with harm. Drops climbing, or adjacencies bouncing, or latency up. Now it is an incident, and the attribution below decides what kind.
- Busy, and the harm has a different cause. Common. The CPU is elevated because something else is wrong — a flap storm, a leaked full table, an interface error rate driving retransmits. Fixing the cause fixes the CPU; tuning the CPU fixes nothing.
Step 2: Which class of work
mpstat -P ALL 1 5Read the columns, and read the rows — the per-core spread carries as much information as the totals.
| Dominant column | Class | Where the answer is |
|---|---|---|
%usr | Userspace | top, then pidstat on the named process |
%soft | Kernel packet path | /proc/net/softnet_stat, /proc/interrupts |
%irq | NIC interrupt handler | /proc/interrupts; rare, and usually a device fault |
%sys | Kernel on behalf of a process | top; often a syscall-heavy exporter |
%steal | The hypervisor | Not this router’s problem — it is the host’s |
One core at 95% while the others idle is the signature of work that cannot be spread: a NIC IRQ landing on a single core, or a single-threaded daemon. All cores evenly loaded is the signature of work that is being spread and is simply too much.
%steal deserves its own note. On a virtualised VyOS instance, steal
time means the hypervisor is not giving this guest the CPU it is
asking for. Nothing configured inside the router changes that, and
every minute spent tuning the guest is a minute not spent talking to
whoever owns the host.
Step 3a: Userspace — name the process
top -bn1 | head -30
ZEBRA_PID=$(pgrep -x zebra)
pidstat -t -p "$ZEBRA_PID" 1 5pidstat -t breaks a daemon down by thread, and for zebra that is
the observation that matters: zebra is single-threaded for RIB
processing. Every route update flows through one thread regardless
of how many cores the router has. A zebra thread at 100% on an
eight-core router is a fully saturated route-processing path even
though mpstat shows the box at 12% overall — and adding cores does
nothing for it. The only lever is fewer updates.
The process name maps to a cause, and each has a different owner:
zebra— route churn. The RIB is being rewritten faster than it can be pushed to the kernel. Look at what is generating the updates.bgpd— protocol message load: a peer reset, a full-table refresh, a peer sending far more than expected, or a flap storm.ospfd— SPF recalculation, driven by LSA churn. Usually a flapping link somewhere in the area, and the cost scales with the area’s size.sshd,snmpd, a telemetry exporter — the management plane. An SNMP walk storm or an exporter in a restart loop, both of which belong to whoever runs the monitoring.- A crypto daemon or a heavy
%sys— go to Step 3c.
show ip route summary
show ip bgp summary
show ip bgp flap-statistics
show ip ospf databaseshow ip route summary against the baseline is the fastest way to
separate “a peer sent us far more than it should” from “the same
routes keep being rewritten”. The first shows as a table that is much
larger than normal; the second as a normal-sized table with a high
flap count.
Step 3b: Softirq — one core or all of them
cat /proc/net/softnet_stat
cat /proc/interrupts | head -20
ethtool -l eth1
ethtool -g eth1/proc/net/softnet_stat has one row per CPU. The first column is
packets processed, the second is packets dropped because that CPU’s
input queue was full, and the third is the number of times the softirq
pass ran out of budget before draining the backlog. The columns are
hex, and the pattern across rows is what matters more than any single
value.
- One row with drops, the rest clean. The NIC’s interrupts are
landing on one core and that core cannot drain the ring. This is a
distribution problem.
ethtool -lshows how many queues the NIC actually has, and/proc/interruptsshows how many are in use. Remediation is RSS plus interrupt affinity — and it belongs to a change, because it is fiddly and because the runtime forms do not survive a reboot. - Every row with drops. The offered rate exceeds what this router can process. No software knob fixes that. Raising the ring buffer moves the drop later and adds latency; raising the backlog absorbs bursts but not a sustained excess. The honest answers are more hardware, offload, or less traffic.
- Squeeze climbing with no drops. The softirq pass is being cut short repeatedly. Often it is being starved by a CPU-bound userspace process — which brings you back to Step 3a on the same box.
Step 3c: Crypto — a distinct signature
A router terminating IPsec or WireGuard does its encryption in the kernel, in softirq context, on whichever core handled the packet’s arrival. So heavy tunnel traffic looks like softirq saturation — with one extra question to ask before concluding the hardware is too small.
grep -m1 '^flags' /proc/cpuinfo | tr ' ' '
' | grep -x aes
lsmod | grep aesIf the CPU advertises aes and the module is loaded, hardware AES is
available and the crypto is as fast as this CPU can make it. If either
is missing, the kernel is falling back to software AES — which is an
order of magnitude slower, and which presents exactly as “this router
is too small for the tunnel”. That is a very cheap check to run before
anyone specifies replacement hardware.
The distribution problem from Step 3b applies here with more force: because the encryption happens on the core that received the packet, a NIC whose interrupts all land on one core caps tunnel throughput at one core’s worth of crypto, no matter how many cores the box has.
Step 4: Transient, at capacity, or faulty
This classification is the output of the runbook. Everything above exists to support it.
| Class | What it looks like | Correct response |
|---|---|---|
| Transient | Rises with an event — peer reset, LSA flood, upgrade — and falls when the event ends | Watch it. Record how long it took to fall. Bound the next one, later. |
| At capacity | Steady, proportional to offered load, no drops or drops only at peak | Capacity conversation with an owner and a date. Not an incident. |
| Faulty | Steady, disproportionate to load, or attributable to something that should not be running | Fix the specific thing. This is the only class where a change is the right immediate answer. |
Write it as a sentence: the class, the subsystem, the process or queue, the evidence. “Softirq, single-core, eth1 interrupts on CPU 0 only, softnet drops on CPU 0 and nowhere else” is an attribution somebody can act on. “CPU high, probably traffic” is not, and it is what the next person will find in the ticket.
Step 5: Stop before changing anything
Every remediation this investigation can point at is a change with its own blast radius:
- RSS and interrupt affinity — improves distribution, does not
persist, interacts with
irqbalance, and can move a hot spot rather than removing it. - Ring buffer size — absorbs bursts, adds worst-case latency. Latency-sensitive traffic pays for it.
- Backlog and socket buffer sysctls — the running value and the
configured value diverge the moment you use
sysctl -w. - Bounding a peer — a change to a peering relationship, with a peer-visible effect if it is wrong.
- Renice or cgroup limits on a routing daemon — trades convergence time for CPU headroom, silently, in a way no routing protocol knows about.
- Restarting FRR — drops every session and rebuilds the table. It reliably clears the symptom, and it also destroys the evidence and costs a full reconvergence across the estate.
Each deserves a change record and a rollback. None of them belongs in an investigation.
Rollback
The procedure is read-only, so there is normally nothing to undo. What needs undoing is anything reached for under pressure:
| Reached for | Undo |
|---|---|
ethtool -L / -X | Restore the previous queue count and indirection table |
A write to /proc/irq/ | Restore the original mask |
sysctl -w | Restore the previous value, and reconcile with the configured value |
renice or a cgroup limit | Remove it; note that it was applied |
| An FRR restart | Cannot be undone. Record it, and expect it in other people’s graphs. |
The runtime forms are the trap. They vanish at the next reboot, which looks like a rollback and is actually a fix quietly ceasing to exist.
Common patterns
| Observation | Likely attribution | Next step |
|---|---|---|
%usr high, bgpd top, one peer recently reset | Convergence burst | Wait; time the recovery; bound the peer afterwards |
%usr high, zebra top, route count normal, flaps high | Route churn from an unstable source | Route instability investigation, not CPU tuning |
%usr high, ospfd top, LSA count moving | SPF thrash from a flapping link or an oversized area | Stabilise the link; area design is engineering’s |
%soft at 95% on one core, others idle | NIC interrupts on a single core | RSS and affinity, as a change |
%soft high on every core, drops everywhere | Offered rate exceeds capacity | Capacity, offload, or less traffic |
%soft high with tunnels, no aes flag or module | Software AES fallback | Confirm hardware crypto before sizing anything |
%sys high, an exporter restarting repeatedly | Management-plane loop | Monitoring owner |
%steal non-trivial | The hypervisor is not delivering CPU | Host owner; nothing to tune in the guest |
| CPU normal, packets dropping | Not a CPU problem at all | Drop counters, not top |
Escalation
Escalate when:
- The attribution is a peer’s behaviour. Bounding a peer is a change to a relationship, not an operator action mid-incident.
- Every core is saturated in softirq at the offered rate. That is capacity, and capacity is bought, not configured.
%stealis significant. The conversation is with whoever owns the hypervisor.- The load is unsolicited traffic aimed at the control plane. That is a security and control-plane-protection question.
- Anyone proposes restarting FRR or rebooting. Both clear the symptom and destroy the evidence, and both should be a decision with a name against it.