Cryptography is the only part of HTTPS that is expensive
Serving a cached page over an established connection is close to free: a symmetric cipher runs at gigabytes per second per core and the content comes out of memory. Starting a new HTTPS connection is a different kind of work. Before any request exists, your server has to:
- Complete the TCP handshake (cheap, but see SYN floods for what happens when even this is attacked).
- Generate an ephemeral key share and compute a shared secret.
- Sign something with your certificate's private key, to prove the certificate is yours.
- Send the certificate chain — typically three to six kilobytes of it.
Step 3 is the expensive one, and the order matters more than the cost: in both TLS 1.2 with ECDHE and TLS 1.3, the server performs that signature in the flight it sends immediately after the ClientHello — before it has any evidence that the client on the other end intends to finish, or exists at all.
| Operation your server performs per handshake | Rough throughput, one modern core |
|---|---|
| RSA-2048 private-key signature | low thousands per second |
| ECDSA P-256 signature | tens of thousands per second |
| X25519 or P-256 key agreement | tens of thousands per second |
| AES-GCM bulk encryption of the response that follows | gigabytes per second — effectively free by comparison |
Do not take those numbers from a web page: run openssl speed rsa2048 ecdsap256 ecdhx25519 on the machine that actually terminates your TLS. The result, divided by your core count and halved for everything else the box is doing, is your real ceiling in new HTTPS connections per second. For a great many origins that number lands somewhere between one and ten thousand — which is well within the reach of a rented botnet, and nowhere near the gigabits those same providers advertise protection against.
The asymmetry, written out
| The attacker pays | You pay | |
|---|---|---|
| Bytes on the wire | One ClientHello, ~300–600 bytes | ServerHello, key share, certificate chain and signature: ~3–6 KB |
| Cryptography | Optional — a client that abandons the handshake performs none of it | One signature plus one key agreement, every time, before it knows the client is real |
| State | None, if it never answers | A connection and a worker slot held until your handshake timeout expires |
| Scale cost | Pennies per hour per thousand sources | A CPU ceiling you cannot raise during the incident |
Read the first row twice. Even ignoring CPU entirely, an abandoned handshake makes you transmit roughly eight times what the attacker transmitted, which quietly turns a modest inbound flood into a meaningful outbound one. This is why egress often rises faster than ingress during a handshake flood — a detail that misleads people into looking for a scraper.
Six shapes of the same attack
| Variant | What the client does | What it costs you |
|---|---|---|
| Abandoned handshake | Sends a ClientHello and never answers the server's flight | The full signature, key share and certificate chain, with nothing recovered |
| Complete-then-drop | Finishes the handshake, sends no request, closes | The handshake plus a session entry you will never reuse |
| HTTPS flood on fresh connections | One request per connection, no keep-alive, no resumption | A handshake per request — request rate looks ordinary while crypto saturates |
| Renegotiation flood (THC-SSL-DOS) | Repeatedly asks to renegotiate inside one open connection | Many handshakes from a single cheap socket. Removed in TLS 1.3; still reachable on TLS 1.2 stacks that permit client-initiated renegotiation |
| Resumption abuse | Presents malformed or stale session tickets | A full handshake every time, plus cache lookups for tickets that will never match |
| Expensive-parameter pressure | Advertises only the costliest group or ciphersuite you still support | The slowest cryptographic path you permit, chosen for you by the attacker |
The last row is the one defenders usually miss. Your TLS configuration is a menu, and the attacker orders from it. If a large finite-field Diffie-Hellman group or an RSA key-exchange suite is still on that menu for compatibility, a ClientHello advertising nothing else will be served exactly that.
The first three rows are also where a handshake flood blurs into neighbouring attack families. One request per connection at scale is simultaneously a Layer 7 attack and a crypto-exhaustion attack; an abandoned handshake that is deliberately kept half-finished for as long as your timeout allows is a low-and-slow attack that happens to be aimed at the TLS layer. Real incidents rotate between them, which is the subject of multi-vector attacks.
Your bandwidth graph will mislead you again
The arithmetic is the same lesson as a SYN flood, one layer up:
| New handshakes/sec | Inbound traffic, roughly | What it typically takes down first |
|---|---|---|
| 500 | ~2 Mbps | A small VM terminating TLS with an RSA certificate |
| 2,000 | ~8 Mbps | Most single origins, and any PHP or Node process sharing those cores |
| 10,000 | ~40 Mbps | Untuned load balancers and reverse proxies; mid-range appliances |
| 100,000 | ~0.4 Gbps | Anything without a filtering layer upstream of the TLS terminator |
Forty megabits per second will not register on a dashboard calibrated for gigabits. If your monitoring plots bandwidth and requests per second but not new connections per second, a handshake flood is close to invisible to you until the site is already down, and the symptom — high CPU, HTTPS timing out, access log quieter than usual — reads exactly like a server fault.
Confirming that this is what is happening
Five checks, in the order that settles it fastest:
- Profile the CPU.
perf topon the TLS terminator during the incident. If OpenSSL symbols dominate user time — the P-256 or RSA montgomery routines — the box is spending itself on handshakes, not on your application. - Compare connection rate against request rate.
nstat -az TcpPassiveOpenssampled a few seconds apart gives new connections; your access log gives requests. A connection rate at or above the request rate means almost nothing is being amortised over keep-alive, which is the signature of this attack. - Count resumptions. Log
$ssl_session_reusedin nginx (orssl_fc_is_resumedin HAProxy). Healthy traffic resumes a large share of its sessions; a flood resumes essentially none. - Read the error log in bulk. Thousands of
SSL_do_handshake() failedorwhile SSL handshakinglines mean handshakes are being started and abandoned at a rate no browser population would produce. - Know your ceiling before you need it.
openssl s_time -connect host:443 -newmeasures your full-handshake rate and-reusemeasures the resumed rate. The gap between those two numbers is precisely the exposure this attack targets, and it is better measured on a quiet Tuesday than during an incident.
If instead your logs are full of plausible-looking requests and your CPU is in application code, you have a request flood rather than a handshake flood, and how to stop a DDoS attack is the page you want.
What to fix on the server
Each of these is worth doing. None of them is the answer.
Serve an ECDSA certificate. Swapping an RSA-2048 signature for ECDSA P-256 is the single largest change available to you — typically an order of magnitude more handshakes per core. Modern stacks let you configure both and serve whichever the client supports, so there is no compatibility argument left for RSA-only in 2026.
Trim the menu. Offer X25519 and P-256 and stop there. Remove large finite-field DH groups and any remaining RSA key-exchange suites. You cannot stop the attacker choosing the most expensive option you support; you can shorten the list.
Make resumption work for real visitors. Session tickets with sensible rotation, and the TLS 1.3 PSK path, mean your genuine traffic rarely pays for a full handshake. This does nothing to the attack and a great deal to your headroom while it is happening.
Disable client-initiated renegotiation, and prefer TLS 1.3. TLS 1.3 removed renegotiation entirely, which closes the THC-SSL-DOS shape outright. If you still terminate TLS 1.2 anywhere, confirm that client-initiated renegotiation is refused rather than assuming your distribution did it.
Shorten the handshake timeout. nginx's ssl_handshake_timeout defaults to 60 seconds, which is an invitation to hold a worker slot for a minute at no cost. A few seconds is plenty for a real browser.
Keep keep-alive on, and generous. A real visitor's handshake should pay for dozens of requests. Every connection you force a browser to re-establish is free capacity you hand to the attack.
Cap concurrent connections per source, with limit_conn or equivalent. This works against one noisy host and does almost nothing against three thousand residential addresses, for the reasons set out in what is a botnet and rate limiting.
Together these might multiply your survivable handshake rate by ten. The attacker multiplies theirs by renting a second botnet for the price of a lunch, and — this is the part no tuning touches — every one of your defences still runs after the packets have arrived on your uplink and after your CPU has looked at them. A cost you pay per attack packet is a cost the attacker gets to set.
Where a TLS handshake flood is actually stopped
Upstream of the thing holding the private key, cheapest decision first:
- TCP terminated away from your origin. A source that cannot complete a TCP handshake never reaches the TLS layer at all, which removes every spoofed packet from the problem before any cryptography is considered.
- Per-source connection-rate admission control. A cap on how fast a single source may start new handshakes, applied before the signature is performed rather than after — with visitors who have already proven themselves admitted ahead of the cap, so a flood cannot crowd out your regulars.
- Kernel-level drops for proven attackers. Once a source has been judged, its packets should cost a table lookup and nothing else. The whole game is the ratio between what a decision costs you and what it costs them.
- Aggregate handshake capacity. Many cores across several points of presence have a vastly higher combined crypto ceiling than one origin, and the flood divides across them instead of concentrating on you.
- Resumption and connection pooling at the edge. When real visitors resume at the nearest point of presence and the edge reaches your origin over a small pool of long-lived connections, your origin's handshake rate stops being a function of your visitor count — or of your attacker's.
- Only then, request-level defence. Challenges, behavioural signatures and rate limits all presuppose a request. They are the right tools for a Layer 7 flood and they arrive too late for a handshake that is abandoned before any request exists.
One prerequisite sits outside that list and silently voids all of it: your origin must be unreachable except through the protection network. If an attacker can still open a TCP connection to your server's real address, none of the above is in the path — see how to hide your origin IP address. The general shape of this argument, for every attack family rather than this one, is in what is DDoS mitigation.
How Itnetic stops TLS handshake floods
Itnetic's edge is designed around exactly the cost ordering above — push every decision as low as it will go, and let only the traffic that survives all of it be interesting.
| Layer | What it does |
|---|---|
| Packet filtering at the driver hook | An eBPF filter at XDP enforces protocol identity per flow and drops what is already disqualified, before a socket — let alone a handshake — is allocated |
| Kernel drop of proven attackers | Repeat offenders are held in an nftables set and dropped before any TLS handshake is paid for. A dropped packet costs us a lookup; a handshake would cost three asymmetric operations |
| TLS handshake admission control | Per-source connection-rate caps on the handshake itself, on every plan including the free one, with proven visitors admitted ahead of the cap |
| TCP and TLS terminated at the edge | Your origin never terminates a visitor's TLS, so its private key, its cores and its worker slots are not the resource under attack |
| Upstream volumetric absorption | Frankfurt sits behind X4B, a network with 500 Gbps of capacity; Prague runs on Eternyx behind its own network-level protection; Toronto and Singapore run on Linode (Akamai) behind Linode's. Padded and volumetric floods are soaked up before our edge — the current list is on the network page |
| Adaptive per-host detection | Navigation and connection rates are counted at the point of presence itself and flip a host into challenge mode in about a second, without a round trip to a control plane, and that verdict fans out to every other node so re-resolving to a quieter one does not help |
| Evidence | Per-request logs carry status, latency, TLS details, client fingerprint and the verdict applied, so the post-mortem is an export rather than a reconstruction |
Two commercial details matter as much as the packet path. Mitigated attack traffic is never metered against your bandwidth quota — including the certificate chains a handshake flood makes an edge transmit — so a week-long flood does not arrive as an invoice. And going live is two DNS records at your existing provider: no nameserver move, about five minutes, one hostname at a time if you want to watch it first. Itnetic's DDoS protection covers the rest of the pipeline, and every feature named above is on every plan, free included.
A seven-point checklist
- Measure your own ceiling:
openssl s_time -connect yourhost:443 -newversus-reuse, andopenssl speedon the TLS terminator. - Plot new connections per second next to bandwidth and requests, and alert on it. You cannot respond to a metric you do not graph.
- Serve an ECDSA certificate, keeping RSA only as a fallback if you still need one.
- Restrict offered groups and ciphersuites to cheap modern ones; delete large FFDHE groups and RSA key exchange.
- Verify session resumption is actually happening for real visitors by logging it, not by assuming it.
- Lower
ssl_handshake_timeoutto single-digit seconds and confirm client-initiated renegotiation is refused on any TLS 1.2 listener. - Firewall the origin to your protection network, so the handshake an attacker pays for is never one your server could have answered.
If this is happening right now, work through how to stop a DDoS attack rather than finishing this page. When it is over, how to test your DDoS protection covers how to confirm — safely and legally — that the next flood meets a filter instead of your private key.