Plate 71
OpenSSL TLS Handshake Lab: Full vs Resume on Localhost
Hands-on TLS handshake lab: Python full HS p50 TLS1.3 1.51 ms vs TLS1.2 1.20 ms (~72× TCP); openssl resume ~1.5× / 4.1×. Localhost RSA numbers, no Docker.
Aditya Challa5 min read
Intro — what this post promises
Every new HTTPS connection pays a TLS handshake before the first byte of app data. Session resume (tickets / IDs) is supposed to cut that cost. On a real WAN the 1-RTT story dominates; on localhost you mostly see crypto + userspace.
This is a hands-on lab with measured numbers:
- Python
ssl.wrap_socketfull handshake p50 for TLS 1.3 vs TLS 1.2 vs plain TCP connect. openssl s_time-newvs-reuseconnection rates on the same self-signed cert.- How much resume helped on this OpenSSL 3.5 stack — and where Python’s
session_reusedstayed false.
Related links:
- HTTP Keep-Alive vs Connection: close localhost lab
- TCP_NODELAY vs Nagle localhost lab
- Unix domain socket vs TCP localhost lab
- Why your average latency graph is lying (p50 / p95 / p99)
Lab honesty (30 Sep 2026 IST): Shared Linux lab box (8 cores, kernel 6.12). Python 3.13.5, OpenSSL 3.5.7. Self-signed RSA-2048 CN=localhost. Loopback only. No Docker. No GPU. No API keys. Affiliates: 0.
Verdict up front: Python full HS TLS1.3 p50 = 1.51 ms, TLS1.2 = 1.20 ms, plain TCP 0.021 ms (~72× / ~57×). openssl s_time resume: TLS1.3 ~1.5×, TLS1.2 ~4.1× more conn/real-s than -new.
What we compared
| Arm | Tool | What it measures |
|---|---|---|
| A | Python ssl | Full handshake wall p50 (n=50) |
| B | openssl s_time -new | Fresh handshakes / real second |
| C | openssl s_time -reuse | Session-resume handshakes / real second |
| Baseline | bare connect() | TCP-only connect p50 |
Related links:
Lab topology
Arm A — Python full handshake (p50, n=50)
| Setup | p50 ms | p95 ms | vs plain TCP |
|---|---|---|---|
| TLS 1.3 full | 1.507 | 2.948 | ~72× |
| TLS 1.2 full | 1.200 | 2.908 | ~57× |
| plain TCP connect | 0.021 | 0.055 | 1× |
On this localhost RSA stack, a full TLS handshake is tens of microseconds of TCP plus ~1.2–1.5 ms of crypto/userspace. TLS 1.2 edged TLS 1.3 here — that is a localhost RSA result, not a WAN 1-RTT claim.
Arm B — openssl s_time new vs reuse (3 s wall)
| Version | Mode | Connections | Conn / real s | vs -new |
|---|---|---|---|---|
| TLS 1.3 | -new | 1852 | 617 | 1× |
| TLS 1.3 | -reuse | 2768 | 923 | ~1.49× |
| TLS 1.2 | -new | 2143 | 714 | 1× |
| TLS 1.2 | -reuse | 8840 | 2947 | ~4.13× |
Resume helped both versions. TLS 1.2 resume was the big win on this cert/stack (~4×). TLS 1.3 resume still ~1.5× over full — smaller relative gain when the full handshake is already cheaper in RTT terms on the wire (here we only see local CPU).
Python session_reused stayed false
We tried passing SSLSession back into wrap_socket(..., session=...). session objects existed, but session_reused remained False for both TLS 1.2 and 1.3 on Python 3.13.5 / OpenSSL 3.5.7. Resume throughput numbers above are therefore openssl s_time -reuse only — labeled as such, not invented Python wins.
Related links:
When resume / keepalive still win the design
- Short-lived clients that reconnect often (CI curls, health probes, serverless cold paths).
- HTTP/1.1 without keep-alive — you pay handshake × request; pair this lab with the keep-alive post.
- TLS 1.2 estates where resume still shows multi-× local gains (Arm B).
- Not a substitute for measuring WAN RTT — localhost hides the 1-RTT advantage TLS 1.3 is famous for.
Pitfalls we hit (or avoided)
- Calling localhost TLS a WAN result — we measure crypto/userspace, not cross-city RTT.
- Assuming Python
session=resumes — it did not here; verifysession_reused. - Comparing TLS 1.3 vs 1.2 without pinning — arms forced single versions.
- Ignoring the TCP baseline — 0.021 ms shows almost all cost is above TCP.
- RSA-2048 self-signed only — ECDSA / different ciphers will move absolute ms.
Practical checklist
- Prefer connection reuse / HTTP keep-alive / HTTP/2+ before chasing cipher micro-wins.
- Confirm resume with
session_reused,openssl s_time -reuse, or server metrics — do not assume. - Report version + cert type + new vs reuse + localhost vs WAN with every handshake claim.
- On localhost, expect ~1 ms-class full HS for RSA; treat sub-ms claims with suspicion unless ECDSA/hardware offload.
- Pair with keep-alive / NODELAY posts when the question is request RPS, not handshake alone.
Verdict
Localhost self-signed RSA-2048: Python full handshake p50 TLS1.3 = 1.51 ms, TLS1.2 = 1.20 ms, plain TCP 0.021 ms. openssl s_time resume boosted real conn/s by ~1.5× (TLS1.3) and ~4.1× (TLS1.2). Python session_reused did not flip true — resume evidence is openssl-only on this stack.
Evidence path on the lab box: lab-evidence/26-openssl-tls-handshake/results/. Affiliates: 0.
Lab evidence
What I found running this
Lab 30 Sep 2026 IST. Python 3.13.5 + OpenSSL 3.5.7. Self-signed RSA-2048 CN=localhost. Python wrap_socket full HS p50 (n=50): TLS1.3 1.507 ms; TLS1.2 1.200 ms; plain TCP connect 0.021 ms (~72× / ~57×). openssl s_time 3s wall real/s: TLS1.3 new 617.3 reuse 922.7 (~1.49×); TLS1.2 new 714.3 reuse 2946.7 (~4.13×). Python session_reused stayed False — resume from openssl -reuse only. No Docker. Affiliates: 0. Evidence: lab-evidence/26-openssl-tls-handshake/.
Related links
Plate 12
islice vs list Slice Windows: Localhost Lab
Hands-on itertools.islice vs list slice window lab: real ops/s taking ranges from sequences, measured on Linux localhost in this hands-on lab for SREs.
Observability & SRE · 1 Oct 2026
Plate 07
heapq.merge vs sorted(chain): Localhost Lab
Hands-on heapq.merge vs sorted(chain) multi-way merge: real records/s on pre-sorted lists, measured on Linux localhost today in this hands-on lab for SREs.
Observability & SRE · 1 Oct 2026
Plate 88
mmap Write vs pwrite Region: Localhost Lab
Hands-on mmap MAP_SHARED write+msync vs pwrite region update: real MB/s with durability labels, measured on Linux localhost in this hands-on lab for SREs.
Observability & SRE · 1 Oct 2026