Updated

To see network throughput on a Linux server right now, read /proc/net/dev twice and divide the difference by the seconds between reads. That is what every tool here does for you: nload and bmon draw it per interface, iftop splits it per connection, sar -n DEV keeps a week of it on disk, and ss -ti tells you whether those bytes moved once or were retransmitted. The kernel never hands you a rate, only cumulative byte counters, and that single detail explains most of the confusion around Linux bandwidth monitoring.

Key takeaways

  • /proc/net/dev exposes 16 cumulative counters per interface. Two samples and a division is the entire trick.
  • On 64-bit kernels the byte counters are 64-bit and will not wrap in practice. On a 32-bit kernel they roll over every 34 seconds at 1 Gbps, which silently produces negative deltas.
  • A 1 Gbps link carries 125 MB/s of frames and lands near 118 MB/s of TCP payload at a 1500 byte MTU. Compare your MB/s against that, not against 1000.
  • Summing every interface double counts. Container traffic is measured on veth, then docker0, then eth0, and lo is memory to memory with no wire behind it.
  • Throughput does not detect packet loss. At 1 Gbps with 64 byte frames the packet rate ceiling is 1,488,095 pps, which you hit long before the bit rate looks alarming.

The methods, side by side

Method Install needed Best for Per connection Keeps history
/proc/net/dev No, kernel interface Scripts, exporters, custom alerting No No
ip -s link No, ships with iproute2 Totals plus errors and drops No No
nload Yes A live rate for one interface No No
bmon Yes Several interfaces on one screen No No
iftop Yes, and needs root Which peer is eating the link Yes No
sar -n DEV Yes, sysstat 7 to 28 days of local history No Yes, on the host
ss -ti No, ships with iproute2 Retransmits, cwnd and RTT per socket Yes No
Agent One line Fleet history, alerting, correlation No Yes

Method 1: /proc/net/dev and the counter problem

Every tool on this page reads the same file. It is a plain text table with one row per interface.

$ cat /proc/net/dev
Inter-|   Receive                                                |  Transmit
 face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
    lo: 5218844   41022    0    0    0     0          0         0  5218844   41022    0    0    0     0       0       0
  eth0: 412839201 2914022    0    0    0     0          0     18240 9218402113 4102938    0   14    0     0       0       0
docker0: 10482301   88214    0    0    0     0          0         0   20841022   91004    0    0    0     0       0       0

Those byte figures are totals since the interface came up, so a single read tells you nothing about right now. The kernel leaves differentiation to userspace, which is why a rate always needs two samples and an interval.

Here is the smallest correct version of that calculation.

#!/bin/bash
# Throughput on one interface over a 1 second window
IFACE=${1:-eth0}

sample() {
  awk -v i="$IFACE" '{ sub(/:/, " "); if ($1 == i) print $2, $10 }' /proc/net/dev
}

read rx1 tx1 < <(sample)
sleep 1
read rx2 tx2 < <(sample)

awk -v r="$((rx2 - rx1))" -v t="$((tx2 - tx1))" \
    'BEGIN { printf "rx %.2f MB/s   tx %.2f MB/s\n", r/1048576, t/1048576 }'
$ ./netrate.sh eth0
rx 1.18 MB/s   tx 17.99 MB/s

Two details are deliberate. The sub(/:/, " ") normalises the row first, because the separator between the interface name and the first counter has not been consistent across kernel versions and counter widths, and a glued eth0:412839201 breaks a naive $2 lookup. Field 2 is receive bytes and field 10 transmit bytes, since the receive block occupies fields 2 through 9.

The counters also include protocol headers, so they will never match an application level byte count. Expect interface numbers to read a few percent higher than what your web server thinks it sent.

ip -s link gives you the same data in a friendlier shape, with the error and drop columns right next to the bytes.

$ ip -s link show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
    link/ether 06:1a:2b:3c:4d:5e brd ff:ff:ff:ff:ff:ff
    RX:  bytes packets errors dropped  missed   mcast
     412839201  2914022      0       0       0   18240
    TX:  bytes packets errors dropped carrier collsns
    9218402113  4102938      0      14       0       0

Method 2: nload and bmon for a live per-interface rate

nload is the fastest way to watch one interface without writing anything. Neither tool is installed by default.

# Debian and Ubuntu
sudo apt install nload bmon

# RHEL, Rocky, AlmaLinux (both live in EPEL)
sudo dnf install epel-release && sudo dnf install nload bmon
$ nload eth0

Device eth0 [10.0.1.14] (1/3):
==============================================================================
Incoming:
                                              Curr: 9.84 MBit/s
                                              Avg: 8.12 MBit/s
   .|||.                                      Min: 1.02 MBit/s
  .|||||.                                     Max: 41.20 MBit/s
 .||||||||.                                   Ttl: 402.11 GByte
Outgoing:
                                              Curr: 147.22 MBit/s
                                              Avg: 131.04 MBit/s
  |||||||||                                   Min: 12.40 MBit/s
 ||||||||||.                                  Max: 402.88 MBit/s
.|||||||||||                                  Ttl: 8.42 TByte

Note the unit. nload reports bits per second by default while /proc/net/dev counts bytes, and mixing the two by a factor of 8 is the most common mistake I see in capacity tickets. Arrow keys cycle interfaces, and nload -m shows them all at once.

bmon covers the same ground with a list view that scales better past three or four interfaces, which is the normal situation on any host running containers.

bmon -p eth0,ens5 -o ascii

Method 3: iftop when you need to know who is using the bandwidth

Interface totals tell you the link is busy. They never tell you which client, backup job or replica is responsible. iftop reads packets through libpcap, so it needs root, and it groups traffic by source and destination pair.

sudo apt install iftop      # Debian and Ubuntu
sudo dnf install iftop      # RHEL with EPEL enabled, and Fedora
$ sudo iftop -i eth0 -nNP
                12.5Mb        25.0Mb        37.5Mb        50.0Mb       62.5Mb
+-------------+-------------+-------------+-------------+-------------------
10.0.1.14:443        => 203.0.113.9:52344       18.2Mb  16.4Mb  9.11Mb
                     <=                         1.02Mb   944Kb   612Kb
10.0.1.14:5432       => 10.0.2.31:41022         6.40Mb  5.88Mb  4.02Mb
                     <=                          820Kb   744Kb   501Kb
--------------------------------------------------------------------------
TX:      cum:  14.2GB  peak:  88.4Mb   rates:  41.2Mb  38.9Mb  22.4Mb
RX:             2.1GB         12.1Mb            3.8Mb   3.2Mb   1.9Mb
TOTAL:         16.3GB        100.5Mb           45.0Mb  42.1Mb  24.3Mb

The three rate columns are 2, 10 and 40 second moving averages, which separates a burst from a sustained transfer. -n skips DNS, -N skips service name lookup and -P shows port numbers. I run all three together because reverse DNS on a busy host makes the display lag reality. Add -B to read bytes instead of bits.

iftop samples continuously and stores nothing, so it belongs in an incident, not in a monitoring setup.

Method 4: sar -n DEV for local history

sar comes from the sysstat package, the same one that provides iostat and mpstat. Debian and Ubuntu ship the collector disabled, so enable it before expecting any history.

sudo apt install sysstat    # or: sudo dnf install sysstat

# Debian and Ubuntu: set ENABLED="true" in /etc/default/sysstat
sudo systemctl enable --now sysstat
$ sar -n DEV 1 3
Linux 6.8.0-45-generic (web-01)   07/25/2026   _x86_64_   (4 CPU)

02:31:12 PM   IFACE  rxpck/s  txpck/s   rxkB/s    txkB/s  rxcmp/s  txcmp/s  rxmcst/s  %ifutil
02:31:13 PM      lo    18.00    18.00     2.14      2.14     0.00     0.00      0.00     0.00
02:31:13 PM    eth0  9842.00 14201.00  1204.55  18422.31     0.00     0.00      0.00    15.10
02:31:13 PM docker0   112.00    98.00    10.44      8.02     0.00     0.00      0.00     0.00

%ifutil is the column worth reading first, because sar has already divided throughput by the negotiated link speed for you. Here 18.4 MB/s outbound on a 1 Gbps interface works out to 15.1% of the link.

For yesterday's numbers, point sar at the daily binary file. The path differs by distribution.

sar -n DEV -f /var/log/sysstat/sa24    # Debian and Ubuntu
sar -n DEV -f /var/log/sa/sa24         # RHEL and derivatives

Retention defaults to 7 days on Debian and Ubuntu and 28 on RHEL, set by HISTORY in the sysstat config. The same limits apply here as with CPU history through sar: the data dies with the host, nothing alerts on it, and correlating twelve servers means twelve SSH sessions.

Method 5: ss for connection state and retransmits

Throughput is only half the picture. ss reads the kernel's socket tables and tells you whether the bytes are actually being delivered.

$ ss -s
Total: 428
TCP:   312 (estab 184, closed 96, orphaned 0, timewait 94)

Transport Total     IP        IPv6
RAW       0         0         0
UDP       8         6         2
TCP       216       210       6
INET      224       216       8

A timewait count in the tens of thousands, or estab climbing while throughput stays flat, points at connection churn rather than bandwidth. For per socket detail, use ss -ti.

$ ss -tin state established '( dport = :443 or sport = :443 )'
     0      0   10.0.1.14:443    203.0.113.9:52344
     cubic wscale:7,7 rto:212 rtt:11.4/2.1 mss:1448 pmtu:1500
     cwnd:24 bytes_sent:1842204 bytes_acked:1842204 bytes_received:9210
     send 24.4Mbps pacing_rate 29.2Mbps delivery_rate 18.1Mbps retrans:0/7

retrans:0/7 means zero retransmits in flight and 7 across the life of the connection. delivery_rate well below pacing_rate on many sockets at once means the path, not the interface, is the constraint. For a host-wide count of retransmissions, nstat is quicker than parsing netstat -s.

$ nstat -az | grep -iE 'retrans|drop'
TcpRetransSegs                  18422              0.0
TcpExtTCPLostRetransmit         12                 0.0
TcpExtListenDrops               0                  0.0

Which interfaces to graph, and which to ignore

ip -br link lists what the host actually has.

$ ip -br link
lo               UNKNOWN  00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
eth0             UP       06:1a:2b:3c:4d:5e <BROADCAST,MULTICAST,UP,LOWER_UP>
docker0          UP       02:42:9c:11:04:aa <BROADCAST,MULTICAST,UP,LOWER_UP>
vethb41f2c9@if12 UP       ce:20:5a:77:0d:31 <BROADCAST,MULTICAST,UP,LOWER_UP>

Only eth0 corresponds to a physical link with a bill and a speed limit attached. The others distort any total you compute.

lo carries every localhost connection, memory to memory, with no wire behind it. An application talking to a local cache or a sidecar proxy can push several Gbps on lo while the machine sends almost nothing outside. Treat it as an activity signal, never as bandwidth.

Docker bridges duplicate rather than invent. A container reaching the internet has its bytes counted on the veth pair, again on docker0, and again on eth0, so summing every interface reports roughly three times the real figure. Graph physical interfaces on their own and let the container platform report container traffic. Hyperping's agent collects bytes in and out per interface but does not ingest per container or per pod metrics, so that split has to come from elsewhere.

What throughput tells you, and what it does not

High throughput on its own is not a problem. It becomes one when it approaches the link's capacity, and you need the capacity number to know that.

$ ethtool eth0 | grep -i speed
	Speed: 1000Mb/s

$ cat /sys/class/net/eth0/speed
1000

On virtualised NICs, virtio and AWS ENA among them, ethtool usually answers Speed: Unknown!. In that case use the instance type's documented bandwidth as your denominator, because the hypervisor enforces it whatever the guest reports.

Three failure modes hide behind a healthy looking bit rate.

Symptom Where throughput misleads What to check instead
Bandwidth saturation Real, but only relative to link speed %ifutil in sar -n DEV, ethtool speed
Packet loss Bytes still flow, they just flow twice ss -ti retrans, errs and drop in /proc/net/dev
Packet rate ceiling Bit rate looks low with tiny packets rxpck/s and txpck/s in sar, %si in top

That last row catches people out. A 1 Gbps interface handling 64 byte frames tops out around 1.49 million packets per second, and the per packet cost lands in softirq processing rather than the bit counter. If %si in top climbs while throughput sits at a fraction of line rate, the fix is batching or offload settings rather than a bigger link. The same reasoning applies to disk I/O, where IOPS and bandwidth are separate ceilings.

The honest limit of interface counters is that they describe one machine's view. They cannot see latency between your server and a user in Sydney, a DNS failure, or a load balancer dropping half the requests before they ever reach the box. That is what external checks are for, and it is why Linux server monitoring works best with both views on one screen.

How to use Hyperping for continuous network monitoring

The commands above answer what an interface is doing this second. An agent turns the same counters into a timeline you can query afterwards and alert on before anyone opens a ticket.

1. Install the agent on the server

Create a server in Hyperping, copy the install command, and run it on the host.

curl -fsSL https://hyperping.com/install.sh | sh -s HP_INSTALL_xxxxx

The agent embeds an OpenTelemetry collector, registers with systemd on Linux and launchd on macOS, and runs on amd64 and arm64. It sits around 50 MB RSS with no JVM, scrapes every 30 seconds, and ships gzipped OTLP/HTTP payloads that arrive within a couple of seconds. If ingest is unreachable, samples buffer in an on-disk queue at /var/lib/hyperping/queue that survives reboots. Re-running the installer rotates credentials while keeping the same server UUID and history. The install the agent docs cover the full flow. Windows is not supported yet.

2. Read network throughput next to CPU, memory, filesystem and disk I/O

The agent collects bytes in and out per interface and derives TX and RX rates in the dashboard, which is the two-sample division from Method 1 done for you every 30 seconds. On the same timeline you get CPU time per state with load averages, memory used and available, per-mount filesystem usage with device and filesystem type, and read and write bytes per block device. Metric names follow the OpenTelemetry hostmetrics semantic conventions, listed field by field in the metrics collected reference. Packet error and dropped packet counters are not ingested, so keep ip -s link and nstat for loss investigations.

Hyperping server detail page for prod-web-01 with a Network bandwidth chart plotting Download and Upload in Mb/s over the last hour, alongside CPU utilization, Memory used, Load average, Disk IO and Disk usage charts

3. Alert on network pressure and agent liveness

Server alerting routes a threshold through your existing escalation policies and on-call schedules. For network, alert on sustained throughput over several consecutive samples rather than a single burst, since backups and deploys trip a one-shot rule every time. Agent liveness is handled for you: metrics move to Stale and then Offline past a threshold, which opens an outage and fires the policy. Server alerting is on the paid plans, with Essentials at $24/mo billed yearly for 5 agents and 7 days of metric history, Pro at $74/mo for 20 agents and 14 days, and Business at $249/mo for 100 agents and 30 days. The free plan gives you 1 agent and 2 days of history without server alerting, enough to try on one box. Details are on the pricing page.

4. Pair interface counters with external uptime checks

Host metrics tell you the link is busy. Uptime checks from 18 to 19 regions tell you whether that cost anyone anything: a throughput spike next to flat response times is a capacity note for next quarter, the same spike next to rising response times in three regions is a page. The server monitoring page puts both on one timeline, and agent-based monitoring plus external checks is what I would run on any fleet above two machines.

5. Publish a status page when saturation reaches users

Once saturation does turn into slow or failed requests, connect the affected monitors to a status page so customers see the incident without emailing support. Uptime history publishes automatically, which removes the manual reporting most teams still do by hand.

Where to start

Debugging a live problem: run sar -n DEV 1 5 for the %ifutil number, then sudo iftop -i eth0 -nNP to find the peer responsible. Check ss -ti for retransmits before concluding the link is too small, because loss and saturation need different fixes.

Building monitoring instead: sar gets you a free week of local history, and an agent makes the numbers outlive the host and page someone at 3am. Graph throughput next to CPU usage and memory usage, since saturation, softirq load and memory pressure usually show up together.