In my previous post, “gRPC Over HTTP/3,” we explored the potential of gRPC with HTTP/3. At that time, some of the pieces were missing and I had to hack on forks of a few repos to make gRPC+HTTP/3 work with Go. The biggest blocker was that the quic-go HTTP/3 implementation didn’t have support for HTTP trailers. But now things have recently changed there and these hacks are no longer needed!

quic-go now supports HTTP Trailers

If you recall, this was a major roadblock for getting gRPC to work over HTTP/3. Trailers are crucial for gRPC’s error handling and status codes, so this was a big deal. This works as of v0.47.0. Here are the related PRs:

As you can see, I’ve contributed most of the work for this!

go get -u github.com/quic-go/quic-go

This now enables HTTP/3 support for ConnectRPC for gRPC, gRPC-Web and Connect for both client and server. While I’d recommend deploying in production with a more established load balancer that supports HTTP/3, quic-go is perfect for experimentation and development. I’ll show you how to set up Go + HTTP/3 (via quic-go) + Connect a bit further down in this article.

Buf’s curl command has a new --http3 flag

That’s right, you can now easily test your gRPC services over HTTP/3 from the command line. This is a fantastic development for quick prototyping, debugging and having a simple tool to call gRPC services. You can use this as of v1.41.0. Here are the related PRs:

Upgrade to the new version today!

Open Source

I’m very happy to have contributed both of these features. Like I said in my previous post about this topic, the Go version of ConnectRPC seemed so close to having full HTTP/3 support in all three protocols: Connect/gRPC-Web and the original gRPC; it just needed trailer support to push gRPC over the finish line. And with other gRPC implementations, like grpc-dotnet, I hope the addition of HTTP/3 to buf curl command can be useful as well.

What does this mean for you?

In short, it means that if you’re working on a gRPC project, it’s now slightly more viable to use HTTP/3 today… in specific contexts. Here’s a recap of the benefits:

  • Faster connections, especially on unreliable mobile connections: HTTP/3’s connection setup is lightning-fast compared to HTTP/2, and it handles flaky networks like a champ. This is a major win for mobile apps and any situation where network conditions aren’t ideal. This can be useful when you’re using gRPC-Web or Connect on the frontend.
  • No more head-of-line blocking: HTTP/3 eliminates this pesky problem that can slow down HTTP/2 in certain scenarios. If your gRPC service handles lots of concurrent streams, you might see an improvement.

Trying it out with ConnectRPC

Here’s an example of starting a HTTP/3 server with ConnectRPC:

func main() {
	mux := http.NewServeMux()
	// Implementation is only in the full source
	mux.Handle(elizav1connect.NewElizaServiceHandler(&server{}))

	addr := "127.0.0.1:6660"
	log.Printf("Starting connectrpc on %s", addr)
	h3srv := http3.Server{
		Addr:    addr,
		Handler: mux,
	}
	if err := h3srv.ListenAndServeTLS("cert.crt", "cert.key"); err != nil {
		log.Fatalf("error: %s", err)
	}
}

This example uses the HTTP/3 server from quic-go to provide HTTP/3. Now you can test it using buf curl. Here’s an example of using buf curl with gRPC:

$ buf curl --http3 -k \
  --protocol=grpc \
  --schema=buf.build/connectrpc/eliza \
  -d '{"sentence":"Hello, with gRPC+h3"}' \
  https://127.0.0.1:6660/connectrpc.eliza.v1.ElizaService/Say
{
  "sentence": "Hello, with gRPC+h3"
}

With gRPC-Web:

$ buf curl --http3 -k \
  --protocol=grpcweb \
  --schema=buf.build/connectrpc/eliza \
  -d '{"sentence":"Hello, with gRPC-Web+h3"}' \
  https://127.0.0.1:6660/connectrpc.eliza.v1.ElizaService/Say
{
  "sentence": "Hello, with gRPC-Web+h3"
}

With Connect:

$ buf curl --http3 -k \
  --protocol=connect \
  --schema=buf.build/connectrpc/eliza \
  -d '{"sentence":"Hello, with Connect+h3"}' \
  https://127.0.0.1:6660/connectrpc.eliza.v1.ElizaService/Say
{
  "sentence": "Hello, with Connect+h3"
}

Note that if you don’t use the --http3 flag this doesn’t work. That’s because we’ve only started an HTTP/3 server. Using the following code, we can run HTTP/3 alongside HTTP/1.1 and HTTP/2:

func main() {
	mux := http.NewServeMux()
	mux.Handle(elizav1connect.NewElizaServiceHandler(&server{}))

	addr := "127.0.0.1:6660"
	log.Printf("Starting connectrpc on %s", addr)
	h3srv := http3.Server{
		Addr:    addr,
		Handler: mux,
	}

	srv := http.Server{
		Addr:    addr,
		Handler: h2c.NewHandler(mux, &http2.Server{}),
	}

	eg, _ := errgroup.WithContext(context.Background())
	eg.Go(func() error {
		return h3srv.ListenAndServeTLS("cert.crt", "cert.key")
	})
	eg.Go(func() error {
		return srv.ListenAndServeTLS("cert.crt", "cert.key")
	})
	if err := eg.Wait(); err != nil {
		log.Fatalf("error: %s", err)
	}
}

With this code, you can now connect using any version of HTTP and with gRPC, gRPC-Web or Connect.

$ buf curl -k --protocol=grpc \
  --schema=buf.build/connectrpc/eliza \
  -d '{"sentence":"Hello, with gRPC+h2"}' \
  https://127.0.0.1:6660/connectrpc.eliza.v1.ElizaService/Say
{
  "sentence": "Hello, with gRPC+h2"
}

The compatibility matrix is now all green when using ConnectRPC, with only a single exception (because HTTP/1.1 servers and clients typically don’t support HTTP trailers):

ProtocolHTTP/1.1HTTP/2HTTP/3
gRPC
gRPC-Web
Connect

See the repo at sudorandom/example-connect-http3 to see the full examples shown here as well as some example client code.

So everything is fast with this, right?

Well, no. HTTP/3 isn’t always a performance win… and actually, today, it may generally be slower or, at best, the same speed as HTTP/2. Part of the cause is that it uses a lot of CPU cycles compared to HTTP/1.1 and HTTP/2. You might be asking: “this awesome protocol that is supposed to make things fast is actually slower? What’s the point?”. This is a good question that’s been answered many times.

QUIC is still mostly implemented in user-space and is lacking the half-century of optimizations that TCP has had. I recently saw this paper which looks to be some decent data regarding actual HTTP/3 performance. Generally, it’s not a good story for QUIC or HTTP/3.

Just for completeness, here are some other testimonies of the performance of HTTP/3 and QUIC:

The results are mixed, but it generally indicates that the receiver end needs more optimizations. Specifically, the proposed solutions involve a technique called UDP generic receive offload (UDP GRO). Some experiments with these kinds of optimizations have shown very promising results. And you do have to remember that the number of round trips to establish a connection being reduced is, at its conceptual level, a winning strategy. The only thing stopping world domination is the pesky details.

 

However, there’s reason for optimism

The tooling surrounding HTTP/3 is rapidly maturing, which is significantly lowering the barrier to entry for developers eager to experiment and adopt this technology early on. With libraries like quic-go now offering comprehensive support for essential features like HTTP Trailers and tools like buf curl providing seamless testing capabilities, the path to integrating HTTP/3 into your gRPC projects is smoother than ever.

Additionally, performance optimizations are actively being researched and implemented. Beyond tooling, the performance landscape for HTTP/3 is far from stagnant. Active research and development are focused on optimizing QUIC implementations, particularly on the receiver side. Promising techniques like UDP Generic Receive Offload (GRO) show the potential to significantly enhance HTTP/3’s efficiency and responsiveness.

Even today, HTTP/3 and QUIC have their niches that are pretty compelling. Specifically, HTTP/3 consistently does pretty well with reducing the number of pauses with video conferencing and video streaming while improving general web usage with slow/unstable networks, typically with mobile devices.

Thanks all

With quic-go’s new support for HTTP trailers and buf curl’s new HTTP/3 flag, experimenting with gRPC over HTTP/3 is now easier than ever. I challenge you to try out gRPC over HTTP/3 in your own projects and share your experiences. I want to help build a community pushing gRPC and protobufs into more places, and this is a small part of that.