Reducing Network Latency with Java’s HTTP/3 Client: What’s New in Java 24

Let’s discuss Java’s improvements for handling modern web communications. HTTP3 (or HTTP/3) was designed to enable faster web communications. This is an improvement on the currently prevalent HTTP2 and the older HTTP1. 

Java 24 and OpenJDK introduce  HTTP/3 for the HTTP Client API via JEP 517

A Primer on  HTTP3  

HTTP3 is the latest advancement to the Hypertext Transfer Protocol, which is used to exchange information on the World Wide Web. HTTP/3 uses the same semantics as its predecessors, such as request/response/session/status codes, but encodes them differently and maintains session state differently. 

The earlier HTTP/2 and HTTP/1 assumed a stateful network connection beneath server and client, (TCP/IP), which provided for guaranteed data packet delivery and promised that the packets will be in order (queued). But this also led to latency and requeuing if one of the packets in the queue was lost. 

Why is HTTP/3 Faster Than HTTP/2? 

HTTP3 is faster than HTTP2, as it replaced the slower TCP protocol in the TLS network layer with the faster QUIC protocol. 

QUIC is pronounced as “KWIK”. It was first designed at Google labs, and as of now most browsers, such as Chrome, Microsoft Edge, Firefox and Safari, support it. 

What is HTTP3’s Underlying QUIC Protocol? 

QUIC improves performance of connection or session-oriented web applications by establishing a number of multiplexed connections between two endpoints. It uses the User Datagram Protocol (UDP), and is designed to obsolete TCP at the transport layer for many applications.

Those who have studied Computer Networking would recollect that UDP used to be the faster protocol. However, it was not used much, as it was connectionless and lacked reliability, error detection and correction, and flow control. It had no order of delivery guarantees. 

There have been many improvements/augmentations to the UDP protocol, and it is now considered suitable for web communications. 

How QUIC Improves Upon and Replaces TCP in the TLS layer 

In HTTP2, if we open multiple data flow streams over a TCP connection, and one of the data packets is delayed for some reason, then all the following TCP data packets are also delayed in the queue. They are lost if the connection times out. 

But in QUIC, as the improved UDP protocol is used, multiple streams of data packets reach all the endpoints independently. So even if a data packet is delayed in one of the many streams, it would reach the destination through some other stream. The underlying mechanisms of integrating the data packets is too involved to be discussed here.

QUIC manages the multiplexed connection efficiently to make it transparent to the user/browser. QUIC also reduces network latency and performs  bandwidth estimation in each direction to avoid congestion.

Several Java implementations of the QUIC protocol or HTTP/3 are available, such as KWIK, Quiche4j, Flupke, Jetty etc. 

Why Use Jetty-based Client/Server Classes? 

While most browsers already have HTTP/3 support, some web servers still do not support HTTP3 as of writing this article.

Jetty is one of the webservers that has implemented both the client and server sides of the HTTP3 protocol, i.e. Jetty server supports HTTP/3. Jetty’s implementation of HTTP/3 involves dealing with network-level client sockets and server sockets, which can make our code lengthy and difficult to understand.

So we use Kwik , a wrapper around the QUIC protocol. We can also use Jetty (v12+),  which provides HTTP/3 support. 

What are Kwik and Flupke? 

Instead of going through the network layer implementation of the QUIC protocol with Client and Server sockets, we can use a wrapper built in pure Java.

KWIK is a client and server implementation of the QUIC protocol (RFC 9000) in pure Java. 

Flupke is a Java HTTP3 implementation that runs on top of Kwik, which can further simplify things for us. 

Let’s write our first program using the traditional HTTP/2 connections, and check its performance. 

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;

public class AnachronisticHttpBenchmark {
    public static void main(String[] args) throws Exception {
        List<String> urls = List.of(            "https://reutersinstitute.politics.ox.ac.uk/digital-news-report/2024/dnr-executive-summary",
"https://www.nytimes.com/2024/12/26/briefing/the-year-in-news.html",
"https://www.heraldnet.com/news/the-top-10-most-read-herald-stories-of-2024", "https://www.cbsnews.com/news/top-news-headlines-of-2024-month-by-month",
"https://www.cfr.org/article/ten-most-significant-world-events-2024"
        );

        HttpClient client = HttpClient.newBuilder()                                      .followRedirects(HttpClient.Redirect.NORMAL)                                      .connectTimeout(Duration.ofSeconds(10))
                                      .build();

        for (String url : urls) {
            HttpRequest request = HttpRequest.newBuilder()
                                            .uri(URI.create(url))
                                            .GET()
                                            .build();

            long startTime = System.nanoTime();
            HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
            long endTime = System.nanoTime();

            long durationMillis = (endTime - startTime) / 1_000_000;
            int responseSizeBytes = response.body().length;

            System.out.printf("URL: %s%n", url);
            System.out.printf("Status: %d%n", response.statusCode());
            System.out.printf("Response Time: %d ms%n", durationMillis);
            System.out.printf("Response Size: %d bytes%n", responseSizeBytes);
            System.out.println("=".repeat(60));
        }
    }
}

Next, let’s repeat the same with an HTTP/3 client , and then we will measure the performance of both. 

import tech.kwik.flupke.Http3Client;
import tech.kwik.flupke.Http3ClientBuilder;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;

public class Sample {
    public static void main(String[] args) throws IOException, InterruptedException {

        List<String> urls = List.of(             "https://reutersinstitute.politics.ox.ac.uk/digital-news-report/2024/dnr-executive-summary",        "https://www.nytimes.com/2024/12/26/briefing/the-year-in-news.html",        "https://www.heraldnet.com/news/the-top-10-most-read-herald-stories-of-2024/","https://www.cbsnews.com/news/top-news-headlines-of-2024-month-by-month","https://www.cfr.org/article/ten-most-significant-world-events-2024"
)

        try {
            for (String url : urls) {
                HttpRequest request = HttpRequest.newBuilder()
                        .uri(URI.create(url))
                        .header("User-Agent", "Flupke http3 library")
                        .timeout(Duration.ofSeconds(1000))
                        .build();

                // Easiest way to create a client with default configuration
                HttpClient defaultClient = Http3Client.newHttpClient();
                HttpClient client = ((Http3ClientBuilder) Http3Client.newBuilder())
                        .connectTimeout(Duration.ofSeconds(1000))
                        .build();

                long start = System.currentTimeMillis();
                HttpResponse<String> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofString());
                long end = System.currentTimeMillis();
                int responseSizeBytes = httpResponse.body().length();
                reportResult(httpResponse, end - start);
            }
        }
        catch (IOException e) {
            System.err.println("Request failed: " + e.getMessage());
        }
        catch (InterruptedException e) {
            System.err.println("Request interrupted: " + e.getMessage());
        }
    }

    private static void reportResult(HttpResponse<String> httpResponse, long duration) throws IOException {
        //System.out.println("Got HTTP response " + httpResponse); - to check the raw response of you prefer
        System.out.printf("Status: %d%n", httpResponse.statusCode());
        System.out.printf("Response Size: %d bytes%n", responseSizeBytes);
System.out.println("Request completed in " + duration + " ms");
        System.out.println("=".repeat(60)); 
    }
}
//long downloadSpeed = httpResponse.body().length() / duration;

Although we just use a few URLs, when the programs are run, you can see a marked difference in their response times. The program using HTTP/3 client fetches responses quicker every time. 

Fig: Comparison of Response Times

You can see above that the LatestHTTPBenchmark class fetches the responses in a lesser number of milliseconds almost every time.

These were just a couple of webpage requests. Just imagine the performance improvement gained when you code a whole web application with HTTP/3 sockets. 

If your server also supports HTTP/3, the performance gains and the reduction in network latency would be much higher.

Your results may vary a bit, depending on your network speed and hardware setup, but the overall trend should be similar to what we observed. 

We also checked the network performance of these two classes using a 360-degree snapshot captured by the yc-script and analyzed by our yCrash tool.

The class using simple HTTP connections uses a larger number of TCP/IP connections, and many TCP/IP connections are in the WAIT state, because some packets were lost in the queue. 

Fig: Network Report: Simple HTTP Connection

In contrast, the class using HTTP/3 creates a lesser number of TCP/IP connections, mostly relies on faster UDP connections, and has just a single thread in the WAIT state. 

Fig: Network Report: HTTP3 Connection

Source:https://ycrash.io/yc-report-netStat.jsp?ou=VHlhRkI1RW1rWlhoeXNsb3o4TzdaUT09&de=host&app=yc&ts=2025-06-07T14-58-54

HTTP/3 Versus HTTP/2, HTTP/1: Conclusions

HTTP/3 not only uses less network resources, but works with faster UDP network connections. A lower number of connections are in the WAIT state. 

This suggests that network jobs are being done quicker, leading to reduced network latency.

One thought on “Reducing Network Latency with Java’s HTTP/3 Client: What’s New in Java 24

Add yours

Share your Thoughts!

Up ↑

Index

Discover more from yCrash

Subscribe now to keep reading and get access to the full archive.

Continue reading