Pointing at the Slow Segment With curl -w
In one line
The timing fields of curl -w break one request into five segments: name resolution / connect / TLS / first byte / total. Turning a report of "it's slow" into a segment name is the start of diagnosis.
Why this exists
"The API is slow" is a sentence with almost no information. Slow name resolution, a slow TLS handshake, and a slow backend query are completely different problems, and the teams responsible differ too. Yet to the user's eyes these three all look the same, "slow".
How it works
One line does it.
curl -o /dev/null -s -w \
'dns=%{time_namelookup} conn=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' \
https://api.example.com/health
Each field is the cumulative time from the start of the request. So you get the time per segment by subtraction.
| Segment | Calculation | Where to suspect if long |
|---|---|---|
| Name resolution | time_namelookup |
Resolver, search domains, ndots |
| TCP connect | time_connect - time_namelookup |
Firewall, distance, SYN retransmission |
| TLS | time_appconnect - time_connect |
Certificate chain, OCSP lookup, protocol negotiation |
| Server processing | time_starttransfer - time_appconnect |
Application, DB |
| Body transfer | time_total - time_starttransfer |
Bandwidth, response size |
How to read it: if connect stretches to the timeout, the firewall; if only appconnect is long, TLS; if appconnect is fast and only ttfb is long, the application or DB.
The remaining options that diagnosis really needs.
curl -v https://api.example.com/health # 요청/응답 헤더 전체
curl --resolve api.example.com:443:10.0.1.50 https://api.example.com/health
curl -H 'Host: api.example.com' http://10.0.1.10:8080/health
curl -sD - -o /dev/null https://api.example.com/ # 헤더만
curl -L --max-redirs 10 -v https://example.com 2>&1 | grep '< [Ll]ocation'
--resolve makes a particular name go to a particular IP without touching DNS. It is essential when testing a new server by its real name before deployment. -H 'Host:' is a way to connect directly by IP while still passing through virtual host routing as originally. When you knock directly on a backend behind an LB, if you do not match Host, the comparison itself becomes meaningless.
-I sends a HEAD request. If the server handles HEAD differently or blocks it altogether, the result differs from GET, so when in doubt it is safer to GET and look at only the headers with -sD - -o /dev/null.
Frequent misdiagnoses
Concluding 502 means "the upstream is dead". 502 means the proxy received an invalid response from the upstream. If the response headers are bigger than the proxy buffer, you get a 502 even though the upstream is fine. And increasing the timeout for a 502 is ineffective — that is the prescription for a 504.
Passing a certificate by looking only at the expiry date. Browsers cache intermediate certificates, so it looks normal on a developer PC. Server-to-server calls have no cache and fail. Confirm by counting the number of certificates in the chain.
echo | openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE'
Looking for a redirect loop in the application. Usually it is a situation where the TLS termination point and the application's HTTPS enforcement do not know about each other. You have to pass X-Forwarded-Proto and overwrite it at the boundary.
Let us also know the redirect codes precisely. 302 may change POST to GET, 303 forces GET, and 307/308 preserve the method. 308 is right for enforcing a protocol, and 303 for switching the screen after a form submission.
What it looks like in the field
"curl works but only the browser fails." In this case you have to look at the browser's own rules, not the server. CORS preflight, SameSite cookies, mixed content blocking, HSTS, and HTTP/2 connection coalescing (421) are the candidates. curl applies none of these rules.
What you will do in the next lab
You start a diagnostic HTTP server and pull out status codes, headers, redirects, virtual hosts, and timing in turn. The highlight is the step where you make a connection by name without DNS using --resolve.