HTTPS Handshake Optimization: Cutting Latency from 200ms to 50ms
Users were complaining about slow page loads. Chrome DevTools showed a TTFB (Time To First Byte) of 400ms, with HTTPS handshake alone taking 200ms. This post documents how I got that down to 50ms.
1. Diagnosing the Problem
1.1 Packet Analysis
# Measure timing for each phase with curl
curl -w "@curl-format.txt" -o /dev/null -s https://terra-nas.com
# curl-format.txt:
# time_namelookup: %{time_namelookup}s\n
# time_connect: %{time_connect}s\n
# time_appconnect: %{time_appconnect}s\n
# time_starttransfer: %{time_starttransfer}s\n
# time_total: %{time_total}s\nResults:
time_namelookup: 0.020s # DNS
time_connect: 0.060s # TCP (RTT ≈ 40ms)
time_appconnect: 0.260s # TLS handshake complete!
time_starttransfer: 0.280s
time_total: 0.350sTLS handshake took 200ms — the time between TCP connection and application data transfer.
1.2 Breaking Down the Handshake
Using openssl s_client for detailed analysis:
openssl s_client -connect terra-nas.com:443 -status 2>&1 | head -50Issues found:
- Server was using TLS 1.2 (requires 2-RTT handshake)
- OCSP response — client was fetching it separately (extra latency)
- Certificate chain included 3 certificates (more data to transfer)
2. TLS Handshake Internals
2.1 TLS 1.2 Handshake (2-RTT)
Client Server
| |
|------- ClientHello --------------->| RTT 1
|<------ ServerHello + Cert ---------|
|<------ ServerKeyExchange ----------|
|<------ ServerHelloDone ------------|
| |
|------- ClientKeyExchange --------->| RTT 2
|------- ChangeCipherSpec ---------->|
|------- Finished ------------------>|
|<------ ChangeCipherSpec -----------|
|<------ Finished -------------------|
| |
|======= Application Data ==========>| Finally can send data!Latency: At least 2 RTTs. With RTT=40ms, TLS handshake ≥ 80ms.
But we measured 200ms — there’s additional overhead somewhere.
2.2 TLS 1.3 Handshake (1-RTT)
Client Server
| |
|------- ClientHello + KeyShare ---->| RTT 1
|<------ ServerHello + KeyShare -----|
|<------ EncryptedExtensions --------|
|<------ Certificate ----------------|
|<------ CertificateVerify ----------|
|<------ Finished -------------------|
|------- Finished ------------------>|
| |
|======= Application Data ==========>| Can send data after 1-RTT!Key optimization: ClientHello includes key exchange parameters upfront, saving one RTT.
2.3 0-RTT (TLS 1.3 Early Data)
# After first connection, server sends Session Ticket
# Subsequent connections can send encrypted data immediately
Client Server
| |
|------- ClientHello + EarlyData --->| 0-RTT!
|======= Application data already flowing ======>|
|<------ ServerHello + ... ----------|Heads up: 0-RTT is vulnerable to replay attacks — only use for idempotent requests.
3. Optimization Steps
3.1 Enable TLS 1.3
Nginx config:
server {
listen 443 ssl http2;
ssl_protocols TLSv1.2 TLSv1.3; # Support both 1.2 and 1.3
ssl_prefer_server_ciphers off; # Not needed for TLS 1.3
# TLS 1.3 cipher suites
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# Enable 0-RTT (use with caution)
ssl_early_data on;
}Result: TLS handshake drops from 2-RTT to 1-RTT, saving 40ms.
3.2 OCSP Stapling
The problem: Clients need to verify if the certificate is revoked. Traditional approach sends an OCSP request to the CA.
Traditional flow:
Client --> Server: Get certificate
Client --> CA (OCSP): Is this cert valid? ← Extra RTT!
CA --> Client: Yes
Client --> Server: Continue handshakeOCSP Stapling: Server pre-fetches the OCSP response and sends it during the handshake.
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/chain.pem;
# Set OCSP responder
resolver 8.8.8.8 valid=300s;
resolver_timeout 5s;Verify it’s working:
openssl s_client -connect terra-nas.com:443 -status 2>&1 | grep -A 15 "OCSP"
# OCSP Response Status: successfulResult: Eliminates client-side OCSP lookup, saving 50-100ms.
3.3 Certificate Chain Optimization
The problem: Full certificate chain might have 3-4 certs, increasing transfer size.
# Check the certificate chain
openssl s_client -connect terra-nas.com:443 -showcerts 2>/dev/null | grep "s:"
# s:/CN=terra-nas.com
# s:/CN=Let's Encrypt Authority X3 # Intermediate cert
# s:/CN=DST Root CA X3 # Root cert (shouldn't be sent!)Fix: Only send site cert + intermediate cert. Skip root cert (browsers already have it).
# Correct certificate file should contain:
# 1. Site certificate
# 2. Intermediate certificate
# (NOT the root certificate)
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;Result: Reduces transfer by ~1KB.
3.4 Session Resumption
Session Tickets (stateless resumption):
ssl_session_tickets on;
ssl_session_timeout 1d; # 24 hours
# Custom ticket key (share across servers if needed)
ssl_session_ticket_key /path/to/ticket.key;Session Cache (stateful resumption):
ssl_session_cache shared:SSL:10m; # 10MB shared cache
ssl_session_timeout 1d;Result: Returning visitors get 1-RTT or even 0-RTT handshakes.
3.5 Switch to ECDSA Certificates
RSA certificate verification is slower. ECDSA is faster and has shorter keys.
# Generate ECDSA private key
openssl ecparam -genkey -name prime256v1 -out privkey.pem
# Generate CSR
openssl req -new -key privkey.pem -out csr.pem
# Request certificate (Let's Encrypt supports ECDSA)
certbot certonly --csr csr.pemComparison:
| Type | Key Length | Security Equivalent | Signing Speed |
|---|---|---|---|
| RSA 2048 | 2048 bits | 112 bits | Slow |
| ECDSA P-256 | 256 bits | 128 bits | 3-4x faster |
3.6 Enable HTTP/2
HTTP/2 multiplexes requests over a single TCP connection, so TLS handshake only happens once.
listen 443 ssl http2;4. Performance Comparison
4.1 Before vs After
# Before optimization
curl -w "TLS: %{time_appconnect}s\n" -o /dev/null -s https://terra-nas.com
# TLS: 0.260s
# After optimization
curl -w "TLS: %{time_appconnect}s\n" -o /dev/null -s https://terra-nas.com
# TLS: 0.050s4.2 Time Savings Breakdown
| Optimization | Time Saved |
|---|---|
| TLS 1.3 (1-RTT) | 40ms |
| OCSP Stapling | 60ms |
| Session Resumption | 40ms |
| Certificate chain | 10ms |
| ECDSA certificate | 5ms |
| Total | ~150ms |
5. Monitoring & Verification
5.1 SSL Labs Test
Head to https://www.ssllabs.com/ssltest/ for a detailed score.
Key metrics to check:
- Protocol Support: TLS 1.3 ✓
- OCSP Stapling: Yes ✓
- Certificate Chain: 2 certificates ✓
5.2 Chrome DevTools
Timing panel:
DNS Lookup: 20ms
Initial connection: 50ms ← Significantly reduced
SSL: 30ms ← TLS handshake
TTFB: 80ms5.3 Prometheus Monitoring
# Monitor TLS handshake time
- job_name: 'nginx'
metrics_path: '/metrics'
static_configs:
- targets: ['localhost:9113']
# Alert rule
- alert: HighTLSHandshakeTime
expr: nginx_http_ssl_handshake_time_seconds_bucket{le="0.1"} / nginx_http_ssl_handshake_time_seconds_count < 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "Over 10% of TLS handshakes taking > 100ms"6. Complete Config Example
server {
listen 443 ssl http2;
server_name terra-nas.com;
# Certificates
ssl_certificate /etc/letsencrypt/live/terra-nas.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/terra-nas.com/privkey.pem;
# Protocol and cipher suites
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/terra-nas.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
# Session resumption
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets on;
# 0-RTT (use carefully)
ssl_early_data on;
# Security headers
add_header Strict-Transport-Security "max-age=31536000" always;
# ...
}7. Summary
| Technique | How It Works | Impact |
|---|---|---|
| TLS 1.3 | 1-RTT handshake | -40ms |
| OCSP Stapling | Server pre-fetches cert status | -60ms |
| Session Resumption | Reuse previous handshake | -40ms |
| Trim cert chain | Less data to transfer | -10ms |
| ECDSA certificate | Faster signing algorithm | -5ms |
Key takeaway: HTTPS optimization isn’t just about choosing algorithms — it’s about reducing round trips (RTT). These optimizations make an even bigger difference on high-latency networks (like cross-continent connections).