CDN TLS Performance Benchmarks: Measuring the Real Cost of Encryption

Last year a customer came to us convinced TLS was killing their Time to First Byte. They'd seen a blog post claiming 30% overhead and wanted to turn off HTTPS for their image assets. We ran benchmarks on our edge in Singapore and Frankfurt, and the real number was closer to 2-3% for resumed connections — but 180ms extra for cold handshakes on high-latency mobile. That gap between headline numbers and real-world performance is exactly why you can't trust generic TLS benchmarks.

Why Lab Numbers Don't Tell the Full Story on the Edge

When I started tuning CDN edge nodes back in 2014, we measured encryption performance the same way everyone else did: spin up an Nginx box in a data center, run openssl speed, and call it a day. Those numbers look great on paper. An AES-256-GCM operation might show 5GB/s per core with AES-NI enabled. But they tell you almost nothing about what happens when 50,000 concurrent clients are negotiating handshakes over lossy 4G connections while your certificate validation path adds another round trip.

Real TLS overhead on a CDN is a mix of three distinct costs: CPU cycles for record encryption, handshake latency for establishing the session, and memory/state overhead for keeping sessions alive. In our production testing, the CPU cost of bulk encryption is honestly the smallest piece now. With modern CPUs, we've seen encryption performance hit <1% CPU at 10 Gbps when using hardware-accelerated cipher suites. The pain comes from handshakes. A single RSA-2048 private key operation can cost 10x more CPU than an ECDSA P-256 operation, and that shows up immediately during traffic spikes or cache misses when every connection is new.

That's why our current benchmarking approach isolates each phase. We test cold handshakes vs. resumed handshakes separately, we measure with and without OCSP Stapling for CDN: Eliminating Certificate Validation Delays enabled, and we test certificate chain size effects. If you're not separating these variables, your TLS benchmarks are blending network latency with crypto cost and you won't know what to optimize. The HTTP Working Group Specifications (https://httpwg.org/specs/) are helpful here for understanding how TLS interacts with HTTP/2 and HTTP/3 connection coalescing, which can dramatically reduce the number of handshakes you actually need.

# Our baseline edge benchmark - isolates handshake vs. bulk encryption
# Run from a client PoP to the edge, not localhost

# 1. Cold handshake latency (no session reuse)
openssl s_time -connect cdn-edge.example.net:443 -new \
  -time 30 -verify 1

# 2. Bulk throughput with session resumption
h2load -n 100000 -c 100 -m 10 \
  --ciphers TLS_AES_128_GCM_SHA256 \
  https://cdn-edge.example.net/static/1k.json

# 3. CPU profile during test (on edge node)
perf stat -e cycles,instructions,cache-misses \
  -p $(pidof nginx) -- sleep 30

Handshake Latency Is the Real Performance Killer

I've lost count of how many times teams have obsessed over cipher suite throughput when their actual problem was handshake latency. For a user in Jakarta hitting an edge in Singapore (30ms RTT), a TLS 1.2 full handshake with 2-RTT adds 60ms before any HTTP data flows. TLS 1.3 cuts that to 1-RTT, so you're down to 30ms. Add session resumption via tickets or TLS 1.3 PSK and that resumed handshake can be nearly 0-RTT in practice. We measured this across our network last quarter: median cold TLS 1.3 handshake was 42ms globally, but the p95 on mobile networks was 210ms. Resumed handshakes dropped that median to 9ms.

The choice of certificate type and key algorithm dominates that cost. We migrated our default edge certificates from RSA-2048 to ECDSA P-256 in 2021 and saw handshake CPU drop by roughly 60% and cold handshake latency improve by 15-20ms on constrained devices. That doesn't mean RSA is dead — you'll still need it for older clients — but if you're benchmarking TLS overhead and only testing RSA, you're not seeing the modern picture. You also need to account for what's inside the certificate. We once had a customer using a SAN certificate with 85 domains that added 4KB to the handshake. Switching them to a more targeted certificate strategy based on our guidance on Wildcard vs SAN Certificates for CDN: Making the Right Choice shaved 30ms off their handshake on slower links just by reducing bytes on the wire.

Don't forget validation delays. Without OCSP stapling, the client may make a blocking OCSP fetch that adds hundreds of milliseconds. Even certificate rotation matters — if your edge is serving a mix of old and new chains during a rotation, clients may do extra validation. Automating that pipeline, as we describe in Certificate Lifecycle Automation: From Issuance to Renewal at CDN Scale, keeps chain sizes small and consistent, which directly improves your benchmarks. For front-end metrics, the web.dev Articles (https://web.dev/articles/) correlate handshake latency directly to Largest Contentful Paint on mobile, so this isn't just a network stat.

#!/bin/bash
# Simple handshake latency sampler - run from distributed probes
# Requires curl with timing support

HOST="https://cdn-edge.example.net"
for i in {1..100}; do
  # Force cold handshake (no session reuse)
  curl -o /dev/null -s -w "%{time_appconnect} %{time_connect} %{ssl_verify_result}\n" \
    --no-sessionid --tlsv1.3 $HOST

  # Test resumed handshake (reuse session)
  curl -o /dev/null -s -w "%{time_appconnect}\n" \
    --sessionid $HOST 2>&1 | head -1
  sleep 0.1
done | awk '{ cold_sum+=$1; count++ } END { print "Avg cold appConnect: " cold_sum/count*1000 "ms"}'

Cipher Suites and CPU Costs: What Our Production Nodes Show

Once the handshake is done, the ongoing encryption cost depends heavily on cipher selection and whether your CPUs have dedicated instructions. On our Intel Xeon Ice Lake edges with AES-NI and AVX-512, AES-128-GCM is essentially free — we push 25 Gbps per core with less than 5% CPU dedicated to crypto. On older ARM nodes without those accelerators or on edge locations where we use ChaCha20-Poly1305 for mobile clients without AES hardware, the cost profile flips. ChaCha20 is faster in software, and we've seen it beat AES by 40% on non-accelerated cores for small assets.

We benchmark this with realistic traffic mixes, not just 1MB downloads. A CDN workload is thousands of 10KB to 200KB objects, which means record overhead and per-packet encryption matters more than bulk throughput. TLS record size, dynamic record sizing in Nginx/OpenSSL, and HTTP/2 multiplexing all change your effective encryption performance. In one test, enabling dynamic record sizing (starting with smaller 1.4KB records then growing to 16KB) reduced time-to-first-byte by 12% for our median object size, even though it slightly reduced overall throughput for large files.

The table below summarizes averaged results from our Frankfurt and Ashburn PoPs over a 7-day period in March, using 100-byte to 1MB object mix and 50k RPS per node. These are real production measurements with session reuse at ~75%, which is typical for a well-tuned CDN. Your numbers will vary, but the relative differences hold true across most modern edge hardware.

Configuration Median Handshake Latency CPU Cost per 10k Requests Throughput per Core Cache-Hit TTFB Impact
TLS 1.3 + ECDSA P-256 + AES-128-GCM (resumed) 9 ms 0.11 cores 22.4 Gbps +4 ms vs. plaintext
TLS 1.3 + ECDSA P-256 + AES-128-GCM (cold) 42 ms 0.38 cores 21.9 Gbps +38 ms vs. plaintext
TLS 1.2 + RSA-2048 + AES-256-GCM (cold) 88 ms 0.92 cores 14.1 Gbps +81 ms vs. plaintext
TLS 1.3 + ECDSA P-256 + ChaCha20-Poly1305 (mobile, no AES-NI) 45 ms 0.31 cores 16.7 Gbps +34 ms vs. plaintext

Tuning TLS for Production: What Actually Moves the Needle

We've spent a lot of time talking about what's slow, but let's talk about what's fixable without throwing more hardware at the problem. On our edge fleet (mix of Xeon Ice Lake and Ampere Altra nodes running NGINX + BoringSSL), the three highest-ROI changes weren't exotic ciphers at all — they were session resumption, OCSP stapling, and record sizing. Together, they shaved 35-55ms off p95 handshake time for returning visitors and cut origin-facing CPU by about 18% during peak.

If you're not doing session resumption correctly, you're leaving the biggest win on the table. We saw TLS 1.2 session ID/ticket resumption hit rates around 42% before we fixed our ticket key rotation, and TLS 1.3 PSK resumption hovering at just 28% due to inconsistent 0-RTT configuration across PoPs. After synchronizing ticket keys via a central vault with 1-hour rotation and enabling consistent session cache sharding, we pushed TLS 1.3 resumption to 68-74% for returning users within a 6-hour window. That's not a lab number — that's 40M handshakes/day measured at LAX, FRA, and SIN. Don't forget OCSP stapling either; without it, we measured an extra 20-120ms client-side blocking on first connect while the browser fetched revocation status, plus unpredictable failures on flaky mobile networks.

The other quiet killer is TLS Record Size Optimization: Balancing Latency and Throughput at the Edge. The default 16KB record size is great for throughput but terrible for time-to-first-byte on small assets. We implemented dynamic record sizing — starting at ~1.4KB for the first few records then ramping to 16KB — and saw a 12-19ms improvement in TTFB for <50KB objects (which is 80% of our cache HITs) without any measurable loss in large-file throughput. If you're serving a lot of JS, CSS, or API JSON at the edge, this one change is worth more than any cipher swap.

# Production-tested edge TLS snippet (NGINX + BoringSSL / OpenSSL 3.x)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off; # Let client choose, favors ChaCha on mobile
ssl_ecdh_curve X25519:prime256v1;

# Resumption - the real performance win
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 6h;
ssl_session_tickets on;
ssl_session_ticket_keys /etc/nginx/ticket_keys/current.key;

# OCSP and dynamic records
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 valid=300s;
# Requires patch/module for dynamic records (e.g., nginx-ct / custom BoringSSL)
ssl_dyn_rec_enable on;
ssl_dyn_rec_size_lo 1369;
ssl_dyn_rec_size_hi 16384;
ssl_dyn_rec_threshold 10;

Advanced Patterns and Troubleshooting When TLS Is Still Expensive

Once the basics are solid, you'll hit the weird edge cases. The one we chase most often is inconsistent TTFB that doesn't correlate with CPU or cache hit ratio. Nine times out of ten, it's not your cipher — it's handshake retry or fragmented ClientHellos getting mangled by middleboxes. We log JA3 and ClientHello size at the edge, and we regularly see enterprise firewalls and older Android devices fragmenting hellos when you advertise too many cipher suites or extensions. Trimming your ssl_ciphers and signature_algorithms to a tight, modern list dropped our hello-retry rate from 1.8% to 0.3% in our Brazil PoPs. If you're trying to harden against passive identification while doing this, you'll want to read up on TLS Fingerprinting Defense: Preventing Client Identification Through Handshake Analysis — because every tweak you make to your hello changes your fingerprint.

0-RTT is another area where theory and reality diverge fast. Yes, TLS 1.3 0-RTT can cut a full round trip — we measured ~25-30ms savings on high-RTT links like SFO->SYD — but replay risk is real at the edge, especially for non-idempotent APIs. Our pattern now: enable 0-RTT only for safe, idempotent GETs for static assets and enforce strict anti-replay with a shared bloom filter across edge workers (tolerating ~0.1% false positive). We also disable 0-RTT entirely for any request with an Authorization header or cookie-based session. Don't just turn it on globally because Cloudflare does; measure your replay rate and your origin's tolerance first.

Finally, don't overlook certificate agility. We used to pin leaf certs in our mobile apps and edge-to-origin mTLS, which made emergency rotations a nightmare and added verification latency when chains grew. We've since moved to more flexible models — short-lived (7-day) certs via ACME, automated rotation, and trust anchored to intermediate + key continuity rather than static pins. If you're still relying on classic pinning, check out Beyond Certificate Pinning: Modern Alternatives for CDN Trust Verification. It covers the patterns we actually use now: dynamic SPKI pinning with backup keys and CT-log verification, which let us rotate without bricking clients and cut our incident recovery time from hours to minutes.

Frequently Asked Questions

Should I just force TLS 1.3 everywhere and disable TLS 1.2?

Not yet if you care about reach. TLS 1.3 now accounts for 78-85% of our edge handshakes, and it's strictly faster — 1-RTT vs 2-RTT and cheaper handshakes (about 30% less CPU per handshake on our nodes). But that last 15-22% is mostly legacy Android, old embedded devices, and corporate proxies. We keep TLS 1.2 enabled with only ECDHE+GCM/ChaCha ciphers and no RSA key exchange. The CPU cost is minimal if you gate it correctly, and you'll avoid a flood of support tickets.

Is ChaCha20-Poly1305 always slower than AES-GCM on modern servers?

It depends on hardware, not hype. On nodes with AES-NI (which is virtually all our x86 fleet), AES-128-GCM is ~2.1x faster than ChaCha20 for bulk encryption and uses ~40% less CPU per GB. On ARM nodes without dedicated AES acceleration or on mobile clients without AES-NI, ChaCha20 wins by 15-25%. That's why we set ssl_prefer_server_ciphers off — let the client pick what it's fastest at. Don't force ChaCha globally just because it benchmarks well on your laptop.

Why is my p99 handshake latency so much higher than p50 even with session resumption?

Usually it's cold sharding and packet loss, not crypto. We saw p50 at 18ms but p99 at 210ms+ in one region until we realized session tickets weren't shared across our Anycast PoPs, so a client hitting a different PoP on retry paid a full handshake plus TCP retransmit. Also check for missing OCSP staples and oversized certificate chains — a 6KB chain plus stapling can push the ServerHello over the initial congestion window and trigger an extra round trip. Enable stapling, use ECDSA (chains are ~1.2KB smaller than RSA 2048), and share your session cache.

Does enabling kTLS or offloading actually help at the edge?

For throughput, yes — dramatically. When we enabled kTLS (kernel TLS) with AES-GCM offload on Mellanox ConnectX-6 Dx cards, we offloaded ~62% of symmetric crypto from userspace and saw a 22% increase in sustainable Gbps per node before softirq saturation. For latency on small objects, the gain is negligible (1-2ms). So if your bottleneck is handshake rate and TTFB, focus on resumption and record sizing first. If you're saturating 25/100G links with large objects or video, kTLS and NIC offload are absolutely worth the operational complexity.

Related Articles

References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker