DEV Community

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

Posted on

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

When you check a website's response time, "847ms" doesn't tell you much. Is it slow DNS? 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
Enter fullscreen mode Exit fullscreen mode

847ms. But why? Could be anything.

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))
Enter fullscreen mode Exit fullscreen mode

What each metric means

DNS Lookup - resolving domain to IP. Slow (100ms+) means DNS server issues or cold cache.

TCP Connect - the three-way handshake. Mostly about physical distance to the server.

TLS Handshake - negotiating encryption. Usually 50-150ms. Over 300ms means something's off - slow server or problematic certificate chain.

TTFB - time to first byte. Includes DNS + TCP + TLS + server processing. This is 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))
}
Enter fullscreen mode Exit fullscreen mode
go run main.go https://httpbin.org/delay/2
Enter fullscreen mode Exit fullscreen mode

Output:

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

TTFB is everything until the first byte arrives. Transfer is downloading the body - here it's tiny. Total = TTFB + Transfer.

With a larger response, Transfer becomes significant:

go run main.go https://proof.ovh.net/files/10Mb.dat
Enter fullscreen mode Exit fullscreen mode
DNS:  3.621333ms
TCP:  54.364208ms
TLS:  116.879041ms
TTFB: 286.073291ms
Transfer: 14.221007833s
Total: 14.507351083s
Enter fullscreen mode Exit fullscreen mode

Same TTFB formula, but now Transfer dominates - 14 seconds to download 10MB.

Watch out for connection reuse

Go's http.Client reuses connections by default. Second request to the same host - DNS, TCP, TLS all show 0ms.

For accurate measurements, disable keep-alives:

client := &http.Client{
    Transport: &http.Transport{
        DisableKeepAlives: true,
    },
}
Enter fullscreen mode Exit fullscreen mode

What slow numbers tell you

  • DNS > 100ms → Try a faster DNS provider (Cloudflare, Google)
  • TCP > 100ms → Server is far from users, consider a CDN
  • TLS > 200ms → Check certificate chain, enable session resumption
  • TTFB > 500ms → Backend problem: slow database, cold starts, heavy processing
  • Transfer high → Large response, enable gzip/brotli compression

The 200ms rule

Users notice delays over 200ms. If your TTFB alone exceeds that, the page will feel slow - no matter how optimized your frontend is.


Try it on your site: upsonar.io/tools/diagnose - free, no signup.


What tools do you use for debugging slow requests?

Top comments (0)