DEV Community

Cover image for Getting Started with HTTP/3 in Golang: A Practical Guide
abibeh
abibeh

Posted on

Getting Started with HTTP/3 in Golang: A Practical Guide

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:

 

📜 Official Specifications & Technical References

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:

  1. Golang version — Go 1.19+ is recommended (I’ll use Go 1.22 in examples).
  2. HTTP/3 library — We’ll use github.com/quic-go/quic-go/http3, the maintained HTTP/3 implementation for Go.
  3. TLS requirement — QUIC (and therefore HTTP/3) always uses TLS 1.3, so we must have a certificate, even for local development.
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

 
Step 2 — Install HTTP/3 package

go get github.com/quic-go/quic-go/http3
Enter fullscreen mode Exit fullscreen mode

 
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
Enter fullscreen mode Exit fullscreen mode

 
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)
 }
}
Enter fullscreen mode Exit fullscreen mode

Run it:

go run .
Enter fullscreen mode Exit fullscreen mode

You should see output similar to:

2025/08/15 14:42:51 Starting HTTP/3 server at https://localhost:4433
Enter fullscreen mode Exit fullscreen mode

 
Step 5 — Test with curl (macOS)

macOS’s built-in curl may not support HTTP/3. Install the Homebrew version:

brew install curl
Enter fullscreen mode Exit fullscreen mode

After successful installation, you should update the PATH environment, too:

export PATH="/usr/local/opt/curl/bin:$PATH"
Enter fullscreen mode Exit fullscreen mode

Check:

curl --version | grep -i http3
Enter fullscreen mode Exit fullscreen mode

Request:

curl --http3-only -k https://localhost:4433/
Enter fullscreen mode Exit fullscreen mode

Expected output:

Hello from HTTP/3! You requested / via HTTP/3
Enter fullscreen mode Exit fullscreen mode

 
✅ 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
Enter fullscreen mode Exit fullscreen mode

 
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))
}
Enter fullscreen mode Exit fullscreen mode

 
Step 3 — Run it

If you used the simple server on :4433: (in one terminal)

go run .    # from your server project (serving on :4433)
Enter fullscreen mode Exit fullscreen mode

In another terminal:

go run client.go
Enter fullscreen mode Exit fullscreen mode

Example Output:

Status   : 200 OK
Protocol : HTTP/3
Elapsed  : 6.3ms
--------- Body ---------
Hello from HTTP/3!
Enter fullscreen mode Exit fullscreen mode

 
Notes & nice extras

  • resp.Proto confirming HTTP/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}
}
Enter fullscreen mode Exit fullscreen mode

 
Step 2 — Run the Server

go run .
Enter fullscreen mode Exit fullscreen mode

 
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/
Enter fullscreen mode Exit fullscreen mode

 
✅ 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)