/proc/net and Networking sysctls: Where the Numbers Come From

Every Linux networking tool is a formatter over a handful of kernel files. Here is what is in them, how to read the hex, and what happens when you change the sysctls that matter.

A dark terminal card showing a raw /proc/net/tcp line in hexadecimal decoded to 0.0.0.0:22, alongside a TcpExt TCPFastRetrans counter.

Every tool in this cluster is, underneath, a formatter. ifconfig reads a file. netstat -s reads a file. nload, ifstat and bmon read the same file as each other. When those tools disagree, or when the one you want is not installed on the box in front of you, knowing where the numbers actually live turns a dead end into a one-line answer.

This article is part of the Linux networking commands guide. All output is from a Debian 13 host at 10.77.0.100 bridged into a CML lab, captured while it moved real traffic to servers at 10.77.2.10 and 10.77.3.10.

What is in there

j@llmbits:~$ ls /proc/net | wc -l
57
j@llmbits:~$ ls /proc/net | head -12
anycast6
arp
connector
dev
dev_mcast
dev_snmp6
fib_trie
fib_triestat
hci
icmp
icmp6
if_inet6

Two things to know before you read any of it.

First, these are not files. They are kernel data structures rendered as text at the moment you read them. There is no disk involved and no caching, which is why cat /proc/net/dev twice in a row gives different numbers.

Second, and this catches people constantly: /proc/net is per network namespace. Read it inside a container and you get that container's counters, not the host's. This is exactly why netstat run inside a pod shows an almost empty socket table while the host is busy. It is not broken; it is looking at a different namespace.

/proc/net/dev: the file behind half the tooling

j@llmbits:~$ 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: 3808252   64631    0    0    0     0          0         0  3808252   64631    0    0    0     0       0          0
ens224: 75028297  803129    0    0    0     0          0        28 2185022768  527116    0    0    0     0       0          0

Sixteen counters per interface. Now the same numbers through the modern command:

j@llmbits:~$ ip -s link show ens224
    RX: bytes  packets  errors  dropped  missed   mcast
      75028297  803129      0       0       0      28
    TX:  bytes packets errors dropped carrier collsns
    2185022768  527116      0       0       0       0

Identical, to the byte. ifconfig, ip -s link, nload, ifstat and bmon all read this and divide by elapsed time. That is the whole mechanism behind the live bandwidth monitors, and it explains why the counter-based ones agree with each other so closely while the packet-capture-based ones drift slightly.

The practical use is that you can build a rate meter with no tools at all:

j@llmbits:~$ A=$(awk '/ens224:/{print $10}' /proc/net/dev); sleep 5
j@llmbits:~$ B=$(awk '/ens224:/{print $10}' /proc/net/dev); echo $(( (B-A)*8/5/1000000 )) Mbit/s

Field 10 is transmitted bytes. That is the entire algorithm every one of those monitors implements.

/proc/net/snmp: the counters netstat -s formats

j@llmbits:~$ grep -A1 '^Tcp:' /proc/net/snmp
Tcp: RtoAlgorithm RtoMin RtoMax MaxConn ActiveOpens PassiveOpens AttemptFails EstabResets CurrEstab InSegs OutSegs RetransSegs InErrs OutRsts InCsumErrors
Tcp: 1 200 120000 -1 11296 10614 490 30 1 976175 1662308 9438 0 57 0

The format is a header line naming the fields and a value line in the same order. It is genuinely awkward to read by eye and trivial to parse in awk, which is the point: this file is an interface, not a report. Here, 1,662,308 segments out against 9,438 retransmitted is a retransmit rate of about 0.57 percent for the life of the host.

The counters are live. Snapshot them around 20 pings:

before: InMsgs=2338 InEchoReps=522 OutMsgs=1894
after : InMsgs=2358 InEchoReps=542 OutMsgs=1914

Exactly 20 on each counter. That precision is what makes these files worth reaching for: you can prove a packet was received by the stack, not merely that a tool claims it was.

The Forwarding=2 trap

This one has cost people real time. The first field of the Ip line looks like a boolean and is not:

j@llmbits:~$ sysctl -n net.ipv4.ip_forward
0
j@llmbits:~$ grep '^Ip: [0-9]' /proc/net/snmp | awk '{print "snmp Ip Forwarding field =", $2}'
snmp Ip Forwarding field = 2

The sysctl says forwarding is off. The file says 2. Both are correct, because /proc/net/snmp reports the SNMP MIB value of ipForwarding, where 1 means forwarding and 2 means not-forwarding. Any script that treats that field as true or false gets the answer backwards. Read /proc/sys/net/ipv4/ip_forward if you want a boolean.

/proc/net/netstat: where the interesting TCP counters hide

The extended counters, the ones that actually diagnose things, are in a separate file under the TcpExt prefix:

j@llmbits:~$ awk 'NR==1{for(i=1;i<=NF;i++)h[i]=$i} NR==2{for(i=1;i<=NF;i++) if(h[i]~/Retrans/) print h[i]"="$i}' /proc/net/netstat
TCPLostRetransmit=111
TCPFastRetrans=9348
TCPSlowStartRetrans=3
TCPRetransFail=0
TCPSynRetrans=60

Read that as a diagnosis. 9,348 fast retransmits against 3 slow-start retransmits means loss is being detected by duplicate ACKs, not by timeout, which is the good failure mode: the sender noticed quickly and recovered without stalling. A host with those numbers reversed is timing out, and users are feeling multi-second pauses. TCPSynRetrans=60 counts connection attempts that had to retry the SYN, which is the counter you want when someone reports intermittently slow connection setup.

ListenDrops and ListenOverflows live in the same file, and they are the definitive answer to "is my accept backlog too small." That question connects directly to net.core.somaxconn further down this page.

The hex files, and how to read them

/proc/net/tcp is a socket table written in hexadecimal, little endian, which looks hostile until you decode it once:

j@llmbits:~$ head -3 /proc/net/tcp
  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
   0: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 7039
   1: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 400884
   2: 64004D0A:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 525870

The address is four bytes in host order, so on x86 they are reversed. Port is plain hex:

j@llmbits:~$ python3 -c "
import socket,struct
h='00000000:0016'; ip,port=h.split(':')
print(socket.inet_ntoa(struct.pack('<L',int(ip,16))), int(port,16))"
0.0.0.0 22

Which matches what ss reports for the same socket:

j@llmbits:~$ ss -ltn
State  Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0      128          0.0.0.0:22        0.0.0.0:*
LISTEN 0      4096       127.0.0.1:631       0.0.0.0:*
LISTEN 0      5        10.77.0.100:8080      0.0.0.0:*

Decode the third entry yourself: 64004D0A reverses to 0A 4D 00 64, which is 10.77.0.100, and 1F90 is 8080. The st column is the TCP state as a hex enum, where 0A is LISTEN and 01 is ESTABLISHED. Everything ss shows you starts here.

/proc/net/route works the same way:

j@llmbits:~$ head -4 /proc/net/route
Iface	Destination	Gateway 	Flags	RefCnt	Use	Metric	Mask
ens192	00000000	0158A8C0	0003	0	0	101	00000000
ens224	00004D0A	00000000	0001	0	0	0	00FFFFFF
ens224	00004D0A	01004D0A	0003	0	0	0	0000FFFF

Decoded:

iface    destination      gateway          mask
ens192   0.0.0.0          192.168.88.1     0.0.0.0
ens224   10.77.0.0        0.0.0.0          255.255.255.0
ens224   10.77.0.0        10.77.0.1        255.255.0.0
ens192   192.168.88.0     0.0.0.0          255.255.255.0

Two routes to 10.77.0.0 with different masks: the connected /24 on the wire and a /16 static pointing at the lab gateway. That is the longest-prefix-match setup described in ip route, visible in raw form.

Note what this file cannot show you. It is IPv4 only, it has no concept of routing tables beyond the main one, and it cannot express anything policy routing does. ip route is not a nicer front end for this file; it is a different, more capable interface to the kernel. Same story for /proc/net/arp:

j@llmbits:~$ cat /proc/net/arp
IP address       HW type     Flags       HW address            Mask     Device
10.77.0.1        0x1         0x2         aa:bb:cc:00:04:00     *        ens224

j@llmbits:~$ ip neigh show dev ens224
10.77.0.1 lladdr aa:bb:cc:00:04:00 REACHABLE

The file gives you a flags bitmask, 0x2, which means the entry is complete and tells you nothing else. ip neigh gives you the actual neighbor state, REACHABLE here, which is the difference between "there is an entry" and "the kernel has confirmed it recently." Watch the same entry a minute later and it will read STALE. The full state machine is in ARP on Linux.

/proc/net/sockstat: memory pressure at a glance

j@llmbits:~$ cat /proc/net/sockstat
sockets: used 253
TCP: inuse 4 orphan 0 tw 11 alloc 6 mem 0
UDP: inuse 3 mem 0
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0

Small file, high value. orphan counts sockets with no file descriptor still holding kernel memory, tw counts TIME_WAIT, and mem is in pages, not bytes. When a box starts refusing connections and the socket table looks fine, this file and net.ipv4.tcp_mem usually explain it.

The sysctls worth knowing

/proc/sys/net is the writable half. sysctl is a thin wrapper: sysctl -n net.ipv4.ip_forward and cat /proc/sys/net/ipv4/ip_forward return the same value from the same place.

j@llmbits:~$ for k in net.ipv4.ip_forward net.ipv4.conf.all.rp_filter \
    net.ipv4.tcp_congestion_control net.ipv4.ip_local_port_range net.core.somaxconn \
    net.ipv4.tcp_rmem net.ipv4.tcp_wmem net.ipv4.icmp_ratelimit \
    net.core.netdev_max_backlog net.ipv4.tcp_syncookies; do
      printf '%-42s %s\n' "$k" "$(sysctl -n $k)"; done
net.ipv4.ip_forward                        0
net.ipv4.conf.all.rp_filter                0
net.ipv4.tcp_congestion_control            cubic
net.ipv4.ip_local_port_range               32768	60999
net.core.somaxconn                         4096
net.ipv4.tcp_rmem                          4096	131072	6291456
net.ipv4.tcp_wmem                          4096	16384	4194304
net.ipv4.icmp_ratelimit                    1000
net.core.netdev_max_backlog                1000
net.ipv4.tcp_syncookies                    1
net.ipv4.ip_forward
Turns the host into a router. Required for any NAT gateway, container bridge or VPN concentrator. If traffic reaches a box and dies there, check this first.
conf.all.rp_filter
Reverse path filtering. Set to 1 it silently drops packets arriving on an interface the reply would not use, which breaks asymmetric routing and multihomed hosts in a way that leaves no log entry.
tcp_congestion_control
The sender-side algorithm. cubic by default on Linux. Changing it alters how the connection reacts to loss, and it only affects sockets opened afterward.
ip_local_port_range
The ephemeral source port pool, 28,231 ports here. A busy proxy or NAT device with many short connections can exhaust it, and the symptom is connection failures under load with nothing wrong on the network.
net.core.somaxconn
Ceiling on a listening socket's accept queue. Set too low, new connections are dropped during bursts and ListenDrops in /proc/net/netstat climbs.
tcp_rmem / tcp_wmem
Minimum, default and maximum socket buffers. The maximum caps the window, which caps throughput on high latency paths. 6 MB of receive buffer over 100 ms of RTT is roughly a 480 Mbit/s ceiling.
icmp_ratelimit
How often the host will emit ICMP errors. This is why a traceroute's final hop flickers on a perfectly healthy path, covered in detail in the traceroute article.
netdev_max_backlog
Queue depth between the NIC driver and the IP stack. Overflow here shows up as drops in /proc/net/softnet_stat, not on the interface, which is why the NIC looks clean while packets vanish.

Changing one and watching the wire change

Reading is half of it. Here are two sysctls changed live on the lab host, with the observable result.

Congestion control

j@llmbits:~$ sysctl -n net.ipv4.tcp_available_congestion_control
reno cubic

Only two are loaded here; others (bbr, vegas) ship as modules and appear once you modprobe them. Running iperf3 to the same server under each, three ten-second runs each, alternating:

cubic : [  5]   0.00-10.00  sec  38.5 MBytes  32.3 Mbits/sec  122   sender
reno  : [  5]   0.00-10.00  sec  36.8 MBytes  30.8 Mbits/sec  155   sender
cubic : [  5]   0.00-10.00  sec  38.8 MBytes  32.5 Mbits/sec  144   sender
reno  : [  5]   0.00-10.00  sec  38.0 MBytes  31.9 Mbits/sec  171   sender
cubic : [  5]   0.00-10.00  sec  38.0 MBytes  31.9 Mbits/sec  148   sender
reno  : [  5]   0.00-10.00  sec  36.0 MBytes  30.2 Mbits/sec  165   sender

cubic averaged 32.2 Mbit/s with 138 retransmits; reno averaged 31.0 with 164. A small, consistent win for cubic on this path, and consistently fewer retransmits, which is what you would expect from cubic's gentler response to loss.

The methodology note matters more than the result. The first time I ran this, one run each, reno came out at 44.7 Mbit/s against cubic's 33.5, which would have made a dramatic and completely wrong claim. Six runs later the ordering had reversed and stabilized. A single ten-second sample on a virtual path with real loss is noise. If you are going to tune a sysctl on the strength of a measurement, measure it more than once.

You can confirm the setting took effect per socket, mid-flow:

j@llmbits:~$ ss -tin dst 10.77.3.10
ESTAB 0      273672   10.77.0.100:45098   10.77.3.10:5201
	 reno wscale:11,7 rto:208 rtt:5.016/0.803 mss:1448 pmtu:1500 rcvmss:536
	 advmss:1448 cwnd:17 ssthresh:11 bytes_sent:16305965 bytes_retrans:123080
	 bytes_acked:16158270 segs_out:11264 segs_in:5999 data_segs_out:11262
	 send 39259968bps lastrcv:3000 pacing_rate 47111960bps delivery_rate 28029032bps
	 delivered:11161 busy:3000ms unacked:17 retrans:0/85 rcv_space:14480
	 rcv_ssthresh:64088 notsent:249056 minrtt:1.568 snd_wnd:352256 rcv_wnd:64256

(That is one long line in the terminal, wrapped here for readability.)

The algorithm name is the first token. Note also ssthresh:11 below cwnd:17, which means this connection has already hit loss and is in congestion avoidance rather than slow start.

Ephemeral port range

Squeeze the pool down to eleven ports and watch source ports land inside it:

j@llmbits:~$ sysctl -w net.ipv4.ip_local_port_range="45000 45010"
net.ipv4.ip_local_port_range = 45000 45010

j@llmbits:~$ sudo tshark -i ens224 -c 4 -f "tcp port 80 and tcp[tcpflags] & tcp-syn != 0" \
    -T fields -e ip.src -e tcp.srcport -e ip.dst -e tcp.dstport
10.77.0.100	45008	10.77.2.10	80
10.77.2.10	80	10.77.0.100	45008
10.77.0.100	45010	10.77.2.10	80
10.77.2.10	80	10.77.0.100	45010

Both connections drew from the eleven-port window. This is the mechanism behind port exhaustion: a NAT gateway or a proxy making thousands of short-lived outbound connections per second, with each one holding its port through TIME_WAIT, can genuinely run out. When it does, the application reports connection failures and the network looks perfectly healthy, because it is.

Making it stick

sysctl -w writes to a kernel data structure and nothing else. Reboot and it is gone. For persistence, drop a file in /etc/sysctl.d/:

j@llmbits:~$ ls /etc/sysctl.d/
README.sysctl

j@llmbits:~$ echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-routing.conf
j@llmbits:~$ sudo sysctl --system

Files are read in lexical order, so a 99- prefix wins against distribution defaults. Two things that bite here: settings applied to an interface that does not exist yet at boot are silently skipped, so per-interface tunables for a bond or VLAN often need to be reapplied after the interface comes up; and a container gets its own namespace, so a sysctl set on the host does not necessarily apply inside it.

Key takeaways

  • /proc/net files are kernel structures rendered on read, not files, and they are per network namespace. That single fact explains most container networking confusion.
  • /proc/net/dev is what ifconfig, ip -s link, nload, ifstat and bmon all read. A five-line shell loop over field 10 is a bandwidth monitor.
  • In /proc/net/snmp, Ip: Forwarding uses SNMP semantics where 1 means on and 2 means off. It is not a boolean.
  • The useful TCP counters are in /proc/net/netstat under TcpExt. Many TCPFastRetrans with few TCPSlowStartRetrans is fast recovery working; the reverse means timeouts and user-visible stalls.
  • /proc/net/tcp and /proc/net/route are hex and little endian. Reverse the four address bytes and the port is plain hex.
  • ss and ip are not just prettier versions of these files. They use netlink and can express states, families and tables the legacy files cannot.
  • Changing tcp_congestion_control affects only new sockets, and ss -ti shows the algorithm per connection.
  • Measure a sysctl change more than once. A single run gave the opposite result to six runs on the same host.
  • sysctl -w does not survive a reboot. Use /etc/sysctl.d/99-name.conf and sysctl --system.

These files are where every other tool in the cluster gets its numbers, which makes them the place to go when the tools disagree or are missing. The rest of the toolkit, from ss to tshark to the bandwidth monitors, is indexed in the complete guide to Linux networking commands.

Read next