- Rust 100%
- build.rs codegen from a checked-in precompiled FileDescriptorSet (no protoc/buf needed at build time; regenerate via buf) - standalone hyper Server + pooled HttpClient (HTTP/1.1) - benchmark: codec sweep 64K-2M (buffa), concurrency scaling with TCP conn counts, large-payload, bidi (into_split halves) |
||
|---|---|---|
| proto/greet/v1 | ||
| src | ||
| .gitignore | ||
| build.rs | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
rust-connect-demo
A minimal demo of a Rust server and client speaking Connect RPC
via connectrpc/connect-rust
(the connectrpc crate — Tower-based, serving Connect, gRPC, and gRPC-Web
clients over HTTP), with message types generated by buffa.
This is the Rust Connect counterpart of the sibling repos, all sharing the same schema, all four RPC modes, and the same benchmark methodology:
pyrpc-demo— Connect RPC + protobuf-py (Python)pygrpc-demo— gRPC + classic protobuf (Python)rust-grpc-demo— gRPC + tonic/prost (Rust)golang-grpc-demo— gRPC + protobuf-go (Go)golang-quic-rpc— Connect over HTTP/3 / quic-go (Go)- this repo — Connect RPC + connectrpc/buffa (Rust)
Project layout
proto/greet/v1/greet.proto # the schema: greet.v1.GreetService (4 RPC modes)
proto/greet/v1/greet_descriptor.bin # precompiled FileDescriptorSet (checked in)
build.rs # connectrpc-build codegen from the descriptor set
src/server.rs # Connect server (standalone hyper-based Server)
src/client.rs # Connect client (HttpClient with connection pooling)
src/benchmark.rs # performance benchmark (codec + RPC throughput)
Dependencies (Cargo.toml)
| Dependency | Why it's here |
|---|---|
connectrpc (features client, server) |
Connect runtime: Tower-based handlers, standalone hyper server, pooled HTTP client. |
connectrpc-build (build) |
build.rs codegen — service traits, clients, and message types in one pass. |
buffa, buffa-types |
Message types: generated structs derive buffa's wire-format traits (and serde for the JSON codec). |
serde, serde_json, http, http-body |
Required by the generated code's bounds (JSON codec / HTTP types). |
tokio, futures |
Async runtime and stream combinators. |
Codegen without protoc
build.rs reads a precompiled FileDescriptorSet (greet_descriptor.bin,
checked in), so neither protoc nor buf is needed at build time. Regenerate
the descriptor after changing greet.proto (needs any buf binary, e.g. from a
sibling pyrpc-demo venv):
/path/to/pyrpc-demo/.venv/bin/buf build -o proto/greet/v1/greet_descriptor.bin --as-file-descriptor-set
Note: our proto uses syntax = "proto3" (implicit presence), so generated
scalars are plain String/i32 — not Option — and views borrow &str
zero-copy from the request buffer.
Run
cargo run --release --bin server # terminal 1
cargo run --release --bin 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. HttpClient::plaintext() is hyper's
pooled HTTP/1.1 client — the direct counterpart of pyrpc-demo's pyqwest
client — so the concurrency sweep shows the HTTP/1.1 fingerprint (one socket
per in-flight request). (connect-rust also offers h2c and TLS-h2 transports;
not benchmarked here.)
- Codec — in-process serialize/deserialize of
GreetRequestat 64 / 128 / 256 / 512 / 1024 / 2048 KiB payloads (buffa wire format), each timed ≥0.5 s per direction. - Unary throughput vs concurrency — 2,000
Greetround-trips at each concurrency level (1, 4, 16, 64), with live TCP connection counts (vianetstat; note it sees both ends of loopback connections, so counts read roughly 2× the real connection count plus pool churn). - Unary throughput, large payloads — 200 round-trips, 2 MiB payloads.
- Bidi streaming throughput — 5,000 messages over one
GreetChatstream (split into send/receive halves viainto_split()).
cargo run --release --bin server # terminal 1
cargo run --release --bin benchmark # terminal 2
Sample results (M-series MacBook, 2026-09)
| concurrency | req/s | TCP conns (netstat, ~2× real) |
|---|---|---|
| 1 | 20,844 | 2 |
| 4 | 42,111 | 8 |
| 16 | 91,941 | 36 |
| 64 | 117,887 | 170 |
| Benchmark | this repo | Connect/pyqwest (py) | gRPC/tonic (Rust) |
|---|---|---|---|
| unary 2 MiB (c=8) | 937 req/s (1,966 MB/s) | 189 req/s (397 MB/s) | 626 req/s (1,313 MB/s) |
| bidi streaming (1 KiB) | 90.5k msg/s | 18.2k msg/s | 572k msg/s |
Observations:
- Fastest Connect stack in the family on HTTP/1.1: 118k req/s at c=64 (~9× the Python Connect client) and ~2 GB/s on 2 MiB payloads — the protocol's per-request HTTP/1.1 handling costs little when it's all Rust.
- Still HTTP/1.1: throughput is bought with sockets like every other HTTP/1.1 stack, and single-stream bidi (90.5k msg/s) trails tonic's HTTP/2 (572k msg/s).
- Codec: buffa's wire format is on par with prost on serialize and notably faster on deserialize (zero-copy views).
See ../pyrpc-demo's README (at
git.burakemir.ch/burak/pyrpc-demo)
for the cross-language results table and charts.