Using OpenSSL’s TLS 1.3 0‑RTT to Cut Handshake Latency – What Engineers Need to Know
Learn how to enable and safely use OpenSSL’s SSL_write_early_data API, the replay‑protection mechanisms it provides, and the trade‑offs you must evaluate before deploying 0‑RTT in production.
23 Jan 2026, 06:01 UTC

Problem: Handshake latency adds noticeable delay for short‑lived connections
When a client repeatedly opens TLS connections to the same service, each full handshake consumes round‑trips that can add 30‑50 ms on a typical WAN. For latency‑sensitive workloads (e.g., API calls, micro‑service chats) this overhead becomes a measurable performance bottleneck.
Thesis: OpenSSL’s TLS 1.3 0‑RTT lets you send application data immediately after the ClientHello, but you must weigh the latency gain against replay and forward‑secrecy risks.
How OpenSSL exposes 0‑RTT
Starting with OpenSSL 1.1.1, the library provides two key APIs:
SSL_CTX_set_early_data_enabled(ctx, 1)– enables the early‑data extension on a client or server context.SSL_write_early_data(ssl, buf, len, &written)– attempts to write application data before the handshake finishes.
OpenSSL automatically checks that the server advertised the early_data extension and that the session ticket’s age is within the configured replay window. If those conditions fail, the call returns 0 or a negative value.
Worked example: a minimal client that uses early data
The following steps illustrate how to compile, run, and verify early‑data transmission. Replace placeholders with your own paths and values.
Build OpenSSL 3.0+ with TLS 1.3 enabled (the defaults already include it):
# ./config enable-tls1_3 # make # sudo make install # installs to /usr/local/ssl by defaultCreate a simple C client (
early_client.c):#include #include #include #include #include int main(int argc, char **argv) { const char *host = argv[1]; const char *port = argv[2]; const char *early_data = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"; SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); if (!ctx) { fprintf(stderr, "SSL_CTX_new failed\n"); return 1; } /* Enable early data */ SSL_CTX_set_early_data_enabled(ctx, 1); /* Optional: set a callback to inspect/reject early data */ SSL_CTX_set_early_data_cb(ctx, [](SSL *s, void *arg) -> int { /* Return 1 to allow, 0 to reject */ return 1; }, NULL); SSL *ssl = SSL_new(ctx); if (!ssl) { fprintf(stderr, "SSL_new failed\n"); SSL_CTX_free(ctx); return 1; } /* Connect – use BIO for simplicity */ BIO *bio = BIO_new_ssl_connect(ctx); BIO_get_ssl(bio, &ssl); char hostport[256]; snprintf(hostport, sizeof(hostport), "%s:%s", host, port); BIO_set_conn_hostname(bio, hostport); if (BIO_do_connect(bio) <= 0) { fprintf(stderr, "Connection failed\n"); ERR_print_errors_fp(stderr); goto end; } if (SSL_do_handshake(ssl) <= 0) { fprintf(stderr, "Handshake failed\n"); ERR_print_errors_fp(stderr); goto end; } /* Attempt to write early data */ size_t written = 0; int ret = SSL_write_early_data(ssl, early_data, strlen(early_data), &written); if (ret > 0) { printf("Early data accepted, %zu bytes sent\n", written); } else if (ret == 0) { printf("Server rejected early data\n"); } else { printf("SSL_write_early_data error\n"); ERR_print_errors_fp(stderr); } /* Continue with normal request/response handling … */ /* (omitted for brevity) */ end: SSL_free(ssl); SSL_CTX_free(ctx); return 0; }Compile the client (adjust include/library paths if OpenSSL is not in system dirs):
gcc -Wall -O2 early_client.c -o early_client -lssl -lcryptoRun a test server that advertises early data (using OpenSSL’s
s_server):# ./apps/openssl s_server -accept 4433 -cert cert.pem -key key.pem -early_data -tls1_3Execute the client against the server:
./early_client 127.0.0.1 4433Verify early‑data transmission:
- Check the program’s output – a line like “Early data accepted, XX bytes sent” indicates OpenSSL sent the data before receiving the server’s Finished message.
- Optionally capture traffic with Wireshark or
tcpdump -i lo port 4433 -w early.pcapand look for a TLS Application Data record immediately following the ClientHello, before the server’s Handshake (Finished) record.
Test replay protection:
- Capture the early‑data blob from the pcap.
- Wait until the ticket age exceeds the server’s replay window (you can adjust the window with
-early_dataand-ticket_key_fileoptions). - Re‑send the captured blob using a tool like
tcpreplayor a custom client that callsSSL_write_early_dataagain. - Observe that the server returns 0 or a negative value, indicating rejection.
Trade‑offs and limitations
- No forward secrecy for 0‑RTT data. If an attacker compromises the server’s ticket‑encryption key, they can decrypt any early data sent during the ticket’s lifetime. Rotating ticket keys frequently (e.g., every few hours) limits exposure.
- Replay window. OpenSSL will reject early data that arrives after the ticket age exceeds the configured limit, but it cannot prevent replays within that window. Application‑level idempotence or anti‑replay tokens (nonces, timestamps) are required for safety.
- Safe operations only. The OpenSSL documentation recommends limiting 0‑RTT to requests that have no side‑effects (e.g., GET requests that retrieve static data). POST, PUT, or any request that modifies state should avoid early data unless you add your own replay protection.
- Performance gain is conditional. The 30‑50 ms reduction assumes a functioning ticket‑key rotation policy and a network where the extra round‑trip of a full handshake is noticeable. On a LAN or with already‑cached sessions, the benefit may be marginal.
Actionable checklist for engineers
- Confirm that your service’s requests are idempotent or can tolerate replay.
- Enable early data on the client side with
SSL_CTX_set_early_data_enabledand handle the return value ofSSL_write_early_data. - On the server, enforce a short ticket lifetime and rotate ticket keys regularly (OpenSSL’s
SSL_CTX_set_tlsext_ticket_key_cblets you provide a custom key‑rotation callback). - If you cannot guarantee idempotence, add an application‑level nonce or timestamp and reject early data that repeats a seen value.
- Measure latency before and after enabling 0‑RTT in a staging environment that mimics your production network.
- Monitor ticket‑key usage and replay‑window events via OpenSSL’s logging or custom callbacks to detect anomalies.
Conclusion
OpenSSL’s TLS 1.3 0‑RTT implementation gives you a concrete way to shave tens of milliseconds off connection setup, but the safety of that gain hinges on proper ticket‑key management and application‑level replay defenses. By following the steps above and weighing the trade‑offs, you can decide whether 0‑RTT is a worthwhile optimization for your specific workload.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.