Packet loss on a VPS uplink is reported by three different layers, and they disagree. Increase netstat -s counters point at the kernel stack, ifconfig drop counters point at the virtual interface, and the provider’s SLA refers to a physical link you cannot see. This article walks the tooling that isolates where the loss actually happens: tcpdump for capture, nstat for kernel counters, and a small eBPF program to attribute drops to a specific point in the receive path.
Start with counters — three of them, not one
# kernel stack errors
nstat -az | grep -Ei 'drop|error|retrans|overflow|prune|collaps'
# virtual interface drops
ip -s link show eth0
# per-socket retransmit rate
ss -ti | grep -oE 'retrans:[0-9/]+' | head
# tcpdump-level: is it loss or latency? watch SYN/ACK on a probe
tcpdump -ni eth0 -c 200 'tcp[tcpflags] & tcp-syn != 0'
Each counter means something specific. TcpExtListenOverflows and TcpExtListenDrops are application-side: the accept queue filled. TcpExtTCPBacklogDrop is the same queue at a different stage. TcpRetransSegs is loss as TCP saw it, whether the loss was upstream, downstream, or self-inflicted by a buffer overflow. ifconfig‘s RX dropped is the NIC or virtual switch discarding before the kernel ever counts it. Map the symptom to the layer with the table below.
| Counter | Layer | Meaning | Typical fix |
|---|---|---|---|
TcpExtListenOverflows | Application | Accept queue full, app not calling accept() | Raise net.core.somaxconn, fix slow app |
TcpExtListenDrops | Application | SYN dropped because queue full | Same, plus increase backlog in app |
TcpRetransSegs | TCP | Segment lost or ACK lost | Investigate path, not the server |
TcpExtTCPRcvCollapsed | Kernel memory | Receive buffer too small | Raise tcp_rmem max |
ifconfig RX dropped | NIC / virtual switch | Hardware or hypervisor discard | Provider-side, raise queue |
UdpRcvbufErrors | Socket | UDP buffer overflow | Raise net.core.rmem_max |
The distinction that saves the most time: server-side counters explain loss the server caused. A steady TcpRetransSegs climb with clean listen counters and zero receive-buffer collapses means the loss is on the path, and no amount of server tuning will remove it. Choosing a provider whose network is actually sized for the workload is the real fix, which is why network tier and peering quality belong in the buying decision rather than being an afterthought.
tcpdump correctly — and its limits
tcpdump sees packets that reached the socket layer, so it cannot prove loss on the wire. It can prove the opposite: if a SYN arrives and no SYN-ACK leaves within a few milliseconds, the drop is server-side. If retransmissions appear in the capture without corresponding duplicates, the loss is upstream of the capture point.
# watch retransmits and duplicate ACKs for one flow, ring-buffer to limit disk use
tcpdump -ni eth0 -s 96 -W 10 -C 50 \
'host 203.0.113.10 and (tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0)' -w /tmp/probe.pcap
# after capture, count retransmissions and duplicates
tcpdump -nr /tmp/probe.pcap 'tcp[tcpflags] & tcp-ack != 0' | wc -l
tshark -r /tmp/probe.pcap -Y 'tcp.analysis.retransmission || tcp.analysis.duplicate_ack' -T fields -e tcp.seq 2>/dev/null | wc -l
The critical caveat: drop counters you see in tcpdump output (“packets dropped by kernel”) mean tcpdump itself could not keep up, not that the network lost packets. On a busy VPS, always filter before capturing rather than capturing everything, and note the drop count printed at the end of a run. A capture with self-reported drops is not evidence of network loss.
eBPF for attribution when counters are ambiguous
When nstat shows drops but nothing points to a cause, a tracepoint on the kernel’s drop function gives the exact reason and call site. skb:kfree_skb fires for every dropped packet and carries the reason code, which is the missing link between “we dropped 4,000 packets” and “we dropped them because the socket receive queue was full.”
# Option 1: bpftrace — count drops by reason code (kernel /dev/null || sudo /usr/sbin/tcpdrop-bpfcc
The reason codes that matter in practice: NO_SOCKET means no matching listener or connection (port scanning, or a service that died); TCP_ABORT_ON_CLOSE and TCP_RESET are reset-driven; SOCKET_FILTER means your own iptables/nftables or eBPF filter discarded it; QDST_COALESCE and MEMORY mean kernel memory pressure on queues. If you see SOCKET_FILTER, the “network loss” is your own firewall and the investigation ends at nft list ruleset.
One practical note on VPS kernels: bpftrace and bcc need matching kernel headers (linux-headers-$(uname -r)) and a kernel with BTF or a full debuginfo set. On providers that ship custom kernels without headers, install the headers first — if they are unavailable, use tcpdrop from the kernel’s own tools/bpf only after verifying /sys/kernel/btf/vmlinux exists. Container-based VPS plans often restrict bpf() with seccomp, so run these on the host, not inside a Docker container.
Putting it together
The decision order is: nstat first to classify the layer, tcpdump second to confirm which direction a flow fails in, and eBPF third only when counters show a drop with no identifiable cause. In most incidents involving a self-managed VPS, the answer is in the first command — either a listen overflow from an application that stopped accepting, or a steady retransmit rate that turns out to be a congested path outside the server entirely.
When everything above comes back clean and loss persists, the constraint is provider-side: an oversubscribed uplink, a saturated virtual switch, or a peering path with insufficient capacity at peak hours. At that point no sysctl will help, and the decision becomes a procurement one. The VPS performance and network tier comparison is the place to check before migrating, because a host on a congested shared uplink will reproduce the same loss pattern on any configuration you apply.


Leave a Reply
You must be logged in to post a comment.