Linux TCP_INFO: Read Per-Socket Telemetry Without Assuming a Fixed ABI
Use Linux TCP_INFO to inspect RTT, delivery, loss, and socket state while handling struct growth, field availability, sampling limits, and platform boundaries.
TCP_INFO is a Linux TCP socket option that returns a snapshot of kernel-maintained connection information in struct tcp_info. It can help an application explain why one of its own TCP connections is slow, stalled, retransmitting, or constrained by a peer’s receive window. It is an observation interface, not a tuning knob, a packet capture, or a portable definition of TCP health.
Treat the result as a versioned prefix of a kernel/userspace ABI. Fields have been added over time, and the kernel running a program may be older than the headers used to compile it. A robust collector checks the returned length before reading newer fields, associates every sample with the lifetime of one socket, and records units and sampling time alongside values.
Query the TCP socket you actually own
Call getsockopt() at IPPROTO_TCP level with TCP_INFO and a buffer for the struct tcp_info declared by the build headers. The option is Linux-specific. Other operating systems may expose a similarly named option or a related connection structure, but those interfaces are not interchangeable ABIs.
optlen is an input buffer size and a returned length. Do not assume that a successful call filled every field in a current header. The kernel may return a shorter structure when a field was added after that kernel version. Conversely, application code should cap the usable size to its own compiled structure before calculating field availability.
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stddef.h>
#include <stdio.h>
#include <sys/socket.h>
static void
report_tcp_info(int fd)
{
struct tcp_info info = {0};
socklen_t returned = sizeof(info);
if (getsockopt(fd, IPPROTO_TCP, TCP_INFO, &info, &returned) == -1) {
/* Report errno for this socket; do not publish a partial sample. */
perror("getsockopt(TCP_INFO)");
return;
}
size_t available = returned < sizeof(info) ? returned : sizeof(info);
#define HAS_FIELD(member) \
(available >= offsetof(struct tcp_info, member) + sizeof(info.member))
if (HAS_FIELD(tcpi_rtt))
printf("smoothed_rtt_us=%u\n", info.tcpi_rtt);
if (HAS_FIELD(tcpi_total_retrans))
printf("total_retrans=%u\n", info.tcpi_total_retrans);
#undef HAS_FIELD
}
Compile and run this collector on Linux with headers that define TCP_INFO and struct tcp_info. This example uses the libc <netinet/tcp.h> declaration and reads only older prefix fields. That header is a compile-time ceiling: its struct tcp_info may lag the kernel UAPI, so a runtime length check cannot make a field visible to a compiler if the installed libc header does not declare it. A Linux-only collector that needs newer fields can instead build against <linux/tcp.h> from the matching UAPI headers, while keeping the same runtime length guards. Select one structure definition for a translation unit; including both TCP headers can produce conflicting declarations. Check the actual build headers and the deployed kernel rather than assuming that either version alone guarantees a metric.
Interpret metrics as samples, not verdicts
The structure exposes state, retransmission information, windows, timing, delivery-rate estimates, and cumulative byte and segment counters. The exact set grows, so an absent field means “not available in this ABI,” not zero. A present zero also needs context: the connection may not yet have enough traffic to produce a timing sample, or the relevant state may not apply.
In the cited Linux source snapshot, the TCP implementation reports tcpi_rtt from its smoothed RTT state in microseconds. It is not the most recent one-way delay, and it does not reveal where time was spent in an application, queue, proxy, or remote service. Several newer fields have their own documented units; for example, the UAPI header labels busy, receive-window-limited, and send-buffer-limited time counters in microseconds, while total RTO-recovery time is in milliseconds. Keep units attached to metrics rather than applying one conversion to every integer in the structure.
Counters such as bytes acknowledged, bytes received, and retransmission totals are meaningful as deltas only when both samples refer to the same socket lifetime and a monotonic interval. A reconnect creates a new TCP state machine and resets many counters. The peer’s advertised window, congestion window, and delivery estimate describe different constraints; none alone proves that the application is currently blocked. A retransmission counter is evidence that retransmission occurred, not a complete diagnosis of why it occurred.
Use TCP_INFO when the process already has the socket descriptor and needs its local TCP view. It does not provide payload data, a complete packet history, or a universal way to inspect arbitrary sockets owned by unrelated processes. It also cannot replace application timing: record when the request was queued, when the first byte was written, when the response arrived, and when application work completed if end-to-end latency is the question.
Make collection resilient to ABI and lifecycle changes
Probe the length returned on every call, because the program may run across kernel versions. Guard a field with offsetof(struct tcp_info, field) + sizeof(field), not with an assumed kernel release number. This keeps code correct when vendor kernels backport fields or when the build headers and runtime kernel differ. If a desired field is absent, emit an explicit availability flag or omit that metric; do not substitute zero and let a dashboard report a false measurement.
Take samples at useful transitions: after connect completes, when an application request begins to exceed its budget, after a retry or recovery, and before closing a long-lived connection. Extremely frequent polling adds syscalls and produces many highly correlated snapshots. It can make graphs look precise without making the diagnosis more reliable. Keep the sampling cadence independent of the application’s retry loop.
Tag every sample with a stable connection-generation identifier and an observation timestamp from a monotonic clock. A file descriptor number can be reused after close, so it is not a connection identity by itself. Record whether the socket is connecting, established, closing, or already failed, and avoid assuming that a field retains the same meaning in every state. When comparing before-and-after values, discard samples that straddle a reconnect or descriptor replacement.
For a field-level check, a small Linux test client can open a loopback TCP connection, exchange a known amount of data, and call getsockopt() before and after the transfer. Verify that the returned length is respected, that required fields are reported only when present, and that a second connection gets a new sample identity. A meaningful diagnostic test also exercises an idle connection, a deliberately slow receiver, and an early close; it should not assert that a local test must produce a particular RTT or retransmission count.
Keep platform and measurement boundaries visible
Do not reuse Linux’s struct tcp_info layout to parse macOS, FreeBSD, or another operating system’s socket option. Similar names do not guarantee identical field order, widths, units, state constants, or availability. Put platform-specific collection behind separate adapters and normalize only the metrics whose semantics have been verified on each platform.
Use a packet trace when the question requires sequence-level evidence, and use application traces when the question concerns queueing or request latency. TCP_INFO is strongest when it is treated as a compact local snapshot of the TCP control block. It becomes misleading when a graph collapses state, cumulative counters, changing ABI prefixes, and application outcomes into one unlabeled “connection health” score.
Related:
- Linux TCP_USER_TIMEOUT: Bound Stalled Established Connections
- Linux SO_TIMESTAMPING: Trace Packet Time Through the Network Stack
Sources:
- Linux
tcp(7): TCP_INFO and Linux TCP socket options getsockopt(2): value-resultoptlencontract- Pinned Linux UAPI TCP socket definitions and
struct tcp_info - Pinned Linux TCP implementation: populating TCP_INFO fields
- Pinned Linux internal TCP state: scaled
srtt_usunits - glibc source discussion: synchronizing
netinet/tcp.hfields with Linux UAPI