Carry a large file through a tunnel and the interface counter climbs by the size of the file. Nothing about that number implies the same volume reached a disk. Three different measurements get compared in these discussions, and they answer three different questions: how much traffic crossed a network layer, how many bytes a process read and wrote through system calls, and how much work reached the storage layer.

Comparing the first with the third is the usual mistake, and it produces either a phantom disk problem or a capacity plan that ignores the writes that really happen. The three are separable, and each has a correct use.

Three numbers that get conflated

Interface counters describe traffic handled at a network layer. They are the right number for bandwidth, for the size of a tunnel, and for billing models that price transit.

Process character counters describe bytes moved through read and write calls on any descriptor, including pipes, terminals and character devices. They are the right number for asking how busy a process is at the system-call boundary, and they are not a storage bill.

Storage counters describe bytes the kernel attempted to move to or from a block device. They are the right number for capacity planning, with the caveat that the kernel documents its own accounting limits: cached reads do not become storage reads, and a write that is later cancelled does not become a completed device write.

MeasurementQuestion it answers
Interface bytesHow much traffic crossed this layer
rchar and wcharHow much data the process moved through system calls
read_bytes and write_bytesHow much work reached the storage layer

Why a file descriptor is not a disk write

A userspace VPN exchanges packets with the kernel through a TUN device, which is a character device rather than a regular file. Reading from it hands over a packet; writing to it injects one. There is no page of file content being staged for storage, and no filesystem path involved.

That is why a busy tunnel can show enormous character counters with almost no storage counters. The process really did move those bytes, and the kernel really did not need a disk to do it.

cat /proc/$(pgrep -n openvpn)/io
ip -s link show tun0

Read the first output as cumulative since the process started, not as a rate. The second gives the interface view of the same traffic. A large gap between the two is expected and does not indicate a leak, a queue or a misconfiguration.

Where the bytes really come from

Persistent output has its own budget, and it is driven by operations rather than by payload. A verbose log level, a client list that changes often, or an error that repeats in a tight loop can generate substantial writes while the tunnel carries very little.

A status file is a snapshot rewritten at an interval, so its cost depends on the interval and on how many records it contains. It is not a running copy of every client's traffic, and shortening the interval changes the write volume without changing the traffic volume.

Ownership matters when you go looking for those writes. If the service hands messages to a logging daemon, the journal or log file is written by that daemon, and the bytes show up in the daemon's accounting rather than in the VPN process's own counters. Looking only at the VPN process explains neither the disk activity nor its absence.

Other contributors are easy to overlook: certificate and configuration reads at startup, hooks that write their own records, accounting plugins, crash dumps, and unrelated workloads on the same host. None of them should be estimated by equating tunnel traffic with bytes written.

Reading each counter correctly

Three details decide whether a comparison means anything. The process identity matters, because a restarted service resets its counters. The time interval matters, because a cumulative figure and a per-second chart are different measurements. And the unit matters, because a log file's current size is not a rate either.

grep -E 'rchar|wchar|read_bytes|write_bytes' /proc/$(pgrep -n openvpn)/io
du -sh /var/log/openvpn 2>/dev/null
journalctl --disk-usage

If you suspect real payload writes, look for a component whose job is to store payload: a packet capture tool, a proxy that caches bodies, a plugin that archives. Those produce storage traffic that scales with what they are asked to record, and they belong to a separate policy decision about retention and access.

Planning capacity with the right number

Size the packet path with network figures: throughput, cryptographic processing and memory movement. Size storage with the observability choices you actually made: log level, retention, status interval, and any capture that is deliberately enabled.

Keeping those two budgets separate also makes incidents easier to read. A full disk with modest tunnel traffic points at logs, captures or something unrelated, and the interface counter will never explain it. A saturated link with a quiet log points the other way.

The comparison worth making is between measurements of the same kind. Interface bytes against interface bytes over time tells you about the path. Storage writes against storage capacity tells you about retention. The cross-comparison, traffic against disk, only looks meaningful and never is.

Winndannde jowitiiɗe heen