There are two main ways that a TLS handshake can go: Full handshake, or resume.

There are two benefits to resumption:

  1. it can save a round trip between the client and server.
  2. it saves CPU cost of a public key operation.

Round trip

Saving a round trip is important for latency. Some websites don’t use a CDN, so a roundtrip could take a while. And even those on a CDN can be tens of milliseconds away. Maybe won’t matter much for a human, but roundtrips can kill the performance of something that needs to do sequential connections.

E.g. Australia is far away:

$ ping -c 1 -n www.treasury.gov.au
PING treasury.gov.au (3.104.80.4) 56(84) bytes of data.
64 bytes from 3.104.80.4: icmp_seq=1 ttl=39 time=369 ms

That’s about a third of a second. Certainly noticeable to a human. Especially since rendering a web page usually requires many connections to different hosts.

For TCP based web requests (in other words: not QUIC), there’s usually four roundtrips involved (slightly simplified):

  1. TCP connection establishment.
  2. ClientHello & ServerHello.
  3. Client & Server ChangeCipherSpec.
  4. HTTP request & response.

So from the UK to Australia, that’s about one full seconds, just setting up the connection. And then another third of a second for the request.

So that could be better.

CPU cost of public key operation

This can be a problem even if latency is low. Public key operations can take a lot of CPU. Especially because as key sizes grow, the cost grows superlinerarly.

$ openssl speed rsa2048 rsa4096
[…]
                  sign    verify    sign/s verify/s
rsa 2048 bits 0.000520s 0.000015s   1921.7  65540.4
rsa 4096 bits 0.003473s 0.000054s    287.9  18544.8

Doubling key size makes signatures (server) almost 7x as expensive, and verifications (client) 3.5x as expensive. (on my laptop. YMMV)

At scale, if you’re doing millions of QPS, this can add up. This is only 287 handshakes per second, per CPU. Not Web Scale™.

TLS before 1.3, and resumptions

The ClientHello sent by the client will contain a session ticket or session ID (the difference doesn’t matter here), basically saying “Hey, we’ve talked before. How about we skip some negotiation, and get right back to where we were?”. If the Server agrees, then that skips roundtrip 3.

This takes www.treasury.gov.au down to ~1s for any followup (resumed) connections, but it still takes 1.2s for the first request.

TLS 1.3 and round trips

TLS 1.3 shaves off a roundtrip to the initial request by optimistically making assumptions about what the server supports. That way it can say “Hello”, introducing which ciphers it supports, and select one, assuming that the server does, too.

In the normal case, this merges roundtrips 2&3, and now it’s only three roundtrips.

In our Australia example, this get the initial request down to ~1s as well, (except www.treasury.gov.au doesn’t support TLS 1.3).

TLS 1.3 and resumptions

Alright, so with resumptions, TLS brings us down to just two roundtrips, right? ~600ms total request time to Australia!

Not so fast. (heh)

The handshake is now:

  1. TCP connecting.
  2. Hello & optimistically choose cipher parameters.
  3. HTTP GET.

Which one would resuming remove? They’re all needed, and you can’t anything before you get a reply from the previous… or can you?

In the general case, no you can’t. But if you’re willing to sacrifice a specific bit of security, there’s something we can do. Specifically if the server is fine with being vulnerable to a session being replayed, then we send the GET without waiting for the handshake.

From the server’s point of view, it now sees “Hello, you and I are resuming, so please use the previous key, and here’s the encrypted payload”. If all looks good then the server can process the request, and return the reply. TLS calls this 0RTT, in that after the the TCP connection has been established, there’s only… uh… one RTT left. So 0RTT as in no additional roundtrip?

Sending the data early, like this, is called “early data”, and is new with TLS 1.3. It can’t be used on initial requests, since no session key has been negotiated yet.

TLS 1.3 early data and resumptions

Unlike previous versions, TLS 1.3 only saves a roundtrip if it has early data. An HTTP POST cannot be used for early data (because of the replay problem), so is the same number of roundtrips whether resumed or not.

So who supports the perfect setup?

For my Australia example I’m actually struggling a bit to find an Australian service that can fully optimize for latency.

  • www.treasury.gov.au doesn’t support TLS 1.3
  • queenslandtech.com.au doesn’t support early data
  • All others I’m randomly guessing use a CDN.
  • I’m too lazy to set up a VM there just to make the numbers bigger.

Oh well, we’re going to have to pretend, using a more nearby server. That means we’ll deal with tens of milliseconds instead of hundreds, though.

Measuring TLS handshakes times

I made a tool: tlshake.

TLS 1.3 with a request in early data

With a GET request in early data, TLS 1.3 resumes, and saves us a roundtrip. Note that the handshake time is approximately the same on both requests, but total time is very much shorter on the resumed request, since TLS and HTTP used the same roundtrip.

$ tlshake --http-get / www.example.com
Connection: initial
  Target:           www.example.com
  Endpoint:         www.example.com:443
  Connect time:     67.414ms
  Handshake time:   42.208ms
  Handshake kind:   Full
  Protocol version: TLSv1_3
  Cipher suite:     TLS13_AES_256_GCM_SHA384
  ALPN protocol:    None
  Early data:       Not attempted
  Request time:     95.782ms
  Reply first line: HTTP/1.1 200 OK
  Total time:       205.450ms

Connection: resume
  Target:           www.example.com
  Endpoint:         www.example.com:443
  Connect time:     57.422ms
  Handshake time:   39.769ms
  Handshake kind:   Resumed
  Protocol version: TLSv1_3
  Cipher suite:     TLS13_AES_256_GCM_SHA384
  ALPN protocol:    None
  Early data:       accepted
  Request time:     62.583ms
  Reply first line: HTTP/1.1 200 OK
  Total time:       159.804ms

Resumption without early data

We can resume the session without using early data. That saves the CPU usage for the handshake, but won’t save any roundtrip. Notice the very similar total time in this case:

$ tlshake --http-get / --disable-early-data www.example.com
Connection: initial
  Target:           www.example.com
  Endpoint:         www.example.com:443
  Connect time:     68.945ms
  Handshake time:   41.140ms
  Handshake kind:   Full
  Protocol version: TLSv1_3
  Cipher suite:     TLS13_AES_256_GCM_SHA384
  ALPN protocol:    None
  Early data:       Not attempted
  Request time:     89.317ms
  Reply first line: HTTP/1.1 200 OK
  Total time:       199.437ms

Connection: resume
  Target:           www.example.com
  Endpoint:         www.example.com:443
  Connect time:     66.183ms
  Handshake time:   39.028ms
  Handshake kind:   Resumed
  Protocol version: TLSv1_3
  Cipher suite:     TLS13_AES_256_GCM_SHA384
  ALPN protocol:    None
  Early data:       Not attempted
  Request time:     90.413ms
  Reply first line: HTTP/1.1 200 OK
  Total time:       195.653ms

Only the TLS handshake

If we only handshake, without sending a request, then it won’t try to resume at all (note that both requests are of the Full kind).

$ tlshake www.example.com
Connection: initial
  Target:           www.example.com
  Endpoint:         www.example.com:443
  Connect time:     66.218ms
  Handshake time:   43.683ms
  Handshake kind:   Full
  Protocol version: TLSv1_3
  Cipher suite:     TLS13_AES_256_GCM_SHA384
  ALPN protocol:    None
  Early data:       Not attempted
  Total time:       109.930ms

Connection: resume
  Target:           www.example.com
  Endpoint:         www.example.com:443
  Connect time:     66.416ms
  Handshake time:   41.626ms
  Handshake kind:   Full
  Protocol version: TLSv1_3
  Cipher suite:     TLS13_AES_256_GCM_SHA384
  ALPN protocol:    None
  Early data:       Not attempted
  Total time:       108.068ms

This seems to be because the server doesn’t provide the session ticket until it receives a request. It’s pretty clever. If a TLS session has been established, but no request was received, then the client should probably not try to reuse it. There’s a good chance that something went wrong, so we should redo the full handshake next time.

For comparison: TLS 1.2-only server

With TLS 1.2 a resumed handshake has one less roundtrip.

$ tlshake --http-get / www.treasury.gov.au
Connection: initial
  Target:           www.treasury.gov.au
  Endpoint:         www.treasury.gov.au:443
  Connect time:     330.905ms
  Handshake time:   617.084ms
  Handshake kind:   Full
  Protocol version: TLSv1_2
  Cipher suite:     TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
  ALPN protocol:    None
  Early data:       Not attempted
  Request time:     279.625ms
  Reply first line: HTTP/1.1 301 Moved Permanently
  Total time:       1227.643ms

Connection: resume
  Target:           www.treasury.gov.au
  Endpoint:         www.treasury.gov.au:443
  Connect time:     332.880ms
  Handshake time:   306.455ms
  Handshake kind:   Resumed
  Protocol version: TLSv1_2
  Cipher suite:     TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
  ALPN protocol:    None
  Early data:       Not attempted
  Request time:     308.770ms
  Reply first line: HTTP/1.1 301 Moved Permanently
  Total time:       948.132ms

Play around with TLS handshakes for your own service

Check out tlshake, and see if your TLS resumptions work as you expect, and with the expected number of roundtrips.

If you’re on Cloudflare, you need to enable “0-RTT Connection Resumption” for it to accept early data. It’s under “Speed -> Optimization” per domain.

More info