No description
Find a file
golang-quic-rpc 0d174f6a0c Connect RPC over HTTP/3 (QUIC): quic-go server/client, all 4 RPC modes, benchmark
- Codegen via Buf remote plugins (protocolbuffers/go + connectrpc/go)
- http3.Server serves the Connect handler; http3.Transport on the client
- TLS via demo CA (QUIC mandates TLS 1.3)
- benchmark: codec sweep 64K-2M, concurrency scaling with in-process QUIC
  connection counting (http3.Transport Dial hook)
- connect v1.21 new streaming API
2026-09-08 18:25:57 +02:00
cmd Connect RPC over HTTP/3 (QUIC): quic-go server/client, all 4 RPC modes, benchmark 2026-09-08 18:25:57 +02:00
gen/greet/v1 Connect RPC over HTTP/3 (QUIC): quic-go server/client, all 4 RPC modes, benchmark 2026-09-08 18:25:57 +02:00
proto/greet/v1 Connect RPC over HTTP/3 (QUIC): quic-go server/client, all 4 RPC modes, benchmark 2026-09-08 18:25:57 +02:00
.gitignore Connect RPC over HTTP/3 (QUIC): quic-go server/client, all 4 RPC modes, benchmark 2026-09-08 18:25:57 +02:00
buf.gen.yaml Connect RPC over HTTP/3 (QUIC): quic-go server/client, all 4 RPC modes, benchmark 2026-09-08 18:25:57 +02:00
buf.yaml Connect RPC over HTTP/3 (QUIC): quic-go server/client, all 4 RPC modes, benchmark 2026-09-08 18:25:57 +02:00
go.mod Connect RPC over HTTP/3 (QUIC): quic-go server/client, all 4 RPC modes, benchmark 2026-09-08 18:25:57 +02:00
go.sum Connect RPC over HTTP/3 (QUIC): quic-go server/client, all 4 RPC modes, benchmark 2026-09-08 18:25:57 +02:00
README.md Connect RPC over HTTP/3 (QUIC): quic-go server/client, all 4 RPC modes, benchmark 2026-09-08 18:25:57 +02:00

golang-quic-rpc

A minimal demo of a Go server and client speaking Connect RPC (connect-go) over HTTP/3 / QUIC (quic-go) — same schema, all four RPC modes, and the same benchmark methodology as the sibling repos:

Why this repo exists: the sibling benchmarks show HTTP/2 multiplexing trades connection fan-out for TCP-level head-of-line blocking (one lost segment stalls every stream). QUIC was designed to fix exactly that — independent per-stream loss recovery over UDP. This repo measures what that architecture costs and buys on loopback.

Project layout

proto/greet/v1/greet.proto   # the schema: greet.v1.GreetService (4 RPC modes)
buf.yaml / buf.gen.yaml      # codegen via Buf remote plugins (Go plugins)
gen/greet/v1/greet.pb.go     # generated messages (checked in, idiomatic Go)
gen/greet/v1/greetv1connect/ # generated Connect service stubs (checked in)
certs/                       # demo CA + server cert (gitignored; QUIC requires TLS)
cmd/server/main.go           # Connect server on http3.Server (QUIC)
cmd/client/main.go           # Connect client via http3.Transport
cmd/benchmark/main.go        # performance benchmark (codec + RPC throughput)

Dependencies (go.mod)

Dependency Why it's here
connectrpc.com/connect Connect RPC runtime for Go — handlers are plain net/http handlers, clients are plain http.Client users.
github.com/quic-go/quic-go QUIC + HTTP/3: http3.Server serves the Connect handler over QUIC, http3.Transport gives the client an http.RoundTripper.
google.golang.org/protobuf protobuf-go runtime for the generated message types.

Codegen uses Buf remote plugins (buf.build/protocolbuffers/go and buf.build/connectrpc/go), which run on Buf's servers — no local protoc needed. The generated files are checked in (idiomatic Go); regenerate with any buf binary, e.g. from a sibling pyrpc-demo venv:

/path/to/pyrpc-demo/.venv/bin/buf generate

TLS

QUIC mandates TLS 1.3, so the server listens on https://localhost:8443 with the demo certificate in certs/ (self-signed CA + leaf, 30-day validity — same scheme as pyrpc-demo's HTTP/2 demo). The client trusts certs/ca.pem. Regenerate with openssl if expired (see pyrpc-demo's server_h2.py notes for the CA + leaf procedure).

Run

go run ./cmd/server   # terminal 1 (https://127.0.0.1:8443, QUIC)
go run ./cmd/client   # terminal 2

Expected client output (all four RPC kinds):

--- unary: Greet ---
[client] Hello, world!

--- server streaming: GreetStream ---
[client] Hello #1, streamer!
[client] Hello #2, streamer!
[client] Hello #3, streamer!

--- client streaming: GreetCount ---
[client] server counted 3 requests

--- bidirectional streaming: GreetChat ---
[client] Hi, alice!
[client] Hi, bob!
[client] Hi, carol!

Benchmark

Same methodology as the sibling repos:

  1. Codec — in-process serialize/deserialize of GreetRequest at 64 / 128 / 256 / 512 / 1024 / 2048 KiB payloads (protobuf-go; identical to golang-grpc-demo's numbers by construction).
  2. Unary throughput vs concurrency — 2,000 Greet round-trips at each concurrency level (1, 4, 16, 64). Connection counting is done in-process: the benchmark transport wraps http3.Transport.Dial to track open QUIC connections (quic-go's client UDP sockets don't expose a peer address to lsof/netstat, so OS-level counting doesn't work for QUIC).
  3. Unary throughput, large payloads — 200 round-trips, 2 MiB payloads.
  4. Bidi streaming throughput — 5,000 messages over one GreetChat stream.
go run ./cmd/server     # terminal 1
go run ./cmd/benchmark  # terminal 2

Sample results (M-series MacBook, 2026-09)

Benchmark Connect + quic-go (this repo) Connect + uvicorn h1 (py) gRPC + grpc-go (h2)
unary c=1 (1 KiB) 12,018 req/s 4,871 req/s 19,583 req/s
unary c=64 (1 KiB) 38,234 req/s (3.18×) 13,383 req/s (2.75×) 72,394 req/s (3.70×)
connections at c=64 1 QUIC conn (UDP) 64 TCP conns 1 TCP conn
unary 2 MiB (c=8) 104 req/s (218 MB/s) 189 req/s (397 MB/s) 922 req/s (1,933 MB/s)
bidi streaming (1 KiB) 91k msg/s 18.2k msg/s 398k msg/s

Observations:

  • The HTTP/3 connection model works as advertised: exactly one QUIC connection at every concurrency level, carrying multiplexed streams — same fingerprint as the HTTP/2 stacks, but over UDP.
  • Loopback throughput is lower than kernel TCP — for both small and (especially) large payloads. quic-go implements QUIC in user space: every packet is a syscall and copies through Go, while HTTP/1.1/h1 and gRPC's HTTP/2 ride the kernel TCP stack. At 2 MiB payloads, single-stream QUIC flow control also caps throughput (~218 MB/s vs ~1.9 GB/s for gRPC-Go over TCP on loopback). On real networks with loss, the tradeoff can invert: per-stream loss recovery avoids the TCP head-of-line blocking that stalls every multiplexed stream on a shared HTTP/2 connection.
  • The codec numbers match golang-grpc-demo exactly (same protobuf-go runtime, no networking involved).