Contents

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

1
2
3
4
5
6
7
8
9
# 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\n

Results:

1
2
3
4
5
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.350s

TLS 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:

1
openssl s_client -connect terra-nas.com:443 -status 2>&1 | head -50

Issues found:

  1. Server was using TLS 1.2 (requires 2-RTT handshake)
  2. OCSP response — client was fetching it separately (extra latency)
  3. Certificate chain included 3 certificates (more data to transfer)

2. TLS Handshake Internals

2.1 TLS 1.2 Handshake (2-RTT)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)

1
2
3
4
5
6
7
8
# 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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.

1
2
3
4
5
Traditional flow:
Client --> Server: Get certificate
Client --> CA (OCSP): Is this cert valid?  ← Extra RTT!
CA --> Client: Yes
Client --> Server: Continue handshake

OCSP Stapling: Server pre-fetches the OCSP response and sends it during the handshake.

1
2
3
4
5
6
7
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:

1
2
openssl s_client -connect terra-nas.com:443 -status 2>&1 | grep -A 15 "OCSP"
# OCSP Response Status: successful

Result: 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.

1
2
3
4
5
# 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).

1
2
3
4
5
6
7
# 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):

1
2
3
4
5
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):

1
2
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.

1
2
3
4
5
6
7
8
# 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.pem

Comparison:

TypeKey LengthSecurity EquivalentSigning Speed
RSA 20482048 bits112 bitsSlow
ECDSA P-256256 bits128 bits3-4x faster

3.6 Enable HTTP/2

HTTP/2 multiplexes requests over a single TCP connection, so TLS handshake only happens once.

1
listen 443 ssl http2;

4. Performance Comparison

4.1 Before vs After

1
2
3
4
5
6
7
# 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.050s

4.2 Time Savings Breakdown

OptimizationTime Saved
TLS 1.3 (1-RTT)40ms
OCSP Stapling60ms
Session Resumption40ms
Certificate chain10ms
ECDSA certificate5ms
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

1
2
3
4
5
Timing panel:
  DNS Lookup:     20ms
  Initial connection: 50ms  ← Significantly reduced
  SSL:            30ms      ← TLS handshake
  TTFB:           80ms

5.3 Prometheus Monitoring

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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

TechniqueHow It WorksImpact
TLS 1.31-RTT handshake-40ms
OCSP StaplingServer pre-fetches cert status-60ms
Session ResumptionReuse previous handshake-40ms
Trim cert chainLess data to transfer-10ms
ECDSA certificateFaster 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).