crypto/tls Handshake Path
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
InsecureSkipVerifyoutside 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:443package 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.