Introduction

For low-latency applications, every millisecond counts. One common source of unnecessary latency is the repeated TLS handshake process when making API requests. If your application does not reuse TLS sessions, it incurs extra latency and computational overhead for every new connection.

To mitigate this, we strongly recommend implementing TLS session reuse and connection pooling. This guide explains these concepts and provides examples in Java, Python, and Node.js.


Understanding TLS Session Reuse

What Happens Without TLS Session Reuse?

  1. Your client initiates a connection to our API.
  2. A full TLS handshake occurs, which requires multiple round-trips.
  3. The handshake establishes a secure connection, but if not reused, a new handshake will be required for subsequent connections.

How TLS Session Reuse Helps

  • Reduces handshake latency: By reusing an existing TLS session, your client avoids repeating the costly handshake process.
  • Lowers CPU and memory usage: Each handshake requires cryptographic operations that consume resources.
  • Improves request throughput: Persistent connections reduce the overhead of setting up new TLS sessions.

To achieve this, use connection pooling and enable persistent connections in your HTTP client.


Debugging TLS Handshake Time with Curl

To check your API request's TLS handshake time, use:

curl -o /dev/null -s -w "
      DNS Lookup:        %{time_namelookup}s
      TCP Connect:       %{time_connect}s
      TLS Handshake:     %{time_appconnect}s
      Pre-Transfer:      %{time_pretransfer}s
      Start Transfer:    %{time_starttransfer}s
      Total Time:        %{time_total}s
  " https://api.forter-secure.com 

This will output:

DNS Lookup:        0.388898s
TCP Connect:       0.529150s
TLS Handshake:     0.975320s
Pre-Transfer:      0.975733s
Start Transfer:    1.119405s
Total Time:        1.120284s

TLS Handshake indicates how long it took for the TLS handshake to happen. Notice at the above request it took ~450 milliseconds. If TLS Handshake is consistently high, your client might not be reusing TLS sessions.


Implementing TLS Session Reuse

Java Example (Using Apache HttpClient)

package org.forter.tlsreuseexample;

import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.TimeValue;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.HexFormat;

public class Main {
    public static void main(String[] args) throws Exception {
        HttpGet request = new HttpGet("https://api.forter-secure.com");
        var clientContext = HttpClientContext.create();

        System.out.println("====== Using 0 keep alive w/o connection reuse configuration ========");
        try (CloseableHttpClient client = HttpClients.custom()
            .setConnectionReuseStrategy((request1, response, context) -> false)
            .setKeepAliveStrategy((response, context) -> TimeValue.ofMilliseconds(0))
            .build()) {
            executeRequest(request, client, clientContext);
        }

        System.out.println("====== Using regular pool configuration ========");
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            executeRequest(request, client, clientContext);
        }
    }

    private static void executeRequest(HttpGet request, CloseableHttpClient client, HttpClientContext clientContext) throws URISyntaxException, IOException, InterruptedException {
        System.out.println("Sending request to " + request.getUri());
        client.execute(request, clientContext, response -> consumeResponse(response, clientContext));
        Thread.sleep(100);
        System.out.println("=====================\n");

        System.out.println("Sending request to " + request.getUri());
        client.execute(request, clientContext, response -> consumeResponse(response, clientContext));
        System.out.println("=====================\n");
    }

    private static Object consumeResponse(ClassicHttpResponse response, HttpClientContext clientContext) throws IOException {
        EntityUtils.consume(response.getEntity());
        var sslSession = clientContext.getSSLSession();
        sslSession.invalidate();

        String sessionIdHex = HexFormat.of().formatHex(sslSession.getId());

        System.out.println("SSL Session Id: " + sessionIdHex);
        response.close();
        return null;
    }
}
  • Uses connection pooling to maintain persistent connections.
  • Reuses TLS sessions to minimize handshake overhead.

Notice the output from the above program looks like this, you can see that the SSL session id is the same on the 2nd run, where default pool configuration is used, which uses keepAlive and connection reuse.

====== Using 0 keep alive w/o connection reuse configuration ========
Sending request to https://api.forter-secure.com/
SSL Session Id: 7f60fd646dd1239a9210ad844c4a3e81317e3780dc9b39aaeffd3160c69d99c6
=====================

Sending request to https://api.forter-secure.com/
SSL Session Id: 8df20ed84d6128dd560ccc4abbcd58fb1185198fcae5e23b979d57cba64ca92e
=====================

====== Using regular pool configuration ========
Sending request to https://api.forter-secure.com/
SSL Session Id: 48ab1d57adb15c810cac201d9576475b29bb0f8875838c1de0c94e53e740fe6f
=====================

Sending request to https://api.forter-secure.com/
SSL Session Id: 48ab1d57adb15c810cac201d9576475b29bb0f8875838c1de0c94e53e740fe6f
=====================

Python Example (Using Requests Session)

Note that when using Python Requests library “Session”, it automatically does KeepAlive and reuses the http connection.

import requests

session = requests.Session()
session.get("https://api.forter-secure.com")

# TLS session reuse will happen here
session.get("https://api.forter-secure.com")

Node.js Example (Using HTTPS Agent)

const https = require("https");

const agent = new https.Agent({
  keepAlive: true, // Enables connection reuse
  maxSockets: 10,  // Limits parallel connections
  maxFreeSockets: 5,
});

https.get("https://api.forter-secure.com", { agent }, (res) => {
  console.log(`Status Code: ${res.statusCode}`);
});

// TLS session reuse will happen here as the agent is reused
https.get("https://api.forter-secure.com", { agent }, (res) => {
  console.log(`Status Code: ${res.statusCode}`);
});
  • Enables keepAlive to reuse TLS sessions across requests.
  • Limits open sockets to manage resource consumption effectively.

Conclusion

Implementing TLS session reuse and connection pooling significantly improves API performance by reducing handshake latency and optimizing resource usage. Use the provided examples to ensure your application efficiently reuses connections and minimizes unnecessary overhead.