Networking for Developers
Debugging Network Problems and Networking Interview Questions
curl, dig, ss, tcpdump and Wireshark basics, a troubleshooting checklist, and interview questions with model answers.
On this page
- Words used in this lesson
- The method: one layer at a time
- curl: the tool you will use most
- dig: ask DNS directly
- ss: who is listening, who is connected
- Reach checks: ping, nc and traceroute
- tcpdump: look at the real packets
- Wireshark and tshark: the basics
- Checking TLS from outside: openssl sclient
- Common failures and what they mean
- A troubleshooting checklist, layer by layer
- Try it
- Interview questions
- Key takeaways
- What's next
In one line: most network bugs are found by asking one small question at a time, from the bottom up: can I find the name, can I reach the machine, can I open the port, does TLS work, what does HTTP say, and where did the time go? A handful of tools answers each question (curl, dig, ss, tcpdump), and the error message you get usually names the layer that failed.
This is the last lesson of the series. The earlier lessons gave you the pieces: layers and packets, IP and routing, TCP and UDP, DNS, HTTP, TLS, proxies and load balancers and real-time connections. Now we put them to work. When something is broken at 2 a.m., you do not need more theory. You need a short list of commands, a habit of reading their output, and a fixed order to follow.
The lesson has two halves. The first half is a toolbox, with real output from a small lab. The second half is a long list of interview questions with model answers, because networking is asked about in almost every backend, DevOps and full-stack interview.
What I ran / checked. Everything below ran on a Linux sandbox with curl 8.5.0 (OpenSSL 3.0.13), dig 9.18.39 (from BIND), ss from iproute2 6.1.0, tcpdump 4.99.4 (libpcap 1.10.4), nc (netcat), traceroute, OpenSSL 3.0.13 and Node.js 22. The lab has a tiny Node HTTP server on port 8090 with a /fast page, a /slow page that waits 1.5 seconds, a /redir page that redirects, and a /500 page that fails. For DNS I wrote a 20-line UDP DNS responder in Python on port 5354 that knows two names (app.lab.test and api.lab.test) and answers "does not exist" for anything else, because this sandbox has no working public DNS. Output is pasted from real runs. Ports, sequence numbers, dates and timings change every time you run it. Sources: this lesson is built from my own runs and from the earlier lessons of this series (their lab notes list the specs and docs I read there). I did not read any new web documentation for this lesson, and I did not look up man pages online, so flag meanings come from running the tools and from --help output. I could not run: tshark and Wireshark (neither is installed in this sandbox), so the Wireshark section is explanation only, and the tshark commands in it are from my knowledge, not from a run. I also could not reproduce a real "packets silently dropped" hang (the sandbox has no firewall I can change) or a real internet path with packet loss, so the sections on timeouts and loss describe what to expect and mark which output is real.
Words used in this lesson
- A timeout means a wait that ended because nothing came back in time. It is different from a refusal, where something answered "no".
- Connection refused means the machine answered, and said no program listens on that port. It is an active answer (a TCP RST).
- A packet capture (or pcap) is a recording of packets on a network interface, saved to a file so you can read it later.
- tcpdump is a command-line tool that captures and prints packets.
- Wireshark is a graphical program that reads packet captures and decodes them layer by layer. tshark is its command-line twin.
- A BPF filter is the small filter language (
tcp port 80 and host 10.0.0.5) that tcpdump and Wireshark use to choose which packets to capture. - A socket state is the stage a TCP connection is in:
LISTEN,ESTABLISHED,TIME_WAIT,CLOSE_WAITand so on. - TTFB (time to first byte) is the time from sending a request until the first byte of the answer arrives.
- NXDOMAIN is the DNS answer that means "this name does not exist".
- An exit code is the number a command returns when it ends.
0means success. curl uses many different non-zero codes to say what failed. - A resolver is the part of the system (or a server) that turns a name into an address.
The method: one layer at a time
When a request fails, the worst thing to do is to change things at random. The second worst thing is to stare at the app log. Instead, work through the stack in order. Each step has one question and one tool.
| Step | Question | Tool | A failure looks like |
|---|---|---|---|
| 1. Name | Does the name turn into an address? | dig, curl exit code 6 | NXDOMAIN, Could not resolve host |
| 2. Route | Is there a path to that address? | ip route, traceroute, ping | Network is unreachable, no reply |
| 3. Port | Can I open a TCP connection to the port? | nc -zv, curl exit 7 or 28 | Connection refused, or a hang and timeout |
| 4. Listener | Is the program actually listening, on the right address? | ss -ltnp | Nothing listed, or only 127.0.0.1 |
| 5. TLS | Does the secure handshake work? | curl -v, openssl s_client | certificate has expired, unable to get local issuer |
| 6. HTTP | What status and headers come back? | curl -v, curl -i | 404, 502, 503, redirects |
| 7. Time | Where do the milliseconds go? | curl -w, tcpdump | Long gap before first byte |
The order matters, because a step can only work if the earlier ones did. An HTTP 502 tells you that DNS, routing, TCP and even TLS all worked to reach something. A Could not resolve host tells you to stop looking at the app, since no packet was even sent.
We will go through the tools in this order, then collect them in a checklist.
curl: the tool you will use most
curl speaks HTTP, but it also tells you what happened underneath. Three flags matter most: -v (verbose), -w (write out timings and facts) and -o /dev/null (throw away the body so you can see the facts).
curl -v: see the conversation
$ curl -v http://127.0.0.1:8090/fast* Trying 127.0.0.1:8090...* Connected to 127.0.0.1 (127.0.0.1) port 8090> GET /fast HTTP/1.1> Host: 127.0.0.1:8090> User-Agent: curl/8.5.0> Accept: */*>< HTTP/1.1 200 OK< Date: Sat, 17 Oct 2026 13:30:04 GMT< Connection: keep-alive< Keep-Alive: timeout=5< Content-Length: 8<fast ok* Connection #0 to host 127.0.0.1 left intact(I removed the progress meter and the { [8 bytes data] lines to keep it short.) The marks are the key:
- Lines that start with
*are curl's own notes: which address it tried, whether it connected, what TLS version it got. - Lines that start with
>are what curl sent (the request). - Lines that start with
<are what the server sent back (the response).
So in one command you can answer: did DNS work (it printed an address), did TCP connect (Connected to), what exactly did I send (> lines), what status came back (< first line). When you are unsure whether your code sends the right headers, send the same request with curl -v and compare.
In a shell with a proxy set (HTTP_PROXY, HTTPS_PROXY), curl goes through the proxy, and -v shows Uses proxy env variable near the top. My sandbox has that setting, and it also has a no_proxy list that skips the proxy for local addresses, which is why the lab above connected directly. If your laptop at work behaves differently from your server, check env | grep -i proxy first. We saw the same trap in lesson 5.
curl -w: measure where the time goes
The -w flag prints values after the transfer. These are the ones to know:
| Variable | Meaning |
|---|---|
time_namelookup | Seconds until DNS finished |
time_connect | Seconds until the TCP connection was open |
time_appconnect | Seconds until the TLS handshake finished (0 for plain HTTP) |
time_starttransfer | Seconds until the first byte of the response arrived (this is TTFB) |
time_total | Seconds for the whole thing |
http_code | The status code |
remote_ip, remote_port | The address curl really connected to |
Each value counts from the very start, so they only go up. To get the time of one step, subtract the one before it. Here is the fast page and the slow page, with the same command:
$ curl -s -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code} ip=%{remote_ip}:%{remote_port}\n' http://127.0.0.1:8090/slowdns=0.000019 connect=0.000279 tls=0.000000 ttfb=1.502215 total=1.502338 code=200 ip=127.0.0.1:8090$ curl -s -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' http://127.0.0.1:8090/fastdns=0.000018 connect=0.000132 ttfb=0.001004 total=0.001045 code=200Read the first line like a doctor reads a chart. DNS took almost nothing. The connection opened in 0.3 ms. Then nothing happened for about 1.5 seconds, and the first byte arrived at 1.502. The body then took 0.1 ms (total minus ttfb). So the time is inside the server, between "request received" and "first byte sent". The network is not the problem, and it would be a waste to tune DNS or TLS. That one reading saves hours.
Compare this with the patterns you will meet on real systems:
| Which gap is big | Likely meaning |
|---|---|
namelookup is big | Slow or failing DNS resolver, or a long search-domain list (lesson 3) |
connect minus namelookup is big | Far-away server (a few round trips), or SYN packets being lost and retried (lesson 2) |
appconnect minus connect is big | TLS handshake cost: far server, slow CPU, or no connection reuse (lesson 5) |
starttransfer minus appconnect is big | The server is thinking: database, cold start, queue (the backend) |
total minus starttransfer is big | A big body on a slow link, or a slow client |
I used the "real" lab numbers above, but the table is the general rule. Note that on loopback every network number is tiny. Against a real remote server you will see connect close to one round trip time, and appconnect one more (TLS 1.3) or two more (TLS 1.2).
One timing is not a measurement. The first request may include DNS, a cold cache or a cold connection. Run it five or ten times, and look at the spread, not just the best one. A single slow sample can be a one-off.
curl exit codes tell you the layer
When a curl command fails, its exit code is a fast clue. I triggered each of these on purpose:
$ curl -sS http://no-such-host.invalid/curl: (6) Could not resolve host: no-such-host.invalid # exit=6$ curl -sS http://127.0.0.1:8099/ # nothing listens on 8099curl: (7) Failed to connect to 127.0.0.1 port 8099 after 0 ms: Couldn't connect to server # exit=7$ curl -sS --max-time 0.5 http://127.0.0.1:8090/slow # server waits 1.5 scurl: (28) Operation timed out after 501 milliseconds with 0 bytes received # exit=28And these from the earlier TLS lesson: (60) for a certificate problem, (35) for a handshake failure, (56) for a reset while reading. A cheat sheet:
| Exit code | Meaning | Layer to look at |
|---|---|---|
| 6 | Could not resolve host | DNS |
| 7 | Failed to connect | Port closed, host down, or blocked |
| 28 | Timed out | Slow server, or packets dropped |
| 35 | TLS handshake failed | TLS versions, ciphers |
| 56 | Receive failure (connection reset) | The other side closed or crashed mid-reply |
| 60 | Certificate problem | TLS trust, dates, name |
| 22 | HTTP error with -f | HTTP status 400 or above |
The -f flag is handy in scripts. Without it, curl treats a 500 as success (exit 0), because the HTTP conversation itself worked. With it, you get a failing exit code:
$ curl -s -w 'code=%{http_code}\n' http://127.0.0.1:8090/500boomcode=500$ curl -sf http://127.0.0.1:8090/500 ; echo "exit=$?"exit=22Refused versus timed out is the most useful split in all of networking. "Refused" (exit 7, ECONNREFUSED) comes back in microseconds, because the machine is there and a TCP reset says "no program on that port". A timeout (exit 28, or a connect that hangs) means the packets went into a hole: a firewall that drops silently, a wrong IP, a machine that is off, or a route that goes nowhere. Refused means "fix the service or the port". Timeout means "look at the firewall, security group, route and address". I could only reproduce refused in this sandbox. For the hang case I describe what you will see; I did not capture it here.
More curl tricks that save time
- Follow redirects and show where you ended up. Without
-L, curl shows the redirect itself. With it, you can print how many hops there were:
$ curl -sS -o /dev/null -D - http://127.0.0.1:8090/redir | head -3HTTP/1.1 302 Foundlocation: /fastDate: Sat, 17 Oct 2026 13:30:41 GMT$ curl -sL -o /dev/null -w 'code=%{http_code} redirects=%{num_redirects} url=%{url_effective}\n' http://127.0.0.1:8090/redircode=200 redirects=1 url=http://127.0.0.1:8090/fast- Test a name before DNS is changed, with
--resolve. It makes curl use an address you choose for a name, while still sending the rightHostheader and (for HTTPS) the right SNI. This is how you test a new server before you move DNS to it:
$ curl -s --resolve app.lab.test:8090:127.0.0.1 -w 'code=%{http_code} ip=%{remote_ip}\n' http://app.lab.test:8090/fastfast okcode=200 ip=127.0.0.1- Always set timeouts in scripts:
--connect-timeout 3 --max-time 10, or a hung connection can block a deploy for minutes.
dig: ask DNS directly
curl hides DNS behind the system resolver. dig talks to a DNS server directly and shows the full answer, so you can tell which server said what. The lab DNS server runs on port 5354 on my machine, so I add -p 5354 and @127.0.0.1 (the server to ask). In real life you leave out -p and often leave out @server.
$ dig @127.0.0.1 -p 5354 app.lab.test;; Got answer:;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63281;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0;; QUESTION SECTION:;app.lab.test. IN A;; ANSWER SECTION:app.lab.test. 300 IN A 10.1.2.3;; Query time: 0 msec;; SERVER: 127.0.0.1#5354(127.0.0.1) (UDP);; WHEN: Sat Oct 17 13:30:17 UTC 2026;; MSG SIZE rcvd: 46(The ; <<>> DiG ... banner and a few empty lines are trimmed.) What to read here:
status: NOERRORis the result code.NXDOMAINmeans the name does not exist.SERVFAILmeans the server could not get an answer (often a broken upstream or a bad DNSSEC setup).flags:qris "this is a response".aameans an authoritative answer, from the server that owns the name.rdandramean recursion was asked for and is available. A normal answer from your provider's resolver showsrabut noaa, since it is passing along something it cached or fetched.ANSWER SECTION: name, TTL (300 seconds here), classIN, typeA, and the value. The TTL is how long a cache may keep it. If you ask a caching resolver twice, the TTL counts down, which proves you got a cached answer.Query timeandSERVER: how long the lookup took, and which server really answered. This is how you find out whether your machine uses the server you think it does.
A name that does not exist:
$ dig @127.0.0.1 -p 5354 missing.lab.test | grep -E "status|flags";; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 54930;; flags: qr aa rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0And a DNS server that is not there. Notice that dig tells you in plain words:
$ dig @127.0.0.1 -p 5399 +time=1 +tries=1 app.lab.test;; no servers could be reachedThat is a DNS timeout, and it is the DNS twin of a firewall drop: UDP packets went out, and nothing answered. The system resolver would retry and wait, and your app would show a slow first request.
For scripts, +short prints only the value:
$ dig @127.0.0.1 -p 5354 +short api.lab.test10.1.2.4The dig options I use most, in real work:
| Command | What it answers |
|---|---|
dig example.com | What does my default resolver say? |
dig @1.1.1.1 example.com | What does a different resolver say? (Compare with the first: do they disagree?) |
dig +short example.com A / AAAA / MX / TXT / NS | One record type, value only |
dig +trace example.com | Walk from the root servers down, step by step, without using the cache |
dig -x 192.0.2.10 | Reverse lookup: which name belongs to this address? |
dig example.com +noall +answer | Just the answer section, with the TTL |
dig @ns1.example.net example.com | Ask the authoritative server directly, to bypass every cache |
The most useful pair is the last two. If dig @your-authoritative-server name shows the new record but dig name still shows the old one, the answer is cached somewhere, and the TTL tells you how long to wait. If both show the old one, the change was never published. The first case is waiting. The second case is a bug in your DNS config.
I could not run dig +trace or ask real public resolvers, because this sandbox has no working public DNS. The +trace and -x rows are from my knowledge of the tool and the DNS lesson, not from this lab.
Also remember that dig does not use the same path as your program. Your program calls the system resolver, which can read /etc/hosts, apply search domains from /etc/resolv.conf, use a local cache, and apply rules like nsswitch. So "dig works but my app cannot resolve" is a real thing. When it happens, compare with getent hosts name (which uses the system path), or with curl -v, which does what your app does.
ss: who is listening, who is connected
ss (socket statistics) lists the sockets on the machine. It replaced the old netstat. Two questions cover most uses: "is anything listening on this port?" and "what connections exist?"
$ ss -ltnp | grep 8090LISTEN 0 511 127.0.0.1:8090 0.0.0.0:* users:(("node",pid=849,fd=21))The flags: -l listening sockets only, -t TCP, -n show numbers (do not resolve names, which is faster and clearer), -p show the process (you may need sudo for other users' processes). The columns:
LISTENis the state.127.0.0.1:8090is the local address and port.- The address is the thing people miss.
127.0.0.1:8090means only this machine can connect.0.0.0.0:8090or*:8090means every interface.[::]:8090is the same in IPv6. If a container service works inside the container but not from outside, check this first. A server bound to127.0.0.1inside a container is invisible from outside it. We saw the same fix in lesson 0. users:(("node",pid=849,...))names the program that owns the socket. This answers "what is using port 8090?" when you getEADDRINUSE.
Show the established connections for one port, with extra TCP details (-i) and timers (-o):
$ ss -tnoi state established dport = :8090Recv-Q Send-Q Local Address:Port Peer Address:Port0 0 127.0.0.1:52176 127.0.0.1:8090 ts sack bbr wscale:10,10 rto:204 rtt:0.186/0.093 mss:32741 pmtu:65535 cwnd:10 bytes_acked:1 segs_out:2 segs_in:1 ... minrtt:0.186(Shortened; the real line has more fields.) The interesting part is the second line, which comes straight from the kernel's TCP state: rtt:0.186/0.093 is the smoothed round-trip time and its variation in milliseconds, cwnd:10 is the congestion window (lesson 2), rto:204 is the retransmit timeout. On a real remote connection, a large rtt, a growing retrans field or a small cwnd tells you the path itself is the problem, with no need for a packet capture.
Also look at the summary:
$ ss -s | head -3Total: 91TCP: 517 (estab 63, closed 446, orphaned 0, timewait 446)This sandbox shows 446 sockets in TIME_WAIT, because my lab made many short connections. TIME_WAIT is normal. It is the side that closed first waiting a short time to be safe (lesson 2). It only becomes a worry when a client makes thousands of new connections per second to the same address and runs out of local ports. The cure is connection reuse (keep-alive), not a kernel tweak.
Three states are worth recognising by name:
| State | Meaning | If you see lots of them |
|---|---|---|
ESTABLISHED | A normal open connection | Fine, if the number matches your load |
TIME_WAIT | We closed it and wait before the port is reused | Many short connections. Reuse them. |
CLOSE_WAIT | The other side closed, our program has not | A bug in your code: it forgets to close sockets (a leak) |
SYN-RECV / SYN-SENT | Handshake half-done | Many SYN-SENT out: the far side is not answering. Many SYN-RECV in: a flood, or a slow accept queue |
CLOSE_WAIT is the one that points at your code. The kernel is waiting for your program to call close(). If the count grows without stopping, you have a leak, and the process will eventually run out of file descriptors. I describe this from the TCP state machine; I did not build a leak in this lab.
For old habits: netstat -tlnp shows nearly the same as ss -ltnp. On macOS, ss does not exist; use lsof -iTCP -sTCP:LISTEN -n -P.
Reach checks: ping, nc and traceroute
Three small tools check the middle layers.
nc -zv host port (netcat) tries to open a TCP connection and says whether it worked. It is the cleanest "is the port open?" test, because it involves no HTTP and no TLS. -z means "just scan, send nothing", -v means "talk to me".
$ nc -zv 127.0.0.1 8090Connection to 127.0.0.1 8090 port [tcp/*] succeeded!$ nc -zv 127.0.0.1 8099 ; echo exit=$?nc: connect to 127.0.0.1 port 8099 (tcp) failed: Connection refusedexit=1A connect that hangs for a long time and then fails with a timeout would tell you the packets are being dropped. With nc you can limit the wait: nc -zv -w 3 host port.
ping host sends ICMP echo packets and times the reply (lesson 0). If ping works, the route and the machine are fine. If ping fails, it proves nothing by itself: many networks and cloud security groups block ICMP while TCP works fine. So "ping fails" is not a diagnosis. "The TCP port test fails" is.
traceroute -n host shows the routers on the way, using the TTL trick from lesson 0. The -n flag skips reverse lookups, which makes it much faster.
$ traceroute -n -m 2 127.0.0.1traceroute to 127.0.0.1 (127.0.0.1), 2 hops max, 60 byte packets 1 127.0.0.1 0.351 ms 0.298 ms 0.260 msOn loopback it is one boring hop. On a real path, read it like this: the hop where the time jumps and stays high is where the long link is (an ocean, for example). A row of * * * means that router sent no reply, which is often by design and does not mean the path is broken. If the trace stops and never reaches the target, but the later hops stay silent for everything, the packet may be dropped at the last visible hop. mtr combines ping and traceroute in a live view and is better for finding loss on a hop; it is not installed in this sandbox, so I did not run it.
tcpdump: look at the real packets
When the higher-level tools disagree with each other, or when you need proof, you read the packets. tcpdump is installed on nearly every Linux box and shows each packet as one line. It needs root (sudo).
A good first command has four parts:
sudo tcpdump -i lo -nn -c 12 'tcp port 8090'-i lois the interface (lois loopback; on a real machine it might beeth0orens5;-i anycaptures on all of them).-nnmeans show numbers for addresses and ports instead of looking up names. Name lookups slow the output and can create DNS traffic of their own.-c 12stops after 12 packets, so it cannot run forever.'tcp port 8090'is the filter, in BPF syntax. Without a filter on a busy server you get a flood.
Here is what it showed while curl made one request (this is a real capture; timestamps cut to the second part). Read the Flags column:
13:30:18.717230 IP 127.0.0.1.37700 > 127.0.0.1.8090: Flags [S], seq 3234883332, win 65495, options [mss 65495,sackOK,TS val 3633403536 ecr 0,nop,wscale 10], length 013:30:18.717254 IP 127.0.0.1.8090 > 127.0.0.1.37700: Flags [S.], seq 3572550400, ack 3234883333, win 65483, options [...], length 013:30:18.717270 IP 127.0.0.1.37700 > 127.0.0.1.8090: Flags [.], ack 1, win 64, length 013:30:18.717325 IP 127.0.0.1.37700 > 127.0.0.1.8090: Flags [P.], seq 1:82, ack 1, win 64, length 8113:30:18.717329 IP 127.0.0.1.8090 > 127.0.0.1.37700: Flags [.], ack 82, win 64, length 013:30:18.719153 IP 127.0.0.1.8090 > 127.0.0.1.37700: Flags [P.], seq 1:131, ack 82, win 64, length 13013:30:18.719187 IP 127.0.0.1.37700 > 127.0.0.1.8090: Flags [.], ack 131, win 64, length 013:30:18.719403 IP 127.0.0.1.37700 > 127.0.0.1.8090: Flags [F.], seq 82, ack 131, win 64, length 013:30:18.719637 IP 127.0.0.1.8090 > 127.0.0.1.37700: Flags [F.], seq 131, ack 83, win 64, length 013:30:18.719665 IP 127.0.0.1.37700 > 127.0.0.1.8090: Flags [.], ack 132, win 64, length 0The flags, in order: [S] is SYN (the client asks to open), [S.] is SYN-ACK (the server agrees), [.] is a plain ACK, so the first three lines are the three-way handshake from lesson 2. [P.] means PUSH plus ACK: data. The first data packet has length 81, and that is the HTTP request. The server's length 130 data packet is the response. Then [F.] (FIN) packets close the connection from both sides. The whole exchange took about 2.4 ms on loopback.
The common things you can prove from a capture:
| You see | It means |
|---|---|
[S] repeated, no [S.] back | SYNs are lost. A firewall drop, a wrong IP, or the server is down. This is the "timeout" case. |
[S] then [R.] (RST) | Connection refused. Nothing listens on that port. |
[S.] comes back, but the app is slow | The network is fine. The delay is in the app. |
The same seq appears again | A retransmission. The first copy was lost or its ACK was. Many of them mean a lossy path. |
[R] in the middle of a conversation | One side aborted: a crash, a proxy idle timeout, or a firewall resetting a long-idle flow. |
win 0 | The receiver's buffer is full. It is the slow side, not the network. |
To read HTTP in plain text, add -A (print the payload as ASCII). On unencrypted HTTP you can read the request and the response straight from the packets:
$ sudo tcpdump -i lo -nn -A -s0 'tcp port 8090 and tcp[tcpflags] & tcp-push != 0'13:30:28.865482 IP 127.0.0.1.52186 > 127.0.0.1.8090: Flags [P.], ... length 81...GET /fast HTTP/1.1Host: 127.0.0.1:8090User-Agent: curl/8.5.0Accept: */*13:30:28.866565 IP 127.0.0.1.8090 > 127.0.0.1.52186: Flags [P.], ... length 130...HTTP/1.1 200 OKDate: Sat, 17 Oct 2026 13:30:28 GMTConnection: keep-aliveKeep-Alive: timeout=5Content-Length: 8fast ok(Binary bytes before each text block are trimmed.) The filter tcp[tcpflags] & tcp-push != 0 keeps only packets with the PUSH flag, which are the ones that carry data, so the handshake noise is gone. This is a great way to see the actual bytes when "my client says it sends X but the server says it got Y". With HTTPS you will see only scrambled bytes (lesson 5), so for HTTPS debug at the application level, or use a proxy tool, or the SSLKEYLOGFILE feature described below.
DNS is on UDP and is just as easy to watch:
$ sudo tcpdump -i lo -nn -c 4 'udp port 5354'13:30:32.883603 IP 127.0.0.1.57263 > 127.0.0.1.5354: UDP, length 5313:30:32.883821 IP 127.0.0.1.5354 > 127.0.0.1.57263: UDP, length 46One small packet out (53 bytes: the question), one back (46 bytes: the answer). If you see the question leave and nothing return, the DNS server is not answering, and you have found your delay. (tcpdump 4.99 can also decode DNS names with -v; I did not use that here.)
Save to a file, read it later
On a server you rarely want to stare at live output. Write to a file with -w, then read it with -r on any machine, including in Wireshark:
$ sudo tcpdump -i lo -nn -c 12 -w cap.pcap 'tcp port 8090'$ tcpdump -nn -r cap.pcap | headreading from file cap.pcap, link-type EN10MB (Ethernet), snapshot length 26214413:30:18.717230 IP 127.0.0.1.37700 > 127.0.0.1.8090: Flags [S], ...You can even filter the saved file again. I counted the SYN packets in my file with tcpdump -nn -r cap.pcap 'tcp[tcpflags] & tcp-syn != 0' | wc -l, and got 2 (one SYN and one SYN-ACK, both have the SYN bit set). That was one connection, as expected.
Packet captures can contain passwords, tokens, cookies and personal data (anything that is not encrypted, and sometimes even keys). Capture only what you need, with a narrow filter and a packet count, keep the file private, and delete it afterwards. On a production machine, also remember that capturing costs CPU and disk: use -c or -G/-W to bound it, and a filter to keep it small. And check your company policy first.
Wireshark and tshark: the basics
Wireshark opens a .pcap file and decodes every layer: Ethernet, IP, TCP, then TLS, HTTP or DNS. Three features give most of the value, and none of them need deep knowledge:
- Display filters (not the same language as tcpdump's capture filters). Examples:
ip.addr == 10.0.0.5,tcp.port == 8090,http.request,dns.qry.name == "app.lab.test",tcp.analysis.retransmission,tls.handshake.type == 1(client hellos),tcp.flags.reset == 1. - Follow TCP Stream (right-click a packet). It glues the data of one connection back together and shows the whole conversation as text, like a transcript. For plain HTTP that is the request and response.
- Expert Information (menu Analyze). Wireshark flags the things it found by itself: retransmissions, duplicate ACKs, zero windows, resets. It is like having a junior engineer read your capture first.
Also useful: Statistics, then Conversations, to see which connections moved the most bytes, and Statistics, then I/O Graph, to see bursts and stalls on a timeline.
The command-line twin, tshark, does the same on servers without a screen (for example tshark -r cap.pcap -Y 'http.request' or -Y 'tcp.analysis.retransmission'). I could not run tshark here (not installed), so check tshark --help on your system. For your own HTTPS traffic, browsers, curl and Node can write TLS session secrets to a file named by SSLKEYLOGFILE, and Wireshark can read it to show the decrypted HTTP. I did not run this either.
Checking TLS from outside: openssl s_client
For TLS, one command from lesson 5 is your first stop:
echo | openssl s_client -connect host:443 -servername hostIt prints the certificate chain, the protocol, the cipher, and Verify return code. Use -servername so that SNI is sent. Common reading: Verify return code: 0 (ok) is good, 10 (certificate has expired), 18 or 19 (self-signed), 20 or 21 (cannot get the issuer, often a missing intermediate). Check the dates directly with openssl x509 -noout -dates. And for monitoring, openssl x509 -checkend returns exit code 1 if a certificate expires within the given seconds. The lab output for these is in lesson 5, so I will not repeat it.
Common failures and what they mean
Here is a field guide that links what you see to a cause. The first column is the symptom as the user or log shows it.
| Symptom | Layer | Likely causes | First check |
|---|---|---|---|
Could not resolve host, ENOTFOUND, EAI_AGAIN | DNS | Typo, record missing, resolver down, wrong search domain, no network in the container | dig name, cat /etc/resolv.conf |
Network is unreachable | Route | No default route, interface down, IPv6 address but no IPv6 route | ip route, ip addr |
Connection refused (ECONNREFUSED) | TCP | Service is not running, wrong port, listening only on 127.0.0.1 | ss -ltnp, nc -zv |
Hang, then timed out (ETIMEDOUT) | TCP or route | Firewall or security group drops packets, wrong IP, host down | tcpdump for repeated SYN, check the firewall rules |
Connection reset by peer (ECONNRESET) | TCP | Server crashed or closed, proxy idle timeout, firewall reset, TLS refused | tcpdump to see who sent the RST |
socket hang up, EPIPE (Node) | TCP | Writing to a connection the other side already closed. Often a reused keep-alive connection that the server had closed | Align keep-alive timeouts (client shorter than server) |
certificate has expired, unable to verify | TLS | Expired certificate, missing intermediate, unknown CA, wrong name | openssl s_client, date on the client |
502 Bad Gateway | HTTP at a proxy | The proxy reached nothing useful behind it: backend down or sent garbage | The backend's ss and logs, then proxy logs |
503 Service Unavailable | HTTP | No healthy backend, overloaded, deploying | The load balancer's health checks |
504 Gateway Timeout | HTTP at a proxy | Backend took longer than the proxy's timeout | curl -w directly to the backend: is TTFB slow? |
| Works for some users, fails for others | Mixed | One bad backend behind the balancer, a stale DNS cache, IPv6 versus IPv4, a CDN region, MTU | Test each backend address separately with --resolve |
| Slow only the first request | DNS, TCP, TLS | Cold connection, cold cache, cold start of the app | curl -w timings on the first versus second call |
| Works on Wi-Fi, fails on VPN | Route or MTU | VPN lowers MTU, big packets lost, DNS differs | Try ping -M do -s 1400 host for path MTU (Linux) |
The 502 versus 504 split deserves one more sentence. Both come from a proxy or load balancer (lesson 6). A 502 means the backend answered badly or closed the connection. A 504 means the backend did not answer in time. They point in different directions: 502 means look for crashes and protocol mismatches, 504 means look for slowness. And the key test for both is to bypass the proxy and call the backend directly with curl -w.
The Node-specific error ECONNRESET on a reused connection is worth a second look. HTTP keep-alive (lesson 4) holds a connection open. If the server's idle timeout is shorter than the client's, the server closes the connection while the client thinks it is still good. The next request on it fails with a reset. The fix is to make the client's idle timeout shorter than the server's (and the load balancer's), or to retry safe requests once.
A troubleshooting checklist, layer by layer
Print this, or keep it in your notes. It assumes you are on the client, and that "the service" is https://api.example.com/health.
1. Name (DNS).
dig +short api.example.comreturns an address? If not, DNS is the problem.- Compare resolvers:
dig @1.1.1.1 +short api.example.com. If they differ, a cache or a split-horizon setup is involved. - Check TTLs with
dig api.example.com +noall +answer. - If dig works but the app fails, test the app's path:
getent hosts api.example.com, and look at/etc/resolv.confand/etc/hosts.
2. Address and route (IP).
- Does the client have an address and a default route?
ip -br addr,ip route. - Is the destination private or public, and is the client on a network that can reach it? (Lesson 1: CIDR, NAT.)
traceroute -n hostto see where the path ends. Remember that blocked ICMP is common, so ping alone proves little.- Mixed IPv4 and IPv6:
curl -4andcurl -6to try each.
3. Port (TCP).
nc -zv -w 3 host 443. Success means TCP works end to end. Refused means the server machine answers, but nothing listens (or a rule sends a reset). A timeout means silence: look at security groups, firewalls, network ACLs and routes.- On the server:
ss -ltnp | grep :443. Is the program listening, and on which address? Is it127.0.0.1only? - On the server: is the machine's own firewall blocking? (
iptables -S,nft list ruleset,ufw status, depending on the system.) - Cloud: is the security group open for this source and port? Are the network ACLs open both ways? (An ACL is stateless: the reply direction needs its own rule.)
4. Secure channel (TLS).
echo | openssl s_client -connect host:443 -servername host: look atVerify return code, the chain, the dates and the protocol.- Compare the client's clock:
date -u. - Names in the SAN list match what the client uses? Intermediate included?
- A corporate proxy that re-signs certificates? Check the issuer you actually receive.
5. Request and response (HTTP).
curl -v https://api.example.com/health: status, redirects, headers (Location,Content-Type,Cache-Control,Via,Server).- Same request, different layer: add
-H 'Host: ...'or use--resolveto hit each backend address directly. - Is a redirect looping?
curl -sL -o /dev/null -w '%{num_redirects}\n' --max-redirs 10 url. - Is the answer cached by a CDN? Look for
Age,X-Cachestyle headers (the names depend on the provider).
6. Time.
curl -wwith the timing variables, 5 to 10 runs. Which gap is large?- If the gap is before the first byte and the network numbers are small: go to the server (logs, traces, database). The Scenario Debugging series shows how to trace a slow backend.
- If
connectis large: distance or loss.ss -tiforretrans, or tcpdump for repeated sequences.
7. Capture the packets if two tools disagree. tcpdump -i any -nn -w cap.pcap 'host X and port Y' on the client, and on the server, if you can. Compare: did the SYN leave the client? Did it arrive at the server? Did the reply leave? Where does the packet disappear? Captures at both ends tell you which side of a firewall to blame.
Do not conclude "the network is the problem" without evidence, and do not conclude "the network is fine" because ping works. Use the table in this lesson: pick the layer from the error, test it with the right tool, and keep the output.
Try it
Use any machine with curl, dig, ss, nc and (with sudo) tcpdump. If you want my exact lab, a 10-line Node server with a /slow route that waits 1.5 seconds is enough.
- Start any local HTTP server. Run
ss -ltnpand find it. What address does it listen on? - Run
curl -s -o /dev/null -w 'connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n' http://127.0.0.1:PORT/for a normal route and for a slow one. Which number differs, and what does that tell you about where the delay lives? - Run
curl -sS http://127.0.0.1:59999/(a port where nothing listens). What exit code and message do you get? Is it a refusal or a timeout? How can you tell from the time it took? - Run
curl -sS http://does-not-exist.invalid/. What is the exit code? Which layer does it name? - In one terminal run
sudo tcpdump -i lo -nn -c 12 'tcp port PORT'. In another, run onecurlrequest. Match each packet to the handshake, the request, the response and the close. - A user says "the site is down". Using the table in this lesson, write down the first five commands you would run, in order, and what each result would mean.
Answers
- If you bound to
127.0.0.1you see127.0.0.1:PORT. If you bound to0.0.0.0you see0.0.0.0:PORTor*:PORT. Only the second one is reachable from other machines. time_connectis nearly the same (tiny).time_starttransfer(TTFB) andtotaljump on the slow route. The network and TCP are fine. The delay is in the server's handling of the request.- Exit code 7,
Failed to connect ... Couldn't connect to server. It is a refusal. It returned immediately (I sawafter 0 ms). A timeout would have taken seconds and given exit code 28 or a longconnectwait. - Exit code 6,
Could not resolve host. It is DNS. No packet to the target was ever sent. (.invalidis a reserved name that never resolves.) - Expect
[S],[S.],[.](handshake), then[P.]with length around 80 (request),[.]ack,[P.]with length around 130 (response), ack, and[F.]packets (close). Exact lengths depend on the headers. - A good answer: (1)
dig +short name(DNS), (2)nc -zv -w 3 host 443(route and port), (3)echo | openssl s_client -connect host:443 -servername host(TLS), (4)curl -v https://name/(HTTP status), (5)curl -wtimings plusss -ltnpon the server. Each result names the layer where to dig deeper.
Interview questions
Networking questions come in two forms: "explain how X works" and "something is broken, what do you do?". The answers below are written the way I would say them. The parts I actually tested are marked in the text. Where an answer depends on tools or numbers I did not run, I say so.
What happens when you type a URL in the browser and press Enter? (Must know)
First the browser checks its cache and the URL. It needs an address, so it asks DNS: the browser cache, then the operating system, then a recursive resolver, which walks root, top-level and authoritative servers if it has nothing cached. With an address, the browser opens a TCP connection (three-way handshake). For https it then runs a TLS handshake: the client hello with SNI, the server certificate, key agreement, and the client checks the chain, dates and name. Next it sends an HTTP request over that connection (HTTP/2 or HTTP/3 if both sides support it). A CDN, load balancer or reverse proxy may answer or forward it to a backend, which may query a database. The response comes back, and the browser parses the HTML, fetches more resources (often reusing the same connection) and renders. I would add that each step can be timed separately, and curl -w gives me DNS, connect, TLS and first-byte times, which is how I find which step is slow.
What is the difference between TCP and UDP, and when would you pick each? (Must know)
TCP gives a reliable ordered stream: a handshake first, then numbered bytes, acknowledgements, retransmission of lost data, and flow and congestion control. UDP just sends independent datagrams with no connection, no order and no retry, so it has less delay and less overhead. I choose TCP when I need every byte in order (web, APIs, databases, file transfer). I choose UDP when late data is useless and I would rather skip it (live voice and video, games), when the messages are tiny and a retry is simple (DNS), or when I want to build my own rules on top, which is what QUIC (HTTP/3) does. UDP is not "faster" by magic. It is faster because it does less, and the application must handle loss itself if it cares.
A service says "connection refused". Another says "connection timed out". What is the difference and what do you check for each? (Must know)
Refused is an active answer: the target machine got my SYN and replied with a reset, meaning no program is listening on that port (or a firewall rule sends a reset). It comes back instantly. I saw that with curl: exit code 7 after 0 ms. For that I check on the server with ss -ltnp whether the program runs, on which port, and whether it is bound to 127.0.0.1 only. A timeout means silence: my SYN got no answer at all, so a firewall or security group is dropping it, or the IP is wrong, or the host is down, or the route is broken. For that I check security groups, network ACLs, host firewalls, routes and the address itself, and I run tcpdump on the server to see whether the SYN arrives. I could not produce a real silent drop in my sandbox, so I describe that half from how TCP works, but the refused half I ran.
How would you find out why an API call is slow? (Must know)
I split the time into steps with curl -w: DNS, connect, TLS, time to first byte, total. I run it several times, because one sample can mislead. If DNS is large, I look at resolvers and search domains. If connect or TLS is large, it is distance, packet loss or no connection reuse. If everything is small until the first byte, the network is fine and the time is inside the backend, so I move to logs, traces and the database. In my lab the slow route showed connect=0.000279 and ttfb=1.502215, so the whole 1.5 seconds sat inside the server. If the first byte is fast but the total is large, the body is big or the link is slow. This approach avoids tuning the wrong layer.
What does curl -v show you, and what are the > and < lines? (Must know)
It shows the conversation. Lines starting with * are curl's own information, such as which address it tried, whether it connected, and TLS details. Lines starting with > are what curl sent, the request line and headers. Lines starting with < are what the server sent back, the status line and headers. So I can check the resolved address, the Host header, the exact status, the redirect Location, and the response headers like Content-Type and Cache-Control. When my code fails and curl works, I compare the > lines with what my code sends.
What do these states mean in ss: LISTEN, ESTABLISHED, TIMEWAIT, CLOSEWAIT? Which one signals a bug in your code? (Senior signal)
LISTEN is a server socket waiting for connections. ESTABLISHED is an open connection. TIME_WAIT is the side that closed first waiting a short time so late packets from the old connection cannot mix with a new one. It is normal; my own lab ended with hundreds of them after many short requests. It becomes a problem only with a huge rate of new connections, and the fix is connection reuse. CLOSE_WAIT means the other side has closed and my program has not yet called close. A growing number of those is a leak in my code, and it ends in running out of file descriptors. I describe CLOSE_WAIT from how the state machine works; I did not build a leak in the lab.
localhost:3000 works on my machine but a colleague cannot connect to my IP on port 3000. Why? (Trap)
The server is probably bound to 127.0.0.1, which only accepts connections from the same machine. ss -ltnp shows 127.0.0.1:3000 instead of 0.0.0.0:3000. The fix is to bind to 0.0.0.0 (or the right interface) in the app. If it is already on 0.0.0.0, the next suspects are my machine's firewall, the office network isolating clients, or my colleague using the wrong IP (a private address from a different network). In Docker it is the same trap: a server inside a container bound to 127.0.0.1 cannot be reached through a published port.
dig works, but my application cannot resolve the name. What could be going on? (Trap)
dig asks a DNS server directly. My app calls the operating system resolver, which also reads /etc/hosts, applies the search domains in /etc/resolv.conf, may use a local cache like systemd-resolved, and follows nsswitch.conf. So the paths differ. Another cause is a container or runtime with a different resolv.conf than the host, or a language runtime with its own cache. I test with getent hosts name, which uses the system path, and curl -v to see what an HTTP client does. I also check whether the app gets IPv6 results (AAAA) it cannot use. In Node, ENOTFOUND and EAI_AGAIN come from the system resolver call, not from dig's path.
How do you read a tcpdump capture to tell whether the problem is the network or the application? (Senior signal)
I look at the timing and the flags. If SYN packets repeat with no SYN-ACK, the network or a firewall drops them. A SYN followed by a reset means nothing listens. If the handshake completes quickly, the request is sent and the ACK for it returns quickly, but the response data starts only a second later, the network is fine and the server is slow: the delay sits between the server's ACK and its first data packet. If the same sequence number appears more than once, that is a retransmission, so there is loss. A window of 0 means the receiver's buffer is full. A reset in the middle of a conversation shows who closed it by the direction of the RST. In my capture of one request, the handshake, the 81-byte request, the 130-byte response and the close fit in about 2.4 ms, which is the shape of a healthy connection.
A request through the load balancer returns 502 sometimes, but calling each backend directly works. What would you check? (Senior signal)
A 502 means the proxy could not get a valid response from a backend. Since direct calls work, I suspect the connection between proxy and backend. First, idle keep-alive timeouts: if the backend closes idle connections earlier than the proxy expects, the proxy sends a request on a connection that is just closed and gets a reset. The fix is to make the backend's idle timeout longer than the proxy's. Second, a backend that crashes or restarts under some requests, which I see in its logs. Third, health checks that mark a backend healthy before the app is ready. Fourth, a protocol mismatch, such as the proxy using HTTP/1.1 to a backend that expects something else, or oversized headers. I check the proxy's error log, which usually states the upstream error, and I run a capture between proxy and backend to see who sent the reset.
How do you debug a TLS error like unable to get local issuer certificate? (Must know)
It says the client could not build a chain from the server certificate to a root it trusts. The usual causes are that the server does not send its intermediate certificate, or that the issuing CA is private and not in the client's trust store. I run openssl s_client -connect host:443 -servername host to see the chain the server sends and the Verify return code. If only the leaf is sent, I fix the server to send the full chain. If it is a private CA, I add that CA to the client's trust (--cacert for curl, NODE_EXTRA_CA_CERTS for Node). I do not turn verification off. I tested these cases in the TLS lesson with a private CA, and a missing intermediate failed in Node while a full chain worked.
What is the difference between a 502, 503 and 504? (Must know)
All three usually come from a proxy or load balancer. A 502 Bad Gateway means the proxy got an invalid response or the connection broke while talking to the backend. A 503 Service Unavailable means the service cannot take the request now: no healthy backend, overloaded, or in maintenance. A 504 Gateway Timeout means the proxy waited for the backend and gave up. So 502 points at crashes and protocol problems, 503 points at capacity and health checks, and 504 points at slowness or a proxy timeout that is too short. In all three cases I test the backend directly with curl -w to see if it is the proxy or the backend.
Why can ping fail while the website works, and why can ping work while the website is down? (Trap)
Ping uses ICMP, a different protocol from TCP, and many firewalls and cloud security groups block ICMP while allowing web ports. So a failed ping proves almost nothing. The other way round, ping only shows that the machine is alive and reachable at the IP layer. The web server process may have crashed, the port may be closed, or TLS may be failing. The right test for a web service is a TCP connect to its port (nc -zv) and then an actual HTTP request (curl -v). I never accept "ping works" as proof that the service is up.
Your app makes many outbound HTTPS calls and sometimes fails with ECONNRESET or socket hang up. What do you suspect? (Senior signal)
I suspect reused keep-alive connections. HTTP keep-alive leaves a connection open, and the server (or a load balancer or NAT device in the middle) may close it after being idle for a while. My client does not know and writes a request on a connection that is already dead, and gets a reset. I would check the idle timeouts of all hops: server, load balancer, firewall or NAT. The client should drop idle connections before any of them do (a shorter idle timeout), and safe requests like GET can be retried once. I also check whether NAT table entries expire during long idle periods (lesson 1), which can silently break long-lived connections. If I need proof, a capture shows a reset arriving right after the request on an old connection.
Key takeaways
- Debug one layer at a time, from the bottom up: name, route, port, listener, TLS, HTTP, time. The error message usually names the layer.
- Refused (immediate reset) means a machine answered and nothing listens. Timed out means silence, which usually means a firewall, a wrong address or a dead host. Telling these apart is the most useful first step.
curl -vshows what was sent (>) and received (<).curl -wwithtime_namelookup,time_connect,time_appconnect,time_starttransferandtime_totaltells you which step owns the delay. Run it several times.- curl exit codes name the layer: 6 DNS, 7 connect, 28 timeout, 35 and 60 TLS, 22 HTTP error with
-f. digshows the status (NOERROR,NXDOMAIN,SERVFAIL), the TTL and which server answered. Ask the authoritative server directly to bypass caches. dig is not the same path as your app.ss -ltnpshows what listens and on which address (127.0.0.1versus0.0.0.0).ss -tishows RTT and retransmissions. A growingCLOSE_WAITcount is a bug in your code.nc -zvis the cleanest port test. Ping failing proves little, because ICMP is often blocked.tcpdumpwith a narrow filter shows the handshake, the data and the closing, and proves where a packet disappears. Capture at both ends when tools disagree. Treat captures as sensitive data.- Wireshark and tshark add decoding, display filters, Follow TCP Stream and expert warnings.
SSLKEYLOGFILElets you read your own HTTPS traffic without weakening security. - 502 means a bad answer from the backend, 503 means unavailable, 504 means the backend was too slow. Bypass the proxy and test the backend directly.
What's next
You have finished the networking series. If you read all nine lessons, you can now explain the path of a request from the first DNS lookup to the last byte, and you have the tools to prove where it went wrong. The best next step is to use these skills in the area you work in:
- System Design takes the building blocks from lessons 6 and 7 (load balancers, caches, CDNs, long-lived connections) and shows how they fit into large systems that must scale, stay up and stay fast. It is the natural follow-up for interviews.
- DevOps is where you apply this to real infrastructure: servers, virtual networks and subnets, security groups, DNS records and deployment. The networking lesson in that series covers the cloud side of the same ideas.
- Linux and the Shell teaches the command line that all of the tools in this lesson run in: files, processes, pipes and scripts, so you can chain
curl,digandssinto your own checks. - Scenario Debugging practises the "measure, isolate, fix, check" method on slow-API and slow-UI cases, and picks up exactly where the
curl -wtimings leave off. - Docker and Kubernetes shows container networking, where "bound to
127.0.0.1" and published ports cause the classic problems from this lesson. - Security covers the application side of what we built here, such as secrets, headers and attacks that go through the network.
To practise right now, take any service you run or use and write the first five commands you would run if it failed, in order, with what each result would tell you (Try it, number 6). Then break something on purpose, such as a wrong port, a wrong name or a stopped server, and check that your commands name the right layer. Going back one step, lesson 7 covers WebSockets and server-sent events, which are the long-lived connections that make several of the failure modes above (idle timeouts, resets, proxy buffering) show up more often.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- What Happens When You Type a URL (The Full Version) · The Browser Contract
- Volumes, Networks and Running Containers Safely · Docker & Kubernetes
- Services, Ingress and Networking in Kubernetes · Docker & Kubernetes
Part of the study guide
Related series