Introduction
HTTP/3 is the latest major revision of the Hypertext Transfer Protocol, built on top of QUIC (a UDP-based transport protocol) and designed to make web communication faster, more secure, and more resilient to network changes. It eliminates the TCP bottlenecks of previous versions, supports multiplexed streams without head-of-line blocking, and integrates TLS 1.3 by default.
This article series focuses on implementing HTTP/3 using the Golang programming language. Instead of diving into a long theoretical explanation here, I’ll refer you to some excellent resources that explain the protocol’s background, benefits, and evolution:
- Speeding Through the Web: Understanding HTTP 1.1, HTTP/2, and HTTP/3 — A clear comparison of all three major HTTP versions.
- Web Is Becoming HTTP/3, and Here’s What You Should Know About — A concise, beginner-friendly introduction to HTTP/3.
- HTTP/3: Your Guide to the Next Internet — A more technical explanation of why HTTP/3 exists and how QUIC changes the game.
📜 Official Specifications & Technical References
- RFC 9114: HTTP/3 — The official IETF specification for HTTP/3.
- RFC 9000: QUIC Transport Protocol — The base transport protocol for HTTP/3.
- RFC 9001: Using TLS to Secure QUIC — How TLS 1.3 is integrated into QUIC.
- W3C HTTP Working Group — Maintains and evolves HTTP specifications, including HTTP/3.
In this first step, we’ll set up the foundation for working with HTTP/3 in Go — preparing our environment, choosing the right packages, and running a minimal server that speaks the new protocol.
Setting Up Your First HTTP/3 Server in Go
Before we write any code, let’s understand the key points of our setup:
- Golang version — Go 1.19+ is recommended (I’ll use Go 1.22 in examples).
- HTTP/3 library — We’ll use
github.com/quic-go/quic-go/http3, the maintained HTTP/3 implementation for Go. - TLS requirement — QUIC (and therefore HTTP/3) always uses TLS 1.3, so we must have a certificate, even for local development.
- UDP — HTTP/3 runs over UDP instead of TCP, so your firewall must allow UDP traffic on the chosen port.
— — — — — — — — — — — —
Step 1 — Install Go and Create a Project
Make sure Go is installed:
go version If you don’t have it, download and install Go.
Create a new project:
mkdir h3-demo && cd h3-demo go mod init example.com/h3demo
Step 2 — Install HTTP/3 package
go get github.com/quic-go/quic-go/http3
Step 3 — Create a Self-Signed Certificate
For local testing, you can generate a certificate with OpenSSL:
mkdir cert openssl req -x509 -newkey rsa:2048 -nodes \ -subj "/CN=localhost" \ -keyout cert/key.pem -out cert/cert.pem -days 365
Step 4 — Minimal HTTP/3 Server in Go
Create main.go:
package main import ( "fmt" "log" "net/http" "github.com/quic-go/quic-go/http3" ) func main() { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello from HTTP/3! You requested %s via %s\n", r.URL.Path, r.Proto) }) addr := ":4433" log.Printf("Starting HTTP/3 server at https://localhost%v", addr) if err := http3.ListenAndServeTLS(addr, "cert/cert.pem", "cert/key.pem", mux); err != nil { log.Fatal(err) } } Run it:
go run . You should see output similar to:
2025/08/15 14:42:51 Starting HTTP/3 server at https://localhost:4433
Step 5 — Test with curl (macOS)
macOS’s built-in curl may not support HTTP/3. Install the Homebrew version:
brew install curl After successful installation, you should update the PATH environment, too:
export PATH="/usr/local/opt/curl/bin:$PATH" Check:
curl --version | grep -i http3 Request:
curl --http3-only -k https://localhost:4433/ Expected output:
Hello from HTTP/3! You requested / via HTTP/3
✅ You now have a working HTTP/3 server in Go using the modern quic-go package, running locally over QUIC and TLS 1.3.
Let’s add a tiny HTTP/3 Go client that your readers can run from the terminal and see end-to-end traffic hit the server you built.
Building an HTTP/3 Client in Go
Step 1 — Add the dependency
(You already have this from the server section; listed again for completeness.)
go get github.com/quic-go/quic-go
Step 2 — Create client.go
package main import ( "crypto/tls" "fmt" "io" "log" "net/http" "time" "github.com/quic-go/quic-go/http3" ) func main() { // URL of the HTTP/3 server url := "https://localhost:4433/" // Create an HTTP/3 transport (QUIC) transport := &http3.RoundTripper{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, // For local dev/self-signed certs MinVersion: tls.VersionTLS13, }, } defer transport.Close() // Create HTTP client using the HTTP/3 transport client := &http.Client{ Transport: transport, Timeout: 10 * time.Second, } // Send a GET request start := time.Now() resp, err := client.Get(url) if err != nil { log.Fatal(err) } defer resp.Body.Close() // Read response body body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } elapsed := time.Since(start) // Output results fmt.Printf("Status : %s\n", resp.Status) fmt.Printf("Protocol : %s\n", resp.Proto) // Should be "HTTP/3" fmt.Printf("Elapsed : %v\n", elapsed) fmt.Println("--------- Body ---------") fmt.Print(string(body)) }
Step 3 — Run it
If you used the simple server on :4433: (in one terminal)
go run . # from your server project (serving on :4433) In another terminal:
go run client.go Example Output:
Status : 200 OK Protocol : HTTP/3 Elapsed : 6.3ms --------- Body --------- Hello from HTTP/3!
Notes & nice extras
-
resp.ProtoconfirmingHTTP/3is the quickest sanity check that QUIC was used. - Want to demo multiplexing? Launch multiple clients concurrently (e.g.,
xargs -P 10 -I{} sh -c 'go run client.go -url https://localhost:4433/ -k >/dev/null' <<< "$(yes | head -n 50)") and watch your server’s logs handle parallel streams smoothly.
In the next step, we’ll extend this setup to also serve HTTP/1.1 and HTTP/2 in parallel, so you can compare protocols in one run.
🎁 Bonus: Serving HTTP/1.1, HTTP/2, and HTTP/3 Together in Go
In many real-world deployments, HTTP/3 is introduced alongside HTTP/1.1 and HTTP/2 to ensure compatibility with clients and networks that don’t yet support QUIC.
We can run a single Go app that serves:
- TCP (TLS) → HTTP/1.1 & HTTP/2
- UDP (QUIC) → HTTP/3
Step 1— Update the Server Code
Update main.go:
package main import ( "crypto/tls" "fmt" "log" "net" "net/http" "github.com/quic-go/quic-go/http3" // HTTP/3 server implementation using QUIC ) func main() { // Create a multiplexer to handle HTTP routes mux := http.NewServeMux() // Simple handler: returns a message including the HTTP protocol used mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello from %s!\n", r.Proto) // r.Proto: HTTP/1.1, HTTP/2, or HTTP/3 }) // Paths to your TLS certificate and key files certFile := "cert/cert.pem" keyFile := "cert/key.pem" // ----- HTTP/1.1 & HTTP/2 over TCP ----- // Go's standard library automatically supports HTTP/1.1 and HTTP/2 over TLS tcpSrv := &http.Server{ Addr: ":4433", // TCP port for HTTPS Handler: mux, // Use the multiplexer defined above TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS13, // HTTP/3 requires TLS 1.3; use same for parity }, } // Run TCP server in a goroutine to allow HTTP/3 server to run concurrently go func() { log.Println("Serving HTTP/1.1 and HTTP/2 on https://localhost:443") if err := tcpSrv.ListenAndServeTLS(certFile, keyFile); err != nil { log.Fatal(err) } }() // ----- HTTP/3 over QUIC/UDP ----- // Resolve UDP address for QUIC (HTTP/3) server udpAddr, err := net.ResolveUDPAddr("udp", ":4433") // HTTP/3 uses UDP if err != nil { log.Fatal(err) } // Listen on UDP port udpConn, err := net.ListenUDP("udp", udpAddr) if err != nil { log.Fatal(err) } // Create HTTP/3 server using the same mux h3Srv := http3.Server{ Addr: ":4433", // Port for QUIC Handler: mux, // Same handler for HTTP/1.1, HTTP/2, HTTP/3 TLSConfig: &tls.Config{Certificates: loadCert(certFile, keyFile)}, // TLS for QUIC } // Start HTTP/3 server log.Println("Serving HTTP/3 on https://localhost:4433") if err := h3Srv.Serve(udpConn); err != nil { log.Fatal(err) } } // loadCert loads TLS certificate and key from files // Returns a slice of tls.Certificate required by http3.Server func loadCert(certFile, keyFile string) []tls.Certificate { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { log.Fatal(err) } return []tls.Certificate{cert} }
Step 2 — Run the Server
go run .
Step 3 — Test All Protocols
curl --http3-only -k https://localhost:4433/ curl --http2 -k https://localhost:4433/ curl --http1.1 -k https://localhost:4433/
✅ Now you have a multi-protocol Go server that can handle clients across HTTP/1.1, HTTP/2, and HTTP/3 simultaneously — a realistic deployment scenario.
Conclusion
In this article, we explored HTTP/3, the latest evolution of the web’s core protocol, and learned why it matters: faster page loads, improved reliability, and better handling of modern network conditions thanks to QUIC over UDP.
We also walked through a practical example of setting up an HTTP/3 server in Go, demonstrating how to serve requests over HTTP/1.1, HTTP/2, and HTTP/3 simultaneously. By understanding both the conceptual background and hands-on implementation, you now have a solid foundation to experiment with HTTP/3 in your projects.
Top comments (0)