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.Proto
confirmingHTTP/3
is 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)