tcp_sendq_trace.py monitors per-socket TCP send buffer utilization over time using the Linux ss command. It produces a CSV time series that lets you identify which client connections are slow (not reading data), causing the server's kernel send buffer to back up.
- Runs
ss -tinm state establishedat a configurable interval to capture all established TCP connections. - Parses each connection's Send-Q (bytes queued in the kernel send buffer waiting to be ACKed or read by the remote side) and snd_buf (
tbfield fromskmem) which is the kernel's current send buffer size. - Computes utilization as
Send-Q / snd_buf(0.0 = idle, 1.0 = buffer full). - Writes timestamped rows to a CSV file for offline analysis or graphing.
- Linux with
iproute2installed (provides thesscommand) - No elevated privileges required (reads from
/proc/net/tcpwhich is world-readable)
# Default: sample every 100ms for 10 seconds, write to sendq.csv
python scripts/tcp_sendq_trace.py
# Custom interval and duration
python scripts/tcp_sendq_trace.py --interval 0.5 --duration 60 --out /tmp/trace.csv
# Only capture rows where Send-Q > 0 (reduces noise)
python scripts/tcp_sendq_trace.py --only-nonzero --duration 30
# Skip reverse DNS resolution of peer IPs (faster, no DNS dependency)
python scripts/tcp_sendq_trace.py --no-resolve --duration 30
# Only trace specific clients (by IP or hostname, one per line)
python scripts/tcp_sendq_trace.py --filter-file clients.txt --duration 30
# Only trace specific local (server) ports
python scripts/tcp_sendq_trace.py --port-filter-file ports.txt --duration 30
# Combine both filters (AND logic)
python scripts/tcp_sendq_trace.py --filter-file clients.txt --port-filter-file ports.txt --duration 30| Flag | Default | Description |
|---|---|---|
--interval |
0.1 |
Sampling interval in seconds |
--duration |
10.0 |
Total capture duration in seconds |
--out |
sendq.csv |
Output CSV file path |
--only-nonzero |
off | Only record rows where Send-Q > 0 |
--no-resolve |
off | Skip reverse DNS resolution of peer IPs |
--filter-file |
none | File with peer IPs/hostnames to include (one per line) |
--port-filter-file |
none | File with local ports to include (one per line) |
The CSV contains the following columns:
| Column | Description |
|---|---|
timestamp |
Unix epoch with microsecond precision |
local |
Local address:port |
peer |
Remote address:port |
peer_name |
Reverse DNS hostname of peer IP (empty if unresolvable or --no-resolve) |
send_q |
Bytes queued in kernel send buffer |
snd_buf |
Kernel send buffer size (from skmem tb field) |
util |
Utilization ratio (send_q / snd_buf, 6 decimal places) |
Example output:
timestamp,local,peer,peer_name,send_q,snd_buf,util
1706900000.123456,10.0.0.1:8080,10.0.0.5:12345,slow-client.lan,2621440,2626560,0.998052
1706900000.123456,10.0.0.1:8080,10.0.0.6:22222,fast-client.lan,0,46080,0.000000
1706900000.223456,10.0.0.1:8080,10.0.0.5:12345,slow-client.lan,2621440,2626560,0.998052Both --filter-file and --port-filter-file use the same format: one entry per line, with # comments and blank lines ignored.
Peer filter (--filter-file): each line is an IP address or DNS hostname. DNS names are resolved to IPs at startup.
# clients.txt — watch list
10.0.0.5
192.168.1.100
slow-client.example.com
problematic-server.lan
Port filter (--port-filter-file): each line is an integer port number.
# ports.txt — server ports to monitor
8080
443
5000
When both filters are provided, they compose with AND logic: a connection must match both the port filter and the peer filter to appear in the output.
utilnear 0: The client is reading data promptly. Healthy connection.utilnear 1.0 or above: The client is not reading (or reading too slowly). The kernel send buffer is full. This is a "bad" or slow client.send_qconsistently > 0 across multiple samples: Indicates a sustained backlog, not just a transient burst.
# Capture 30 seconds of data, only non-zero queues
python scripts/tcp_sendq_trace.py --duration 30 --only-nonzero --out slow_clients.csv
# Then analyze with standard tools
# Top offenders by average Send-Q:
awk -F, 'NR>1 {sum[$3]+=$4; n[$3]++} END {for(p in sum) printf "%s avg_sendq=%.0f\n", p, sum[p]/n[p]}' slow_clients.csv | sort -t= -k2 -rn