Learn › The Network Stack › Your Network Interface
ip -s link show - a hands-on Linux lab on a real virtual machine.
Your NIC counts every packet, every byte, and every error. The numbers are in /proc/net/dev, /sys/class/net, and ip -s link show.
A few seconds ago you pressed Enter and this lesson opened. Somewhere in the kernel of the machine you were connected to, a handful of integers went up because of it. Every keystroke you have ever typed into a remote terminal crossed a network interface as a packet, and the interface counted it. Every byte in, every byte out, every error, every packet thrown away. The counting never stops, and almost nobody looks.
Engineers who do look catch problems early. An application feels slow, CPU is fine, disk is fine, memory is fine, and then one counter named rx_dropped turns out to have been climbing for an hour: the kernel has been quietly discarding incoming packets. The counters are where the network confesses.
By the end of this lesson you will know all three places Linux keeps these numbers, why a counter you are watching over the network can never sit still, and how to read an error column and call an interface healthy or sick.
The black boxes below are a practice terminal: a safe sandbox that checks the one command each step teaches. The layout it shows is this exact lab: interface eth0, user student, host stats-lab, and a two-machine network you will meet halfway through. One honest warning about the numbers themselves: counters never repeat. The screens show one recorded session, and when you launch the real VM your values will differ. Every filename, column and header is real. In the real lab your progress is tracked automatically, so just type commands naturally.
A counter is an unsigned integer the kernel keeps in memory, one set per network interface. Each counter counts one thing and only ever goes up: bytes received, packets sent, receive errors, transmit drops, and about twenty more. Together they live in a single C structure the kernel holds for every interface (its name, rtnl_link_stats64, matters less than the idea: one box of numbers per interface).
Linux gives you three windows onto that one box:
| Window | Shape | Best for |
|---|---|---|
/proc/net/dev | One table, every interface | A quick glance at everything |
/sys/class/net/<interface>/statistics/ | One file per counter | Scripts and monitoring, no parsing |
ip -s link show | Formatted RX and TX blocks | Human-readable detail per interface |
All three ask the interface driver the same question through the same kernel call. That has a consequence worth stating plainly: these three can never disagree with each other. If two of them ever seem to, you misread one. There is a fourth ledger, kept by the NIC hardware itself and read with ethtool -S, and that one CAN disagree with the other three. It comes up later in the lesson.
One sentence of history. /proc/net/dev has printed the same jammed, misaligned two-line header since the 1990s, because a generation of tools, ifconfig among them, parses it by column position, and changing a byte of it would break them. When you see it in a moment, you are looking at a fossil that compatibility keeps alive.
The first window is /proc/net/dev. Remember from Lesson 1: /proc files are virtual, generated by the kernel at the moment you read them, so every read is a fresh snapshot.
Before you press Enter, commit to a number: how many interface rows will the table hold? Lesson 1 already told you what this machine has.
cat /proc/net/dev
prompt: student@stats-lab:~$ answer: cat /proc/net/dev output: Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed lo: 1284 16 0 0 0 0 0 0 1284 16 0 0 0 0 0 0 eth0: 98432 1093 0 0 0 0 0 0 105210 887 0 0 0 0 0 0 hint: It is a file. Read it the way you read any file: cat /proc/net/dev
Two rows: lo, the loopback, and eth0, the interface your session rides. The pipe characters split each row in half. Left of the second pipe is Receive, everything that arrived. Right of it is Transmit, everything sent.
Now look at the sizes. eth0 has already moved about 98 KB in and 105 KB out, and you have only just logged in. That is not background noise. That is your own SSH session: the login banner, the MOTD, every character echoed back to your screen. You are reading the meter of the wire you are reading it through.
One more thing, promised in the last step: the header really is jammed. The interface name is right-justified and glued to a colon, and when names get long the name touches the numbers. Scripts that parse this file split on the colon first. Now you know why every serious one does.
The two halves carry eight columns each:
| Column | Side | What it counts |
|---|---|---|
bytes | both | Total bytes moved since the interface existed |
packets | both | Total packets moved |
errs | both | Malformed packets (bad checksum, framing damage) |
drop | both | Packets the kernel had, then threw away, usually a full buffer |
fifo | both | Hardware buffer overruns, the NIC itself could not keep up |
frame | receive | Frames that did not align to byte boundaries |
colls | transmit | Collisions, only possible on old half-duplex Ethernet |
carrier | transmit | The link vanished mid-transmission |
compressed | both | Compressed packets, almost always zero |
multicast | receive | Multicast packets received |
Two precision points that separate engineers who read counters from engineers who guess.
First, when a packet is counted: on receive, when the driver hands it to the kernel, and on transmit, when the driver accepts it for sending. Not when electricity moves on a wire.
Second, what a byte counter counts: the frame the driver saw. On the physical wire, every Ethernet frame also costs a preamble and a gap of enforced silence before the next frame, which the counters never see. A 64-byte frame occupies 84 bytes of wire time. So bandwidth computed from byte counters always understates true wire occupancy, and at small packet sizes it understates it badly. Monitoring graphs built on these counters are honest about data, not about the wire.
On a healthy interface every error column reads zero. The lesson ends with what each kind of non-zero means, and the lab makes you deliver that verdict yourself.
You read the table once. Suppose you now read it again, immediately, having typed nothing else and opened no other connection.
>>> Higher, and the reason is the trap in the first answer: new traffic DID happen, because the first table did not teleport to your eyes. Every character of it was wrapped in SSH encryption and transmitted over eth0, and every keystroke of the command you typed arrived through eth0 and was echoed back out. Watching a counter across the network moves the counter. If you picked the third answer: /proc reads never modify anything. The counters are not a mailbox to empty, they are an odometer, and nothing you read resets an odometer.
Same command. This time you know what to look for.
cat /proc/net/dev
prompt: student@stats-lab:~$ answer: cat /proc/net/dev output: Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed lo: 1284 16 0 0 0 0 0 0 1284 16 0 0 0 0 0 0 eth0: 100844 1117 0 0 0 0 0 0 109028 909 0 0 0 0 0 0 hint: The same read as before: cat /proc/net/dev
Put the two eth0 rows side by side. Receive went up about 2.4 KB and 24 packets: your keystrokes, plus the small TCP acknowledgments your machine sent for everything it received. Transmit went up about 3.8 KB and 22 packets: the previous table, encrypted and shipped to your screen, plus the echo of every character you typed.
You just measured the cost of your own presence. Interactive SSH sends roughly one packet per keystroke in each direction, which is why the packet deltas track how much you typed. This is the observer effect you predicted, and it is why, later in this lesson, a live counter view over SSH is never allowed to sit still.
Now look at lo. Untouched, and notice something that is always true: its Receive and Transmit halves are identical, 1284 bytes and 16 packets on both sides. Loopback packets are sent and received by the same interface, so each one is written into both halves of the same row. If lo ever shows unequal halves, the file was misread.
Reading a file twice and comparing by eye does not scale. The watch command re-runs any command on an interval (the -n flag sets the seconds) and repaints your screen with its output. Add -d and watch compares each repaint with the previous one, character by character, and highlights exactly the cells that changed. For a counter table that means the digits that moved light up and the dead columns stay dark.
watch -d -n 1 cat /proc/net/dev
prompt: student@stats-lab:~$ answer: watch -d -n 1 cat /proc/net/dev ||| watch -n 1 cat /proc/net/dev ||| watch -n1 cat /proc/net/dev ||| watch -d -n1 cat /proc/net/dev output: Every 1.0s: cat /proc/net/dev stats-lab: Wed Aug 5 09:14:32 2026
Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed lo: 1284 16 0 0 0 0 0 0 1284 16 0 0 0 0 0 0 eth0: 103217 1150 0 0 0 0 0 0 114366 941 0 0 0 0 0 0 hint: watch, the difference flag, an interval of one second, then the read: watch -d -n 1 cat /proc/net/dev
The top line is watch itself reporting what it is running and how often. Below it, the table, repainted every second, and on the real VM the eth0 byte digits flash on every single repaint. They have to: each repaint is a screenful of data sent over eth0, so the next repaint shows the cost of the previous one, forever. Roughly two kilobytes per second of transmit is the going rate for watching this table over SSH.
Press Ctrl+C to stop watch and get your prompt back.
In the real lab, the automatic progress check runs when your prompt returns. While watch is repainting, the prompt is away, so checkmarks for completed tasks appear after you press Ctrl+C, not during.
Remember from Lesson 1: /sys/class/net/eth0/ is the kernel control panel for the interface, with one file per fact, address, mtu, operstate. One directory deeper sits statistics/, and it is the counter box opened flat: one file per counter.
List it.
ls /sys/class/net/eth0/statistics/
prompt: student@stats-lab:~$ answer: ls /sys/class/net/eth0/statistics/ ||| ls /sys/class/net/eth0/statistics output: collisions rx_frame_errors tx_carrier_errors multicast rx_length_errors tx_compressed rx_bytes rx_missed_errors tx_dropped rx_compressed rx_nohandler tx_errors rx_crc_errors rx_over_errors tx_fifo_errors rx_dropped rx_packets tx_heartbeat_errors rx_errors tx_aborted_errors tx_packets rx_fifo_errors tx_bytes tx_window_errors hint: List the statistics directory under the eth0 entry in /sys/class/net
Twenty-four files, and the set is worth a second look. You can see the /proc columns in it (rx_bytes, tx_packets, collisions, multicast), but the error counters here are finer grained: the single errs column from the table splits into rx_crc_errors, rx_frame_errors, rx_length_errors and more. The table summarizes. This directory itemizes. It is the only window that shows every counter the kernel keeps.
And a fact that surprises people: every interface on every Linux machine offers exactly this same set of files, even lo. The list comes from generic kernel code, not from the driver, so a script written against these paths works on any interface of any machine.
These paths are long and tab completion eats them. Type cat /sys/cl, press Tab, continue with net/e, Tab, stat, Tab, rx_b, Tab. You will type about a third of the characters, and a path that completes is a path that exists, which kills typos before Enter.
Each file holds a single number and a newline. This is the window built for scripts: no headers to skip, no columns to count, no parsing at all. A monitoring agent that wants receive bytes reads one file and is done, which is exactly what most fleet monitoring does, at scale, every few seconds, on every machine.
cat /sys/class/net/eth0/statistics/rx_bytes
prompt: student@stats-lab:~$ answer: cat /sys/class/net/eth0/statistics/rx_bytes output: 105982 hint: cat the rx_bytes file inside that statistics directory
One number, no ceremony. It is higher than the rx column you saw in the watch frame, because of course it is: you have been typing.
One subtlety for the day you script this. Each file read fetches the whole counter box and prints one field, so two reads, even back to back, are two different instants. Read rx_bytes and then rx_packets and the pair will not describe exactly the same moment, which can make derived math (bytes per packet, for instance) come out slightly impossible. A row in /proc/net/dev is one snapshot, so its fields are consistent with each other. Glance at the table, script the files, and know what each one promises.
To dump every counter with its name in one shot: grep -H . /sys/class/net/eth0/statistics/*. The -H flag prints the filename in front of each value. Remember grep from Foundations; this is it moonlighting as a report generator.
A colleague takes an interface down for maintenance with ip link set and brings it back up a minute later.
>>> Nothing happens to it. The counters live in kernel memory attached to the interface itself, and an administrative down does not destroy the interface, so the odometer rides through. They vanish only when the interface truly ceases to exist: the machine reboots, the driver module unloads, or a virtual interface is deleted. The deliberately uncomfortable part of the correct answer is the second half: there is no reset command. Nothing in ip, ethtool or /proc zeroes these counters, by design, because more than one tool may be reading them. Every monitoring system therefore stores a previous reading and subtracts. If you picked the third answer: nothing is written to disk, ever, which is also why a reboot is one of the ways they clear.
Do not test this prediction on this VM. Your terminal session rides eth0, so ip link set eth0 down saws off the branch you are sitting on: the session hangs and you get to relaunch the lab. Every remote engineer eventually learns this on the wrong interface at the wrong time. Learn it here instead, without running it.
A history note that still bites: these counters used to be 32-bit, and a 32-bit byte counter wraps past zero at 4 GiB, which gigabit Ethernet can move in about 34 seconds. Tools grew wrap-detection logic, and the kernel grew the 64-bit structure you are reading today. On a modern 64-bit counter, a 100 gigabit link would need decades to wrap. If you ever meet a graph with an absurd downward spike on old embedded gear, you are probably looking at a wrap.
The ip link show command from Lesson 1 has a statistics flag, -s, which adds an RX block and a TX block per interface. It reads the same kernel box as the other two windows, formatted for human eyes.
ip -s link show eth0
prompt: student@stats-lab:~$ answer: ip -s link show eth0 ||| ip -s link show dev eth0 ||| ip -stats link show eth0 output: 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 02:fc:00:00:00:05 brd ff:ff:ff:ff:ff:ff RX: bytes packets errors dropped missed mcast 108126 1211 0 0 0 0 TX: bytes packets errors dropped carrier collsns 121408 1006 0 0 0 0 hint: The link show command from Lesson 1, with the statistics flag in front: ip -s link show eth0
The top two lines are Lesson 1: flags, MTU, state, MAC address. New are the RX and TX blocks underneath.
Compare the column names with the /proc table and you will catch each window choosing a different handful from the same box. The table gave you fifo and frame; this view gives you missed and mcast instead. Neither is wrong and neither is complete. Only the statistics directory shows all twenty-four. Knowing which window shows which counters is exactly the kind of thing that looks like trivia until the one counter you need is in the window you did not check.
Add a second -s (ip -s -s link show eth0) and iproute2 appends an error-breakdown line under each block, splitting errors into its causes: length, CRC, frame, FIFO. The sum says something is wrong. The breakdown says what kind of wrong.
A note on the fourth ledger, for when you work on physical servers. ethtool -S eth0 asks the driver for the counters the NIC hardware itself keeps, per queue, and on a datacenter NIC that is hundreds of entries the kernel never sees, including packets the hardware dropped before the kernel ever knew they existed. That is why it alone can disagree with the three kernel windows, and the disagreement is the diagnostic: kernel says zero drops, hardware says thousands, so the loss happened below the kernel. On this microVM the virtio driver keeps only a small per-queue set, so the kernel windows are the story here.
Stop and take stock. You can now read the same counter box three ways: the whole fleet of interfaces at a glance, one counter as one scriptable number, and one interface in a human-readable report. You know the three can never contradict each other, you know none of them can be reset, and you know why the numbers move while you watch them.
What you have not done yet is watch counters count someon
Practice Interface Statistics in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.