On a small VPS, the RSA or ECDSA signing operation in a TLS 1.3 handshake is often the most expensive thing your CPU does per request. Full handshakes from cold clients can consume 5–15% of a single vCPU at just a few hundred requests per second. The fix is not a larger instance — it is session resumption, ticket key rotation, and OCSP stapling configured so that the expensive asymmetric work happens once per client rather than once per connection. This is the configuration and the measurement to prove it worked.
Where the CPU Actually Goes
In TLS 1.3, a full handshake costs one or two signature operations on the server plus a key exchange. An abbreviated handshake via PSK resumption costs a symmetric operation and no signature at all. Measure the difference directly:
# openssl speed: know your signature cost before tuning
openssl speed -seconds 2 rsa2048 ecdsap256 # legacy paths
# on a 1-vCPU KVM guest: rsa2048 ~50-120 sign/s, ecdsap256 ~600-1500 sign/s
# full handshake vs resumption, per-connection cost
for i in 1 2 3; do
openssl s_client -connect example.com:443 -tls1_3 -no_ticket /dev/null \
| grep -i 'New, TLSv1.3'
openssl s_client -connect example.com:443 -tls1_3 /dev/null \
| grep -i 'Reused, TLSv1.3'
done
Track server-side resumption rate rather than guessing. Nginx exposes it through the log format; Prometheus through nginx_vts or the stub_status counters:
# nginx: add resumption indicators to the access log
log_format tls '$remote_addr $ssl_protocol $ssl_curve $ssl_session_reused '
'$ssl_session_id "$request" $request_time';
access_log /var/log/nginx/tls.log tls;
# what fraction of connections resumed?
awk '{print $4}' /var/log/nginx/tls.log | sort | uniq -c | sort -rn
# expect "r" (reused) to dominate for repeat visitors
Session Tickets: Rotation Without Breaking Resumption
TLS 1.3 uses stateless session tickets, so no server-side cache is needed — but the ticket key must be shared across workers and rotated on a schedule (Nginx uses a 128-bit key plus a 4-byte name). The classic mistake is enabling tickets with the default single key and never rotating, which lets a stolen key decrypt recorded sessions indefinitely:
# /etc/nginx/nginx.conf
ssl_session_cache shared:SSL:20m; # 20 MB ~ 80k sessions per worker set
ssl_session_timeout 4h;
ssl_session_tickets on;
# TLS 1.3 requires an explicit ticket key for stable resumption across
# reloads and multi-worker setups on older nginx builds:
ssl_session_ticket_key /etc/nginx/ticket.key; # 80 bytes, mode 0600
# generate with correct entropy and permissions
sudo head -c 80 /dev/urandom > /etc/nginx/ticket.key
sudo chmod 600 /etc/nginx/ticket.key
sudo chown root:root /etc/nginx/ticket.key
Rotate the key on a schedule, keeping the previous key for one full ssl_session_timeout window so in-flight sessions remain resumable:
#!/bin/bash
# /etc/cron.monthly/rotate-ticket-key
set -euo pipefail
KEY=/etc/nginx/ticket.key
cp -p "$KEY" "${KEY}.old"
head -c 80 /dev/urandom > "$KEY.new"
chmod 600 "$KEY.new"; chown root:root "$KEY.new"
mv "$KEY.new" "$KEY"
nginx -t && systemctl reload nginx
# remove the .old key after ssl_session_timeout has elapsed — nginx
# keeps it in memory for the running generation only
find /etc/nginx -name 'ticket.key.old' -mtime +1 -delete
OCSP Stapling: Remove the Client-Side Fetch
Without stapling, a client must contact the CA’s OCSP responder, adding a round trip and, more importantly for you, adding a dependency that fails when the responder is slow — a common cause of intermittent TLS stalls. Stapling embeds a signed, cached OCSP response in the handshake. Verify it works before and after:
# /etc/nginx/conf.d/tls.conf
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ca-chain.pem; # full chain incl. root
resolver 1.1.1.1 [2606:4700:4700::1111] valid=300s ipv6=off;
resolver_timeout 5s;
# verify: look for a non-empty "OCSP Response Status: successful"
openssl s_client -connect example.com:443 -status -servername example.com /dev/null \
| sed -n '/OCSP Response Status/,/^---/p'
# if it says "no response sent", check the nginx error log — usually a
# missing ssl_trusted_certificate or an unreachable resolver
| Configuration | Handshake CPU (1 vCPU) | Client-visible latency |
|---|---|---|
| Full handshake, RSA-2048, no stapling | ~1.0x baseline | +1 RTT to OCSP responder |
| Full handshake, ECDSA P-256 | ~0.15x of RSA cost | +1 RTT |
| Resumption enabled, ECDSA | ~0.03x | 0 extra RTT |
| Resumption + stapling, ECDSA | ~0.03x | 0 extra RTT, no CA dependency |
Measure the Effect Properly
Use a load test that includes a realistic mix of new and returning clients, and watch CPU, not just requests per second:
# 30s run, 50 connections, HTTP/2, TLS 1.3, ECDSA certificate
h2load -n 30000 -c 50 -m 10 --tls13 https://example.com/ -t 2
# CPU consumed by nginx during the same window
pidstat -p $(pgrep -o nginx) 1 30 | tail -5
# syscall-level view of crypto work
sudo perf top -p $(pgrep -o nginx) --stdio 2>/dev/null | head -20
# expect the top frames to be in the handshake path, then drop away
# once resumption dominates
Certificates are part of this budget too: ECDSA P-256 halves handshake cost versus RSA-2048, and you can serve both with an ssl_certificate pair for compatibility. Once handshake CPU drops, the next bottleneck is usually application-side — if that turns out to be the case, the architecture guidance on virtualserversvps.com covers where to place caching and TLS termination. And if you are still deciding how much CPU to buy in the first place, the hosting overview at virtualserversvps.com is a useful sanity check on whether a bigger instance or better TLS configuration is the cheaper answer.
Checklist
- Benchmark with
openssl speedto know your per-signature cost. - Enable
ssl_session_cachewith an explicit shared zone and a 4-hour timeout. - Set and rotate
ssl_session_ticket_keymonthly; never leave the default key in place. - Enable
ssl_staplingplusssl_stapling_verifywith a working resolver and full trusted chain. - Serve ECDSA and verify with
h2loadpluspidstatthat CPU per request actually fell.
TLS tuning is pure measurement: two configuration changes, one verification command each, and a CPU graph that proves the improvement is real rather than imagined.

Leave a Reply
You must be logged in to post a comment.