Skip to main content
RunBook Academy

← All break/fix scenarios in VyOS

advancedvyos-interface~30 min

Interface Packet Drops

Reported symptoms

  • Monitoring has been alerting for six days on rising receive drops on `eth1` of `dist1`, the campus aggregation port
  • The NOC has already replaced the SFP and the fibre patch lead, in two separate maintenance windows. The counter kept climbing through both
  • The nightly storage replication that crosses that port has gone from about 40 minutes to over three hours, and only in the direction that ingresses `eth1`
  • SNMP polls of the router time out every few minutes, so the interface graphs have gaps — which made the monitoring itself the first suspect
  • Interactive traffic is fine. SSH is crisp, small transfers complete normally, and only sustained streams degrade — which reads exactly like a path-MTU problem, and a day has already gone into MSS clamping
  • The router is not busy. Aggregate CPU sits around 24% on the five-minute average and `eth1` is nowhere near line rate, so a capacity argument was rejected early
  • The drops stop overnight and resume at 21:00, within a minute of the replication window opening

Evidence

  • · `show interfaces ethernet eth1` — the interface is up, RX `dropped` is climbing, RX and TX `errors` are both zero
  • · `ip -s link show eth1` — `dropped` rising, `errors` 0, `missed` 0, `carrier` 0
  • · `ethtool -S eth1` — `rx_crc_errors`, `rx_length_errors`, `rx_missed_errors` and `rx_no_buffer_count` are all zero and static
  • · `cat /sys/class/net/eth1/carrier_changes` — 2 since the last boot, unchanged across the whole incident
  • · `cat /sys/class/net/eth1/statistics/rx_dropped` — climbing at roughly 400 per second during the replication window
  • · `/proc/net/softnet_stat`, decoded — the dropped and time-squeeze columns climb on CPU 0 only; CPUs 1 to 3 read zero in both
  • · `mpstat -P ALL 1 5` — CPU 0 at about 92% `%soft`, the other three cores nearly idle, `%usr` low everywhere
  • · `cat /proc/interrupts | grep eth1` — four queue interrupts exist and every one of them is counting on CPU 0
  • · `cat /proc/irq/34/smp_affinity` — `01`, and the same mask on the other three queue IRQs
  • · `ethtool -l eth1` — four combined channels configured out of a pre-set maximum of four
  • · `ethtool -g eth1` — RX ring 1024 of a possible 4096
  • · `sysctl net.core.netdev_max_backlog` — 1000, the kernel default
Diagnosis and resolutionclick to reveal

Root cause

Nothing is being dropped on the wire. `eth1` has four receive queues and all four of their interrupts are pinned to CPU 0, which is the kernel's default state after boot and which nobody changed. Every packet that arrives on the campus aggregation port therefore has its softirq processing done by one core out of four. During the replication window that core saturates, the per-CPU input backlog — sized by `net.core.netdev_max_backlog`, still at its default of 1000 — fills, and the kernel drops whatever it cannot enqueue. Those drops are counted against the interface, which is why `show interfaces` and `ip -s link` show a rising `dropped` figure, and why two optics and two patch leads were replaced chasing a fault that is entirely in software. The discriminator was available from the first minute and costs one command: the driver's own counters in `ethtool -S` are all zero. A kernel drop counter rising while the driver's counters stay flat locates the loss above the driver, not on the link. The other three complaints follow from the same single fault. TCP reads indiscriminate loss as congestion and collapses its window, so bulk streams fall apart while interactive traffic is untouched — the same surface signature as a path-MTU black hole, which is why a day went into MSS clamping. The SNMP timeouts are the polling daemon's packets landing in the same overflowing backlog. And one saturated core out of four reads as 24% aggregate CPU, which is what retired the capacity hypothesis.

Remediation

Fix the layer that is actually dropping, and know what each option costs. Raising `net.core.netdev_max_backlog` from 1000 to 10000 is the cheapest action, takes effect immediately, needs no interface disruption and buys real headroom against bursts — but it does not make a saturated core faster, so it moves the threshold rather than removing it, and it costs kernel memory for queued packets. The durable fix is to spread the four queue interrupts across the four cores by writing a distinct CPU mask into each queue's `/proc/irq/N/smp_affinity`. That change is immediate and does not bounce the link, which makes it unusually cheap for a fix of its size, but it does not survive a reboot: the kernel resets every IRQ to CPU 0 at boot, so an unpersisted fix reappears as the same incident after the next maintenance window. Persistence has to be arranged deliberately, through irqbalance or a boot-time unit, and recorded. Do not raise the ring buffer. It is the reflex fix for a drop counter and it addresses `rx_missed_errors`, which reads zero here; a larger ring would add worst-case latency and change nothing else. Holding is a legitimate option if no change window is available: the drops are load-correlated, so rate-limiting or rescheduling the replication job removes the symptom tonight. Hold needs a named owner and a review time, because it leaves a port that will drop again the moment offered load rises.

Verification

Verify by delta, at load, and from outside the router. The counters in `/proc/net/softnet_stat` and `/sys/class/net/eth1/statistics/rx_dropped` are cumulative and never reset, so a lower number proves nothing: take two readings ten minutes apart during the replication window and require the difference to be zero on every CPU, not merely smaller than before. Read `/proc/interrupts` and require the eth1 queue counts to be climbing in all four CPU columns rather than one, and `mpstat -P ALL 1 5` to show `%soft` spread across the cores with none of them near saturation. Then take the check that does not depend on the router's own bookkeeping: the replication job must complete inside its original window. That is the only measurement in the set that a mis-read counter cannot satisfy. Prove the check can fail by running it at peak rather than at 04:00, when an idle port drops nothing regardless of how the interrupts are arranged. Finally, reboot the router in a window and re-read `/proc/irq/*/smp_affinity`: if the masks have gone back to `01`, the fix was never persisted and the incident is scheduled to recur.

Prevention

Alert on the layer, not on the symptom. A rising interface drop counter says that something dropped a packet and says nothing about what; a per-CPU delta from `/proc/net/softnet_stat` names the layer in one line and would have ended this incident on day one. Add per-core `%soft` to the router dashboard, because an aggregate CPU figure cannot distinguish four cores at 25% from one core at 100%, and those two states have nothing in common. Make interrupt distribution part of build validation for every router and every new port: check the queue count with `ethtool -l` and the affinity masks in `/proc/irq`, and record the expected distribution alongside the interface configuration so a reboot that undoes it is detectable. Write down the rule that closed this case — the driver counters in `ethtool -S` must be non-zero before anyone books a window to swap an optic — because two optics and two outage windows were spent on a counter that increments in software. And sample counters at peak. Everything here looked healthy at 10:00 and the evidence only existed between 21:00 and midnight.

Reported symptoms

dist1 is a four-core VyOS router aggregating a campus. eth1 is its uplink into the distribution layer and carries most of the traffic on the box.

The ticket is six days old and has collected four complaints from three teams. Monitoring alerts on rising receive drops on eth1. The NOC has responded the way the alert invites: an SFP swap in one maintenance window, a fibre patch lead in another. The counter climbed through both. The storage team has a separate ticket saying the nightly replication used to finish in about 40 minutes and now runs past three hours, in one direction only. The monitoring team has a third saying SNMP polls of dist1 time out every few minutes, leaving gaps in exactly the graphs everyone is trying to read.

Two observations have kept the investigation from converging. Interactive traffic is perfect — SSH through the port is crisp and small transfers complete normally — and only sustained streams degrade, which is the textbook shape of a path-MTU black hole. A full day went into MSS clamping on that hypothesis and changed nothing. And the router is not busy: aggregate CPU is around 24% on the five-minute average, eth1 is far from line rate, so nobody believes this is a capacity problem.

The one piece of structure in the noise is the clock. The drops stop overnight and resume at 21:00, within a minute of the replication window opening.

Evidence provided

Read-only / Safedropped is climbing, errors is zero
vyos@dist1:~$ show interfaces ethernet eth1
eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
  link/ether 52:54:00:8a:3f:11 brd ff:ff:ff:ff:ff:ff
  inet 198.51.100.2/30 scope global eth1
  RX:  bytes    packets   errors  dropped  overrun  mcast
  884213977412  912004431      0  4183902        0  22140
  TX:  bytes    packets   errors  dropped  carrier  collisions
  771540221883  801229118      0        0        0      0

Illustrative output

Read-only / Safeevery driver counter is zero and stays zero
vyos@dist1:~$ ethtool -S eth1 | grep -E 'crc|length|missed|no_buffer|fifo'
     rx_crc_errors: 0
   rx_length_errors: 0
   rx_missed_errors: 0
   rx_no_buffer_count: 0
   rx_fifo_errors: 0

Illustrative output

Read-only / Safetwo carrier transitions since boot — the link has not moved
vyos@dist1:~$ cat /sys/class/net/eth1/carrier_changes
2

Illustrative output

Read-only / Safeone row per CPU, little-endian hex
vyos@dist1:~$ cat /proc/net/softnet_stat
3661e02b 003fd97e 0001cebf 00000000 00000000 00000000 00000000 00000000 00000000 00000012
0012629e 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000004
00124ac6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000004
0012538f 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000004

Illustrative output

Decoded, the three columns that matter — processed, dropped and time-squeeze — read:

CPU 0: processed=911884203  dropped=4183902  squeezed=118447
CPU 1: processed=1204118    dropped=0        squeezed=0
CPU 2: processed=1198790    dropped=0        squeezed=0
CPU 3: processed=1201295    dropped=0        squeezed=0
Read-only / Safe24% aggregate, and one core at 92% softirq
vyos@dist1:~$ mpstat -P ALL 1 5 | tail -5
Average:     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal   %idle
Average:     all    3.10    0.00    1.85    0.00    0.00   23.94    0.00   71.11
Average:       0    4.02    0.00    2.71    0.00    0.00   92.15    0.00    1.12
Average:       1    2.88    0.00    1.60    0.00    0.00    1.02    0.00   94.50
Average:       2    2.91    0.00    1.55    0.00    0.00    1.11    0.00   94.43

Illustrative output

Read-only / Safefour queues, one column
vyos@dist1:~$ cat /proc/interrupts | grep eth1
  34:  418822901          0          0          0   PCI-MSI  eth1-TxRx-0
35:  401337882          0          0          0   PCI-MSI  eth1-TxRx-1
36:  399218440          0          0          0   PCI-MSI  eth1-TxRx-2
37:  402881073          0          0          0   PCI-MSI  eth1-TxRx-3

Illustrative output

Read-only / Safethe default mask, on every queue
vyos@dist1:~$ grep . /proc/irq/3[4-7]/smp_affinity
/proc/irq/34/smp_affinity:01
/proc/irq/35/smp_affinity:01
/proc/irq/36/smp_affinity:01
/proc/irq/37/smp_affinity:01

Illustrative output

Work the evidence before reading on

Three teams have three tickets. Decide first how many faults you think are in front of you.

  1. show interfaces reports rising dropped and zero errors, and ethtool -S reports zero on every driver counter. Both claim to describe the same interface. What does the disagreement between them localise?
  2. carrier_changes reads 2 since boot. What hypothesis does that single number retire, and how much did it cost to run?
  3. The aggregate CPU figure is 24% and one core’s %soft is 92%. Which of those two numbers is describing the resource that is exhausted?
  4. Only sustained transfers degrade; interactive traffic is fine. Name two different causes that produce exactly that signature, and the one command that tells them apart.

Then the question that resolves the incident: eth1 has four receive queues and the box has four cores. Why is only one of those cores doing any receive work, and what state is the router in that nobody configured?

Root cause

1. There are two counters called rx_dropped and they mean different things

ip -s link show eth1 and show interfaces ethernet eth1 read the kernel’s per-device statistics. ethtool -S eth1 reads the driver’s own counters, which the hardware and the driver maintain for events they can see: CRC failures, length errors, descriptors unavailable, FIFO overruns.

When the kernel receives a packet from the driver and then fails to enqueue it, it drops the packet and charges the drop to the device. The driver never sees that event and has no counter for it. So a rising kernel dropped alongside a flat set of driver counters is not an inconsistency to be explained away — it is the diagnosis. The loss is happening above the driver, in software, on a link that is behaving perfectly.

This is the single most useful comparison in the whole incident and it costs one command. It was available before the first optic was ordered.

2. softnet_stat names the layer, and names the CPU

The decoded per-CPU rows say two things at once. The dropped column is non-zero on CPU 0 and zero everywhere else, which places the loss at the per-CPU input backlog rather than the ring or a socket. The squeezed column is also climbing on CPU 0, which says the softirq pass on that core is repeatedly running out of budget before it has drained its queue.

The processed column completes the picture: CPU 0 has processed nearly a billion packets and the other three cores have processed roughly a million each. This is not a busy router. It is a router doing all of its receive work on a quarter of itself.

3. Four queues, four cores, one mask

ethtool -l eth1 shows four combined channels, and /proc/interrupts shows four queue interrupts, so the NIC is doing its part: the hash is spreading packets across four rings. Every one of those four interrupts has the affinity mask 01, which is CPU 0.

Nobody set that. 01 is what the kernel assigns at boot, and it persists until something changes it. The port was commissioned, it worked at commissioning-day traffic levels, and the campus grew into a limit that has been latent since the day the interface came up.

With net.core.netdev_max_backlog at its default of 1000, the queue in front of that single core holds about a millisecond of the offered rate during the replication window. Whatever arrives while it is full is dropped and counted against eth1.

4. Everything else on the ticket is a consequence

The replication job degrades because TCP has no way to distinguish a packet lost to a full backlog from a packet lost to congestion. It reads loss as congestion, collapses its window, and a bulk stream that should saturate the link spends its night in recovery. Interactive traffic sends too few packets to be hit often and recovers instantly when it is, which is why SSH feels fine.

That combination — bulk transfers fail, interactive traffic is healthy — is also what a path-MTU black hole looks like from the user’s seat, and it is a reasonable first hypothesis. It is separable from this one in a single reading: a PMTU black hole drops by size and deterministically, so ping -M do finds a hard threshold and the drop counters stay at zero. Here the drops are size-independent, load-correlated, and counted on the interface.

The SNMP timeouts are the same fault seen from the management plane: the poller’s UDP packets arrive on the saturated port and queue behind everything else. The monitoring is not broken. It is reporting, accurately, that it cannot get an answer.

Resolution

  1. Confirm the layer before changing anything. Read ethtool -S eth1, /sys/class/net/eth1/carrier_changes and the decoded /proc/net/softnet_stat together. All three are read-only and together they either support the diagnosis or refute it in under a minute.
  2. Take a baseline you can verify against later. Record the softnet dropped value per CPU and the rx_dropped figure with a timestamp — these counters are cumulative, so a later reading is only meaningful as a difference.
  3. Raise the backlog for immediate relief: set system sysctl parameter net.core.netdev_max_backlog value 10000 and commit. This takes effect at once, does not disturb the link, and buys headroom against bursts. Understand what it does not do: a deeper queue in front of one saturated core postpones the drop rather than preventing it.
  4. Distribute the receive queues across the cores. Write a distinct mask into each queue interrupt — 01, 02, 04 and 08 for IRQs 34 to 37 on a four-core box — so each of the four rings is serviced by its own core. The change is immediate and does not bounce the interface.
  5. Do not enlarge the ring buffer. rx_missed_errors is zero, so the ring is not the layer that is full; enlarging it costs worst-case latency and fixes nothing here. Record that this was considered and rejected, with the counter that decided it.
  6. Persist the affinity deliberately. Writes to /proc/irq/*/smp_affinity do not survive a reboot; the kernel resets every IRQ to CPU 0 at boot. Arrange persistence through irqbalance or a boot-time unit and record it next to the interface configuration, because an unpersisted fix is an incident with a delayed timer on it.
  7. If no change window is available tonight, hold deliberately rather than by default. The drops are load-correlated, so rate-limiting or rescheduling the replication job removes the symptom immediately. Give the hold a named owner and a review time — it leaves a port that will drop again as soon as offered load rises.
  8. Close out the hardware line of enquiry explicitly. Return the two replaced optics to stores rather than scrapping them, and put the reason in the ticket so the next operator does not repeat the swap.

Verification

  1. Softnet drops have stopped, measured as a delta. Read /proc/net/softnet_stat twice, ten minutes apart, during the replication window: the dropped difference must be zero on every CPU. A lower absolute number proves nothing, because the counter never resets.
  2. The interface drop counter is static. cat /sys/class/net/eth1/statistics/rx_dropped returns the same value across the same interval.
  3. The work is genuinely spread. cat /proc/interrupts | grep eth1 shows all four CPU columns advancing, and mpstat -P ALL 1 5 shows %soft distributed with no core near saturation.
  4. The application-level check passes. The replication job completes inside its original window. This is the only measurement in the set that does not depend on the router's own bookkeeping, which is exactly why it belongs here.
  5. The check was run where it can fail. Verify at 21:00 under real offered load, not at 04:00 — an idle port drops nothing regardless of how its interrupts are arranged, so an overnight pass is not evidence.
  6. The fix survives a reboot. In a subsequent window, reboot and re-read /proc/irq/*/smp_affinity. Masks back at 01 mean the persistence step did not take and the incident is scheduled to recur.
  7. SNMP polling is reliable again. The gaps in the interface graphs close — a second independent confirmation, from the subsystem that was originally accused.

Prevention

  • Alert on the per-CPU softnet dropped delta, not only on the interface drop counter. The interface counter says a packet was lost; the softnet counter says which layer lost it. One of those two alerts ends this incident on the first morning.
  • Put per-core %soft on the router dashboard. An aggregate CPU number cannot distinguish four cores at 25% from one core at 100%, and it was the aggregate figure that made “we are not out of capacity” sound true.
  • Validate interrupt distribution at build time and after every reboot. Check the channel count with ethtool -l and the masks in /proc/irq, and record the expected distribution alongside the interface configuration so that drift back to the default is detectable rather than discovered.
  • Require a physical or driver-level counter before booking a hardware swap. carrier_changes, rx_crc_errors and rx_missed_errors justify a window; a kernel dropped figure on its own does not.
  • Sample counters at peak. Every reading taken during business hours on this router looked healthy, and the entire evidence base existed between 21:00 and midnight.
  • Treat “bulk transfers fail, interactive traffic is fine” as a family of causes rather than a diagnosis. Path-MTU black holes and indiscriminate loss present identically to the user and are separated by one look at the drop counters.

Cross-course references

  • The Linux course’s XXII-Linux-NetTroubleshoot covers the same counters from the host perspective: ip -s link, ethtool, and the sysfs statistics tree.
  • The Observability course’s XCVIII-Observability-Troubleshooting covers building the per-CPU and per-layer drop alerts this incident needed, rather than the aggregate one it had.
  • The Ansible course’s XLV-Ansible-Debugging covers the fleet question this raises: how to check interrupt distribution on every router you own once you know it is the kind of thing that quietly reverts.