How We Measure HTTP Timing: DNS, TCP, TLS, TTFB Breakdown

Published: (January 1, 2026 at 11:55 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

Cover image for How We Measure HTTP Timing: DNS, TCP, TLS, TTFB Breakdown

When you check a website’s response time, “847 ms” doesn’t tell you much. Is it a slow DNS lookup? Network latency? The server itself? You need a breakdown.

At upsonar.io we show exactly where the time goes. Here’s how we do it with Go’s net/http/httptrace package.

The problem

start := time.Now()
resp, _ := http.Get("https://example.com")
fmt.Println(time.Since(start)) // 847ms

847 ms – but why? It could be any part of the request lifecycle.

The solution

httptrace hooks into every phase of an HTTP request. You attach callbacks, and they fire as each phase completes:

trace := &httptrace.ClientTrace{
    DNSStart: func(_ httptrace.DNSStartInfo) {
        dnsStart = time.Now()
    },
    DNSDone: func(_ httptrace.DNSDoneInfo) {
        fmt.Printf("DNS: %v\n", time.Since(dnsStart))
    },
    // same pattern for TCP, TLS, etc.
}

req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))

What each metric means

  • DNS Lookup – Resolving the domain to an IP address. > 100 ms often indicates DNS server issues or a cold cache.
  • TCP Connect – The three‑way handshake. Mostly affected by physical distance to the server.
  • TLS Handshake – Negotiating encryption. Typical values are 50–150 ms; > 300 ms suggests server or certificate‑chain problems.
  • TTFB (Time To First Byte) – Includes DNS, TCP, TLS, and server processing. The standard metric for measuring response speed.
  • Transfer – Downloading the response body.

Working example

package main

import (
    "crypto/tls"
    "fmt"
    "io"
    "net/http"
    "net/http/httptrace"
    "os"
    "time"
)

func main() {
    url := "https://httpbin.org/delay/2"
    if len(os.Args) > 1 {
        url = os.Args[1]
    }

    var dnsStart, tcpStart, tlsStart time.Time
    totalStart := time.Now()

    trace := &httptrace.ClientTrace{
        DNSStart: func(_ httptrace.DNSStartInfo) { dnsStart = time.Now() },
        DNSDone:  func(_ httptrace.DNSDoneInfo) { fmt.Printf("DNS:  %v\n", time.Since(dnsStart)) },

        ConnectStart: func(_, _ string) { tcpStart = time.Now() },
        ConnectDone:  func(_, _ string, _ error) { fmt.Printf("TCP:  %v\n", time.Since(tcpStart)) },

        TLSHandshakeStart: func() { tlsStart = time.Now() },
        TLSHandshakeDone:  func(_ tls.ConnectionState, _ error) { fmt.Printf("TLS:  %v\n", time.Since(tlsStart)) },

        GotFirstResponseByte: func() { fmt.Printf("TTFB: %v\n", time.Since(totalStart)) },
    }

    req, _ := http.NewRequest("GET", url, nil)
    req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))

    client := &http.Client{Transport: &http.Transport{DisableKeepAlives: true}}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer resp.Body.Close()

    transferStart := time.Now()
    io.ReadAll(resp.Body)
    fmt.Printf("Transfer: %v\n", time.Since(transferStart))
    fmt.Printf("Total: %v\n", time.Since(totalStart))
}

Run the program:

go run main.go https://httpbin.org/delay/2

Sample output

DNS:  148.00525ms
TCP:  142.56925ms
TLS:  289.685ms
TTFB: 2.956339583s
Transfer: 72.208µs
Total: 2.95670575s

TTFB is everything up to the first byte. Transfer is the time to download the body – here it’s tiny, so the total time ≈ TTFB.

Larger response example

go run main.go https://proof.ovh.net/files/10Mb.dat

Sample output

DNS:  3.621333ms
TCP:  54.364208ms
TLS:  116.879041ms
TTFB: 286.073291ms
Transfer: 14.221007833s
Total: 14.507351083s

With a 10 MB file, Transfer dominates the overall latency.

Watch out for connection reuse

Go’s http.Client reuses connections by default. Subsequent requests to the same host will show 0 ms for DNS, TCP, and TLS because the connection is already established.

For accurate measurements, disable keep‑alives:

client := &http.Client{
    Transport: &http.Transport{
        DisableKeepAlives: true,
    },
}

What slow numbers tell you

  • DNS > 100 ms → Try a faster DNS provider (e.g., Cloudflare, Google).
  • TCP > 100 ms → Server is far from users; consider a CDN.
  • TLS > 200 ms → Check the certificate chain; enable session resumption.
  • TTFB > 500 ms → Backend bottleneck: slow DB, cold starts, heavy processing.
  • Transfer high → Large payload; enable gzip/Brotli compression.

The 200 ms rule

Users notice delays over ~200 ms. If your TTFB alone exceeds that threshold, the page will feel slow regardless of frontend optimizations.

Try it on your own site with the free, no‑signup tool: upsonar.io/tools/diagnose.

Back to Blog

Related posts

Read more »

An Honest Review of Go (2025)

Article URL: https://benraz.dev/blog/golang_review.html Comments URL: https://news.ycombinator.com/item?id=46542253 Points: 58 Comments: 50...

TCP Doesn’t Know What a Message Is

When I was working with HTTP, I carried a quiet assumption in the back of my mind: > if I send one thing, the other side receives one thing. It felt obvious. Al...