Abdulmuiz A.
← All posts
Backend & API designAug 20265 min read

Your API is slow. Stop guessing.

Y
In this post

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

curl -w timing breakdown
$curl -s -o /dev/null -w 'DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n' https://api.example.com/v1/orders
DNS: 0.081s
Connect: 0.210s
TTFB: 1.083s
Total: 1.286s

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 --resolve against the IP you expect,2 then compare time_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]
Proceed with caution

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

  1. Measure before you theorize. A theory without a number is a guess you’ll defend.
  2. 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

  1. curl write-out (-w) format — the manpage documents every timing variable: time_namelookup, time_connect, time_appconnect, time_starttransfer, time_total. ↩

  2. curl --resolve pins a host to a specific address so a request skips DNS entirely. ↩

  3. Time to First Byte — MDN glossary definition. ↩