Debugging a connection that opens but never sends data

The most frustrating class of network bug is the one that doesn't produce an error. A connect() call succeeds, a TCP handshake completes, both ends agree the socket is open — and then nothing. No RST, no FIN, no exception. The client just sits there waiting for bytes that never arrive, until some higher-level timeout eventually gives up and the application reports a generic "connection timed out," which tells you almost nothing about where the problem actually is.

This writeup is less about a single fix and more about the order of checks that narrows the problem down fastest, because in my experience most of the time wasted on these issues is spent checking things in the wrong order.

Step 1: confirm it's not application-layer

Before touching the network at all, rule out the simplest explanation: the server accepted the connection but the application behind it is blocked on something else — a lock, a slow dependency, a deadlock — and simply hasn't read from the socket yet. Check if the server process is actually making progress:

ss -tn state established '( dport = :443 )'

Look at Recv-Q and Send-Q. Data sitting in Recv-Q unprocessed means the kernel got it fine — the application just hasn't read it. That's an application bug, not a network one, and chasing it with packet captures is a waste of time.

Step 2: confirm the handshake is actually clean, not mimicking success

Some failures masquerade as successful handshakes. A middlebox or a transparent proxy can complete a TCP handshake on behalf of a backend that's unreachable, convincing the client that someone answered when nobody really has. A raw capture settles this immediately:

tcpdump -i any -nn 'tcp port 443' -w capture.pcap

Reproduce the hang, then open the capture. What you're looking for: does the handshake's SYN-ACK come from the IP and TTL you expect for the real server, or does something about it look like it originated closer to you than the actual destination? A suspiciously low or inconsistent TTL on the SYN-ACK compared to other traffic from the same host is a sign something in the path is answering instead of forwarding.

Step 3: check for payload-size-dependent throttling

A specific and easy-to-miss pattern: a connection works perfectly for the first several kilobytes, then simply stops — no reset, no close, just silence. If you can reproduce this consistently at roughly the same byte count across unrelated requests, that's a strong signal of some form of deep packet inspection or traffic shaping sitting in the path, rather than anything wrong with either endpoint.

A clean way to isolate this: serve a large file over plain, unencrypted HTTP from a throwaway server and download it from the client side while watching exactly how many bytes transfer before it stalls.

# on the server
python3 -m http.server 8000

# on the client
curl -o /dev/null -w '%{size_download}\n' http://SERVER_IP:8000/testfile

If the download consistently stalls at close to the same byte count run after run, you're not looking at a flaky connection — you're looking at something actively watching the stream and cutting it after a threshold. Note that this test is specifically useful for diagnosing plaintext interference; a path that throttles unencrypted HTTP by payload size won't necessarily behave the same way toward a TLS-wrapped connection, since the inspecting device may be unable to see enough of an encrypted stream to apply the same rule. Test both independently before drawing conclusions about the encrypted case from the plaintext one.

Step 4: isolate client vs. server vs. path

Once you suspect something in the path rather than either endpoint, the fastest way to confirm it is triangulation: try the same connection from a different network entirely (a different ISP, a different country, a VPS in another provider) and see if the same stall reproduces. If it doesn't, the problem is specific to the original network path, not the server. If it does reproduce everywhere, look harder at the server side first.

← Back to all posts