A customer says the API “feels slow”. You open Postman, hit the endpoint again, and it’s… fine. 340ms, no problem. The worst answer in performance work is “it was fine when I tested it” — it kills the conversation without moving it forward, and it’s usually wrong, because the slowness was never on your desk.
This post is the first ten minutes of that investigation, in the terminal. No dashboards, no agents, no purchase order. One command, and then a decision about which layer is lying to you.
You’ll need: a terminal, curl, and an endpoint you can hit repeatedly. That’s it.
Step 1 — get numbers you can’t argue with
Before you theorize, capture the timing of an actual request. curl has a mode built exactly for this — -w, the write-out format, which prints whichever timing variables you ask for after the request finishes:1
Four numbers, one request, zero dashboards. (These are from a demo run — yours will differ, and that’s the point: now they’re yours.)
Here’s the mental model that makes them useful: these timers are checkpoints in a relay race, not four separate measurements. Each one is cumulative from the start of the request:
time_namelookup— how long the domain name took to resolve.time_connect— everything up to the TCP handshake completing.time_starttransfer— everything up to the first byte of the response arriving.time_total— the whole thing, response body included.
Here’s the race, laid out in order:
sequenceDiagram
participant C as Client
participant DNS as Resolver
participant TCP as TCP/TLS
participant S as Server
C->>DNS: resolve api.example.com
Note over DNS: time_namelookup: 0.081s
C->>TCP: TCP handshake + TLS
Note over TCP: time_connect: 0.210s
C->>S: GET /v1/orders
Note over S: server think time: 0.87s
S-->>C: first byte
Note over C: time_starttransfer: 1.083s
Note over C: time_total: 1.286s
Because they’re cumulative, the interesting value is always the gap between two checkpoints. “How long did the server actually sit there thinking?” isn’t TTFB — it’s the gap between the connection and the first byte:
server think time ≈ TTFB − Connect
In the session above: 1.083 − 0.210 = 0.87s. The network took a fifth of a second. The server took four times that, just thinking.
Step 2 — follow the biggest gap
Now you have one number to stare at. The rule: whatever stage adds the most time is where the conversation stops.
- DNS is huge → the resolver or the domain’s DNS config is slow. Bypass it to confirm:
curl --resolveagainst the IP you expect,2 then comparetime_namelookup. - Connect is huge → the network path or TLS handshake is slow. Compare a plain HTTP endpoint (or an IP hit) against HTTPS to see whether it’s the transport or the ceremony.
- The TTFB−Connect gap is huge → the application server. The endpoint, the database, the queue — something between the request arriving and the first byte going out. This is the one that usually bites in an API, and it’s the one you can actually fix.
The same decision, as a tree:
flowchart TD
A[Look at the gaps] --> B{Which gap is biggest?}
B -->|DNS| C[Resolver / DNS config]
B -->|Connect| D[Network path / TLS]
B -->|TTFB minus Connect| E[Application server]
C --> C1[curl --resolve against the IP<br/>compare time_namelookup]
D --> D1[HTTP vs HTTPS<br/>compare time_connect]
E --> E1[Endpoint, database, queue<br/>the fixable one]
Don’t blame the server on TTFB alone. TTFB is cumulative — it silently includes DNS and connection time.3 Always subtract Connect from time_starttransfer before you point at the application, or you’ll spend an hour chasing a database that was never the problem.
If the server gap is big, the terminal keeps paying off: hit the same endpoint twice to see if it’s cold-cache vs warm, hit a health check to see if it’s route-specific, hit it with a small payload to see if it’s serialization. Same -w, different questions.
Step 3 — when the terminal stops being enough
The honest caveat, because it matters: curl -w gives you one request. It tells you what the request just did — not whether it’s slow 5% of the time, not whether it started getting slow at 8pm, not whether the 99th percentile is a different animal than the median.
The terminal gets you to a precise, testable hypothesis in ten minutes. The moment the question becomes “how often” or “when” instead of “how much”, you’ve outgrown the one-liner — that’s what percentiles, tracing, and an APM are for. Knowing exactly where to aim the heavier tool is the entire value of the cheap one.
The cheat sheet
| Timer | Measures | Read the gap between |
|---|---|---|
time_namelookup |
DNS resolution | start → DNS done |
time_connect |
TCP handshake | DNS → connection |
time_appconnect |
TLS handshake | connection → TLS done |
time_starttransfer |
First byte of response | TLS → server’s answer |
time_total |
Full request | first byte → body done |
The rules
- Measure before you theorize. A theory without a number is a guess you’ll defend.
- One layer at a time. Find the biggest gap, fix or rule it out, then look at the next one.
The 340ms in Postman told you nothing. The 0.87s of server think time told you exactly which database call to go interrogate. That’s the whole difference.
Footnotes
-
curlwrite-out (-w) format — the manpage documents every timing variable:time_namelookup,time_connect,time_appconnect,time_starttransfer,time_total. ↩ -
curl --resolvepins a host to a specific address so a request skips DNS entirely. ↩ -
Time to First Byte — MDN glossary definition. ↩