Star 历史趋势
数据来源: GitHub API · 生成自 Stargazers.cn
README.md

connectrpc

crates.io docs.rs CI MSRV deps.rs License

A Tower-based Rust implementation of ConnectRPC, serving Connect, gRPC, and gRPC-Web clients over HTTP with binary or JSON protobuf messages.

Status: pre-1.0. The API surface is settling but may shift in 0.x. Production-quality runtime: passes the full ConnectRPC conformance suite — 3,600 server and 6,872 client tests across the three protocols.

MSRV: Rust 1.88 (declared on the workspace, verified in CI).

Documentation:

  • User guide - long-form coverage of installation, code generation, server/client usage, streaming, tower middleware, TLS, and errors.
  • examples/ - runnable end-to-end examples (streaming, tower middleware, TLS, multi-service, browser/wasm, Bazel).
  • docs.rs - API reference.

Overview

connectrpc provides:

  • connectrpc — A Tower-based runtime library implementing the Connect protocol
  • protoc-gen-connect-rust — A protoc plugin that generates service traits, clients, and message types
  • connectrpc-buildbuild.rs integration for generating code at build time

The runtime is built on tower::Service, making it framework-agnostic. It integrates with any tower-compatible HTTP framework including Axum, Hyper, and others.

Quick Start

Define your service

// greet.proto syntax = "proto3"; package greet.v1; service GreetService { rpc Greet(GreetRequest) returns (GreetResponse); } message GreetRequest { string name = 1; } message GreetResponse { string greeting = 1; }

Generate Rust code

Two workflows are supported. Both produce the same runtime API; pick the one that fits your build pipeline.

Option A - buf generate (recommended for checked-in code)

Runs two codegen plugins (protoc-gen-buffa for message types, protoc-gen-connect-rust for service stubs) and protoc-gen-buffa-packaging twice to assemble the mod.rs module tree for each output directory. The codegen plugins are invoked per-file; only the packaging plugin needs strategy: all.

Installing the plugins

protoc-gen-buffa and protoc-gen-buffa-packaging ship from the buffa repo - see its release page for binaries or cargo install.

For protoc-gen-connect-rust, three options:

1. Download a pre-built binary from the GitHub release. Releases ship Linux (x86_64, aarch64), macOS (x86_64, aarch64), and Windows (x86_64) binaries, each with a SHA-256 checksum, a Sigstore signature (.sig + .pem), and a GitHub-native build provenance attestation.

VERSION=v0.6.0 PLATFORM=linux-x86_64 # or darwin-aarch64, etc. BASE=https://github.com/anthropics/connect-rust/releases/download/${VERSION} BIN=protoc-gen-connect-rust-${VERSION}-${PLATFORM} curl -fSL -o "${BIN}" "${BASE}/${BIN}" curl -fSL -o "${BIN}.sig" "${BASE}/${BIN}.sig" curl -fSL -o "${BIN}.pem" "${BASE}/${BIN}.pem" curl -fSL -o checksums-sha256.txt "${BASE}/checksums-sha256.txt" # Verify the checksum. grep " ${BIN}\$" checksums-sha256.txt | sha256sum -c - # Verify the GitHub-native attestation (no .sig/.pem download needed). gh attestation verify "${BIN}" --repo anthropics/connect-rust # Or verify the cosign signature directly. cosign verify-blob \ --certificate "${BIN}.pem" \ --signature "${BIN}.sig" \ --certificate-identity "https://github.com/anthropics/connect-rust/.github/workflows/release.yml@refs/tags/${VERSION}" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ "${BIN}" install -m 0755 "${BIN}" /usr/local/bin/protoc-gen-connect-rust

2. Build from source via cargo. Pulls the latest published connectrpc-codegen crate from crates.io and installs the binary into $CARGO_HOME/bin:

cargo install --locked connectrpc-codegen

3. Buf Schema Registry remote plugin (planned). Once accepted upstream the plugin will be runnable as remote: buf.build/anthropics/connect-rust in buf.gen.yaml, with no local install step.

# buf.gen.yaml version: v2 plugins: - local: protoc-gen-buffa out: src/generated/buffa opt: [views=true, json=true] - local: protoc-gen-buffa-packaging out: src/generated/buffa strategy: all - local: protoc-gen-connect-rust out: src/generated/connect opt: [buffa_module=crate::proto] - local: protoc-gen-buffa-packaging out: src/generated/connect strategy: all opt: [filter=services]
// src/lib.rs #[path = "generated/buffa/mod.rs"] pub mod proto; #[path = "generated/connect/mod.rs"] pub mod connect;

buffa_module=crate::proto tells the service-stub generator where you mounted the buffa output. For a method input type greet.v1.GreetRequest it emits crate::proto::greet::v1::GreetRequest - the crate::proto root you named, then the proto package as nested modules, then the type. The second packaging invocation uses filter=services so the connect tree's mod.rs only include!s files that actually have service stubs in them. Changing the mount point requires regenerating.

The underlying option is extern_path=.=crate::proto - same format the Buf Schema Registry uses when generating Cargo SDKs. buffa_module=X is shorthand for the . catch-all case.

Option B - build.rs (generated at build time)

Unified output: message types and service stubs in one file per proto, assembled via a single include!. No plugin binaries required at build time.

[build-dependencies] connectrpc-build = "0.6"
// build.rs fn main() { connectrpc_build::Config::new() .files(&["proto/greet.proto"]) .includes(&["proto/"]) .include_file("_connectrpc.rs") .compile() .unwrap(); }
// lib.rs pub mod proto { connectrpc::include_generated!(); }

Implement the server

use connectrpc::{Router, ConnectRpcService, Context, ConnectError}; use buffa::OwnedView; use std::sync::Arc; struct MyGreetService; impl GreetService for MyGreetService { async fn greet( &self, ctx: Context, request: OwnedView<GreetRequestView<'static>>, ) -> Result<(GreetResponse, Context), ConnectError> { // `request` derefs to the view — string fields are borrowed `&str` // directly from the request buffer (zero-copy). let response = GreetResponse { greeting: format!("Hello, {}!", request.name), ..Default::default() }; Ok((response, ctx)) } }

With Axum (recommended)

use axum::{Router, routing::get}; use connectrpc::Router as ConnectRouter; use std::sync::Arc; let service = Arc::new(MyGreetService); let connect = service.register(ConnectRouter::new()); let app = Router::new() .route("/health", get(|| async { "OK" })) .fallback_service(connect.into_axum_service()); let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?; axum::serve(listener, app).await?;

Standalone server

For simple cases, enable the server feature for a built-in hyper server:

use connectrpc::{Router, Server}; use std::sync::Arc; let service = Arc::new(MyGreetService); let router = service.register(Router::new()); Server::new(router).serve("127.0.0.1:8080".parse()?).await?;

Client

Enable the client feature for HTTP client support with connection pooling:

use connectrpc::client::{HttpClient, ClientConfig}; let http = HttpClient::plaintext(); // cleartext http:// only; use with_tls() for https:// let config = ClientConfig::new("http://localhost:8080".parse()?); let client = GreetServiceClient::new(http, config); let response = client.greet(GreetRequest { name: "World".into(), }).await?;

Per-call options and client-wide defaults

Generated clients expose both a no-options convenience method and a _with_options variant for per-call control (timeout, headers, max message size, compression override):

use connectrpc::client::CallOptions; use std::time::Duration; // Per-call timeout let response = client.greet_with_options( GreetRequest { name: "World".into() }, CallOptions::default().with_timeout(Duration::from_secs(5)), ).await?;

For options you want on every call (e.g. auth headers, a default timeout), set them on ClientConfig instead — the no-options method picks them up automatically:

let config = ClientConfig::new("http://localhost:8080".parse()?) .with_default_timeout(Duration::from_secs(30)) .with_default_header("authorization", "Bearer ..."); let client = GreetServiceClient::new(http, config); // Uses the 30s timeout and auth header without repeating them: let response = client.greet(request).await?;

Per-call CallOptions override config defaults (options win).

Streaming, interceptors, middleware, TLS

The Quick Start above shows the unary path. For everything else, see the user guide and the focused examples:

Feature Flags

FeatureDefaultDescription
gzipYesGzip compression via flate2
zstdYesZstandard compression via zstd
streamingYesStreaming compression via async-compression
clientNoHTTP client transports (plaintext)
client-tlsNoTLS for client transports (HttpClient::with_tls, Http2Connection::connect_tls)
serverNoStandalone hyper-based server
server-tlsNoTLS for the built-in server (Server::with_tls)
tlsNoConvenience: enables both server-tls + client-tls
axumNoAxum framework integration

wasm32

The core crate compiles for wasm32-unknown-unknown. Generated clients are generic over ClientTransport, so they work on wasm with a custom transport (e.g. web-sys::fetch). The client/server/tls features require platform networking and zstd requires native C compilation. See examples/wasm-client for a complete Fetch-based transport.

[dependencies] connectrpc = { version = "0.6", default-features = false, features = ["gzip"] }

Minimal build (no compression)

[dependencies] connectrpc = { version = "0.6", default-features = false }

With Axum integration

[dependencies] connectrpc = { version = "0.6", features = ["axum"] }

Generated Code Dependencies

Code generated by protoc-gen-connect-rust requires these dependencies:

[dependencies] connectrpc = { version = "0.6", features = ["client"] } buffa = { version = "0.6", features = ["json"] } buffa-types = { version = "0.6", features = ["json"] } serde = { version = "1", features = ["derive"] } serde_json = "1" http-body = "1"

Protocol Support

ProtocolStatus
Connect (unary + streaming)
gRPC over HTTP/2
gRPC-Web

All 3,600 ConnectRPC server conformance tests and 6,872 client conformance tests pass across all three protocols (2,580 Connect, 1,454 gRPC, 2,838 gRPC-Web). Run the server suite with task conformance:test and the client suites with task conformance:test-client-*.

RPC typeStatus
Unary✓ (POST + GET for idempotent methods)
Server streaming
Client streaming
Bidirectional streaming

Not yet implemented: gRPC server reflection.

Performance

Comparison against tonic 0.14 (the standard Rust gRPC implementation, built on the same hyper/h2 stack). Measured on Intel Xeon Platinum 8488C with buffa as the proto library. Higher is better unless noted.

Single-request latency

Criterion benchmarks at concurrency=1 (no h2 contention), measuring per-request framework + proto work in isolation. Lower is better.

Single-request latency

Raw data (μs, lower is better)
Benchmarkconnectrpc-rstonicratio
unary_small (1 int32 + nested msg)87.6170.81.95×
unary_logs_50 (50 log records, ~15 KB)195.0338.51.74×
client_stream (10 messages)166.1223.81.35×
server_stream (10 messages)109.8110.11.00×

Run with task bench:cross:quick.

Echo throughput

64-byte string echo, 8 h2 connections (to avoid single-connection mutex contention — see h2 #531). Measures framework dispatch + envelope framing + proto encode/decode with minimal handler work.

Echo throughput

Raw data (req/s)
Concurrencyconnectrpc-rstonic
c=16170,292168,811 (−1%)
c=64238,498234,304 (−2%)
c=256252,000247,167 (−2%)

Run with task bench:echo -- --multi-conn=8.

Log ingest (decode-heavy)

50 structured log records per request (~22 KB batch): varints, string fields, nested message, map entries. Handler iterates every field to force full decode. This is where the proto library matters — buffa's zero-copy views avoid the per-string allocations that prost's owned types require.

Log ingest throughput

Raw data (req/s)
Concurrencyconnectrpc-rstonic
c=1632,25728,110 (−13%)
c=6473,31368,690 (−6%)
c=256112,02784,171 (−25%)

At c=256, connectrpc-rs decodes 5.6M records/sec vs tonic's 4.2M.

Raw mode (strict_utf8_mapping): For trusted-source log ingestion where UTF-8 validation is unnecessary, buffa can emit &[u8] instead of &str for string fields (editions utf8_validation = NONE + the strict_utf8_mapping codegen option). CPU profile shows this eliminates 11.8% of server CPU (str::from_utf8 drops to zero). End-to-end throughput gain in this benchmark is smaller (~1%) because client encode becomes the bottleneck when both run on one machine — in production with separate client/server, the server sees ~15% more capacity.

Run with task bench:log.

Fortunes (realistic workload + backing store)

Handler performs a network round-trip to a valkey container (HGETALL of 12 fortune messages, ~800 bytes), adds an ephemeral record, sorts, and encodes a 13-message response. This is the shape of a typical read-mostly service: RPC framing + async I/O wait + moderate-size response. All three servers use an 8-connection valkey pool; client uses 8 h2 connections so protocol framing is the only variable.

Raw data (req/s, c=256)

Cross-implementation (gRPC protocol):

Implementationreq/svs connectrpc-rs
connectrpc-rs199,574
tonic192,127−4%
connect-go88,054−56%

Protocol framing (connectrpc-rs server):

Protocolc=16c=64c=256Connect ÷ gRPC
Connect73,511177,700245,1731.23×
gRPC69,706157,481199,574
gRPC-Web69,067153,727191,811

Connect's ~20% unary throughput advantage over gRPC at c=256 comes from simpler framing: no envelope header, no trailing HEADERS frame. At 200k+ req/s, gRPC's trailer frame is ~200k extra h2 HEADERS encodes per second. The gap grows with throughput (5% @ c=16 → 23% @ c=256).

Run with task bench:fortunes:protocols:h2. Requires docker for the valkey sibling container (image pulled automatically on first run).

Where the advantage comes from

CPU profile breakdown (log-ingest, c=64, 30s, task profile:log):

Cost centerconnectrpc-rstonic
Proto decode (views/owned)14.7%2.1%
UTF-8 validation11.2%4.0%
Varint decode2.2%3.5%
String alloc + copy~06.2%
HashMap ops (map fields)~08.5%
Total proto27.1%~24% (+allocator)
Allocator (malloc/free/realloc)3.6%9.6%

connectrpc-rs spends a larger fraction of CPU in proto decode — because it spends so much less everywhere else. buffa's view types borrow string data directly from the request buffer (zero allocs per string field); MapView is a flat Vec<(K,V)> scan with no hashing. tonic/prost must fully materialize String + HashMap<String,String> for every record before the handler runs.

The framework itself contributes: codegen-emitted FooServiceServer<T> with compile-time match dispatch (no Arc<dyn Handler> vtable), a two-frame GrpcUnaryBody for the common unary case, and stream-message batching into fewer h2 DATA frames.

Custom Compression

The compression system is pluggable:

use connectrpc::{CompressionProvider, CompressionRegistry, ConnectError}; use bytes::Bytes; struct MyCompression; impl CompressionProvider for MyCompression { fn name(&self) -> &'static str { "my-algo" } fn compress(&self, data: &[u8]) -> Result<Bytes, ConnectError> { /* ... */ } fn decompressor<'a>(&self, data: &'a [u8]) -> Result<Box<dyn std::io::Read + 'a>, ConnectError> { // Return a reader that yields decompressed bytes. The framework // controls how much is read, so decompress_with_limit is safe by default. /* ... */ } } let registry = CompressionRegistry::default().register(MyCompression);

Protocol Specifications

This implementation tracks the following upstream specifications:

Local copies can be fetched with task specs:fetch (see docs/specs/).

Contributing

By submitting a pull request, you agree to the terms of our Contributor License Agreement.

License

This project is licensed under the Apache License, Version 2.0.

关于 About

An implementation of the ConnectRPC protocol for Rust

语言 Languages

Rust97.6%
Python1.2%
Shell0.9%
Go0.4%

提交活跃度 Commit Activity

代码提交热力图
过去 52 周的开发活跃度
84
Total Commits
峰值: 22次/周
Less
More

核心贡献者 Contributors