HTTP/3, by ditching TCP’s head-of-line blocking, can surprisingly make your gRPC calls more predictable, not just faster.

Let’s see this in action. Imagine a simple gRPC service that echoes back whatever you send it.

package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/alts" // For QUIC/HTTP/3 support
	pb "your_module_path/echo" // Assume this is your generated protobuf code
)

type server struct {
	pb.UnimplementedEchoServer
}

func (s *server) Echo(ctx context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) {
	log.Printf("Received: %v", in.GetMessage())
	return &pb.EchoResponse{Message: in.GetMessage()}, nil
}

func main() {
	// Use ALTS credentials to enable QUIC
	altsOpts := &alts.TransportSecurityOptions{
		// Configure TLS certificates here if needed for production
		// For local testing, default might be sufficient or require self-signed certs
	}
	quicOpts, err := alts.NewTransportSecurity(altsOpts)
	if err != nil {
		log.Fatalf("failed to create ALTS transport security: %v", err)
	}

	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// Wrap the listener with QUIC support
	quicLis := grpc.NewQuicListener(lis, quicOpts)

	s := grpc.NewServer()
	pb.RegisterEchoServer(s, &server{})

	log.Printf("server listening at %v with HTTP/3", quicLis.Addr())
	if err := s.Serve(quicLis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

And a client to call it:

package main

import (
	"context"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/alts" // For QUIC/HTTP/3 support
	pb "your_module_path/echo" // Assume this is your generated protobuf code
)

func main() {
	// Use ALTS credentials to enable QUIC
	altsOpts := &alts.TransportSecurityOptions{
		// Configure TLS certificates here if needed for production
	}
	quicTransport, err := alts.NewClientTransportCredentials(altsOpts)
	if err != nil {
		log.Fatalf("failed to create ALTS client transport credentials: %v", err)
	}

	// Connect to the gRPC server over HTTP/3
	conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(quicTransport))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewEchoClient(conn)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	r, err := c.Echo(ctx, &pb.EchoRequest{Message: "Hello, HTTP/3 gRPC!"})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

This setup uses google.golang.org/grpc/credentials/alts to enable QUIC, which is the transport layer for HTTP/3. When you run these, you’re not just getting faster throughput; you’re fundamentally changing how data packets travel.

The core problem gRPC (and HTTP/2 before it) on TCP faced was head-of-line (HOL) blocking. Imagine a TCP connection as a single pipe. If one packet in the middle of a stream gets lost, all subsequent packets for all multiplexed streams on that connection have to wait for that lost packet to be retransmitted, even if they’re destined for completely different gRPC services. This leads to unpredictable latency spikes.

HTTP/3, built on QUIC, solves this by having per-stream reliability. QUIC is a UDP-based protocol that provides its own mechanisms for connection establishment, flow control, and loss recovery. Crucially, loss recovery in QUIC is stream-specific. If a packet for stream A is lost, it only blocks stream A; streams B, C, and D can continue to make progress. This is the magic that makes gRPC over HTTP/3 more resilient to network jitter and packet loss, leading to more consistent, lower latency.

The alts package in Go’s gRPC library acts as a bridge. It allows gRPC to leverage the underlying QUIC transport provided by packages like quic-go (which alts often uses implicitly or explicitly). For the server, you wrap your standard net.Listener with grpc.NewQuicListener. For the client, you use alts.NewClientTransportCredentials to configure the dial options.

The most counterintuitive aspect is that even if your network is perfectly reliable, the shift from TCP’s strict ordering to QUIC’s stream-based ordering still yields benefits. The reduction in context switching and the simplified state management within QUIC itself can lead to lower overhead and more efficient use of network resources, resulting in those surprising latency improvements and more stable performance profiles.

The next hurdle you’ll likely encounter is managing TLS certificates for production deployments, as plain QUIC is rarely used outside of development.

Want structured learning?

Take the full Http3 course →