crypto/tls Handshake Path

Updated

September 8, 2026

crypto/tls Handshake Path

Overview

TLS is often the hidden cost in “simple HTTP” clients. Handshake CPU, session resumption, and certificate verification dominate cold connections; pooling and HTTP/2 amortize them.

Diagram: TLS then HTTP

flow:
  [HS]
       |
       v
  [App]

Path

Dial TCP
  -> TLS client hello
  -> server hello / certs
  -> key exchange / finished
  -> Application data (HTTP)
cfg := &tls.Config{
    MinVersion: tls.VersionTLS12,
    // ServerName for SNI verification when not in address
}
d := tls.Dialer{Config: cfg, NetDialer: &net.Dialer{Timeout: 5 * time.Second}}
conn, err := d.DialContext(ctx, "tcp", "example.com:443")

Cost Drivers

Factor Effect
New TCP+TLS each request Highest latency
Session tickets / resumption Cheaper reconnects
Huge trust stores / custom Verify CPU
HTTP/2 reuse One handshake, many streams
mTLS Client certs + private key ops

Client Integration

http.Transport.TLSClientConfig and idle pools determine whether you pay handshake per request. See Transport.

Security Notes

  • Pin MinVersion
  • Avoid InsecureSkipVerify outside tests
  • Prefer system roots or explicit pinned CAs for internal mesh

Deep mTLS ops: TLS/mTLS.

Experiment

go mod init example
# optional network:
# go run . example.com:443
package main

import (
    "crypto/tls"
    "fmt"
    "net"
    "os"
    "time"
)

func main() {
    addr := "example.com:443"
    if len(os.Args) > 1 {
        addr = os.Args[1]
    }
    start := time.Now()
    conn, err := tls.DialWithDialer(
        &net.Dialer{Timeout: 5 * time.Second},
        "tcp",
        addr,
        &tls.Config{MinVersion: tls.VersionTLS12},
    )
    if err != nil {
        fmt.Println("err", err)
        return
    }
    defer conn.Close()
    st := conn.ConnectionState()
    fmt.Printf("version=%x resumed=%v alpn=%q time=%s\n",
        st.Version, st.DidResume, st.NegotiatedProtocol, time.Since(start))
}

What to notice: First dial slower; second process may still be cold unless session cache configured at higher layers.

Try next: Compare http.Transport with ForceAttemptHTTP2 true/false under parallel GETs.