Chapter 16: Debugging and Diagnosing Network Problems
Chapter 16: Debugging and Diagnosing Network Problems
When a distributed service slows down or drops connections in production, application logs and high-level metrics are often insufficient. Packets drop inside router queues, firewalls silently inject RST packets, and application code stalls inside un-instrumented system calls.
To diagnose deep network failures, you must look beneath the Go runtime and examine raw packets, kernel socket buffers, and system calls.
In this chapter, you will master essential network diagnostic tooling: packet capture with tcpdump, protocol analysis with Wireshark, system call tracing with strace, socket inspection with ss, and runtime goroutine debugging with delve.
When Profiling Is Not Enough
Standard profiling (pprof) tells you what your Go code is doing while CPU is actively running or memory is being allocated. But what if your server is completely idle, CPU usage is 0%, yet all client requests are timing out?
In these scenarios, goroutines are blocked in kernel sleep states (_Gwaiting), packets are being dropped by intermediate switches, or the kernel socket receive buffer is overflowing. Solving these issues requires inspecting the boundary between the Go runtime and the Linux kernel.
Packet Capture with tcpdump
tcpdump is the premier command-line packet analyzer for Linux and Unix systems. It captures raw frames directly from network interfaces using the kernel’s packet filter (BPF).
Essential tcpdump Command Recipes
# Capture packets on loopback interface for port 8080 without resolving DNS
sudo tcpdump -i lo -nn port 8080
# Capture with ASCII and Hex payload inspection (-X)
sudo tcpdump -i any -nn -X -s 0 'tcp port 9090'
# Save packets to a pcap file for offline Wireshark analysis
sudo tcpdump -i eth0 -nn -w capture.pcap 'tcp port 443'
# Filter specifically for TCP RST (reset) packets
sudo tcpdump -i any 'tcp[tcpflags] & (tcp-rst) != 0'
# Filter for TCP SYN or FIN packets (connection lifecycle events)
sudo tcpdump -i any 'tcp[tcpflags] & (tcp-syn|tcp-fin) != 0'Analyzing Packets with Wireshark
Wireshark provides a graphical interface for analyzing .pcap files captured by tcpdump.
[ Frame List: No., Time, Source, Destination, Protocol, Length, Info ]
┌───────────────────────────────────────────────────────────────────┐
│ 1 0.000000 127.0.0.1 127.0.0.1 TCP 74 54321 → 8080 [SYN] │
│ 2 0.000042 127.0.0.1 127.0.0.1 TCP 74 8080 → 54321 [SYN,ACK]│
│ 3 0.000065 127.0.0.1 127.0.0.1 TCP 66 54321 → 8080 [ACK] │
│ 4 0.001200 127.0.0.1 127.0.0.1 TCP 85 54321 → 8080 [PSH,ACK]│
└───────────────────────────────────────────────────────────────────┘
Key Wireshark Workflows for Go Engineers
- Follow TCP Stream: Right-click any packet and select Follow \(\to\) TCP Stream to reconstruct the exact bidirectional byte conversation between client and server.
- Detecting Retransmissions: In the display filter, enter
tcp.analysis.retransmissionortcp.analysis.lost_segmentto instantly identify packet loss and network congestion. - Round-Trip Time (RTT) Graphing: Go to Statistics \(\to\) TCP Stream Graphs \(\to\) Round Trip Time to see if latency spikes correlate with packet retransmissions.
System Call Tracing with strace
strace intercepts and records the system calls made by a process and the signals received by it.
Attaching strace to a Running Go Service
# Trace all threads (-f) for a running Go PID, filtering network calls
sudo strace -f -p <PID> -e trace=network,epoll_wait -s 128Understanding strace Output
// Goroutine accepts an inbound connection:
[pid 12345] accept4(3, {sa_family=AF_INET, sin_port=htons(54321), sin_addr=inet_addr("127.0.0.1")}, [112], SOCK_CLOEXEC|SOCK_NONBLOCK) = 6
// Setting socket options:
[pid 12345] setsockopt(6, SOL_TCP, TCP_NODELAY, [1], 4) = 0
// Netpoller registers the new descriptor with epoll:
[pid 12345] epoll_ctl(4, EPOLL_CTL_ADD, 6, {events=EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, data={u32=1432, u64=1432}}) = 0
// Non-blocking read returning data:
[pid 12345] read(6, "GET / HTTP/1.1\r\nHost: ...", 4096) = 28If you see read(6, ...) returning EAGAIN repeatedly, you know the Go runtime netpoller is properly handling non-blocking states. If you see a file descriptor remaining open without subsequent reads or closes, you have found a connection leak.
Connection State Inspection with ss
The ss (Socket Statistics) command replaces legacy netstat, querying socket details directly from the Linux kernel tcp_diag subsystem.
# Display all listening and established TCP sockets with process info:
ss -t -a -p
# Display internal TCP socket timers and memory buffers:
ss -t -i -e -aInterpreting Recv-Q and Send-Q
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 128 0 0.0.0.0:8080 0.0.0.0:*
ESTAB 0 34560 10.0.0.5:8080 198.51.100.2:45210
ESTAB 16384 0 10.0.0.5:8080 198.51.100.9:51234
- For Listening Sockets:
Send-Q: The maximum backlog queue size (somaxconn).Recv-Q: The number of completed connections currently waiting to be accepted byln.Accept(). IfRecv-Q > 0on a listening socket, your application’s accept loop is falling behind!
- For Established Sockets:
Send-Q: Bytes buffered in kernel memory waiting to be sent or acknowledged by peer. High values indicate a congested network or stalled client.Recv-Q: Bytes received by kernel but not yet read by application viaconn.Read(). High values indicate your Go worker goroutine is bottlenecked or stuck.
Debugging Goroutine Hangs with delve
When your Go application freezes, attach the Delve (dlv) debugger to inspect all live goroutines without restarting the process.
# Attach delve to the running process
sudo dlv attach <PID>Delve Commands
# List all goroutines and their current wait state
(dlv) goroutines
# Inspect a specific stuck goroutine
(dlv) goroutine 42
# View stack trace
(dlv) stackLook for goroutines stuck in runtime.netpollblock or internal/poll.(*FD).Read. If a goroutine has been in that state for hours, you have identified a missing read deadline!
Real-World Debugging Walkthroughs
Case 1: The Accumulating CLOSE_WAIT Sockets
Symptom: Running ss -s reveals 15,000 sockets in CLOSE_WAIT state, eventually exhausting file descriptors. Root Cause: CLOSE_WAIT means the remote client has sent a FIN (closed their side), but the local Go application has never called conn.Close(). Fix: Search the codebase for early return statements that bypass defer conn.Close().
Case 2: Sudden Connection Reset by Peer (RST)
Symptom: Clients report intermittent read: connection reset by peer errors. Root Cause Captured in tcpdump: The Go server called conn.Close() while there was still unread data sitting in the socket’s kernel receive buffer. By default, closing a socket with unread data causes the Linux TCP stack to immediately send an abortive RST instead of a graceful FIN. Fix: Ensure all client input is drained before terminating the connection, or use CloseWrite() to signal half-close.
Chapter Summary
- When high-level metrics fail, inspect kernel sockets and raw frames using
tcpdump,ss,strace, anddelve. tcpdump -i any -nncaptures raw packets with BPF filters for offline analysis in Wireshark.strace -f -e trace=networkreveals exact non-blocking system calls,epollregistrations, and file descriptor leaks.- On listening sockets, a non-zero
Recv-Qinssproves the application accept loop cannot keep pace with inbound connection arrival rates. - Sockets stuck in
CLOSE_WAITindicate the local application forgot to callconn.Close(). - In the final chapter, we will synthesize our learnings into the guiding principles of reliable network programming in Go.