tarpc
An async Rust RPC framework that defines services directly in code via a macro instead of a separate IDL, with pluggable transports and deadline propagation.
Repository Health
Technical Analysis
tarpc is an RPC framework for Rust, originally built at Google, that focuses on ease of use by defining services in ordinary Rust code rather than a separate schema language like .proto. A single #[tarpc::service] macro turns a trait definition into a full request/response contract: generated types, a typed async client stub, and a server-side dispatch trait, with no separate compilation step or context switch between languages.
The core client and server are transport-agnostic — any type implementing Stream<Item = Request> + Sink<Response> can carry traffic, from in-process channels used in tests to TCP or Unix sockets via the built-in serde_transport helpers. Beyond basic request/response plumbing, tarpc adds cascading cancellation (dropping a request cancels it end-to-end, including any downstream requests it triggered), configurable deadline propagation through a request context, and tracing/OpenTelemetry instrumentation built into every call.
What You Get
- A
#[tarpc::service]proc macro that turns a single trait definition into a generated service trait, request/response types, a typed async client stub, and a server-side serve trait — no.protofiles or separate codegen step. - A transport-agnostic client (
tarpc::client) and server (tarpc::server) built onStream/Sink, plus ready-madeserde_transporthelpers for TCP and Unix sockets over JSON or bincode. - Cascading request cancellation and configurable deadline propagation via
context::Context, so timeouts and cleanup work automatically across chains of RPC calls. - Built-in
tracinginstrumentation extended with OpenTelemetry for every RPC, plus an optionalserde1feature so in-process transports can skip serialization entirely. - A full example service (
example-service) and runnable examples covering compression, pub/sub, custom transports, and TLS over TCP.
Common Use Cases
- Typed RPC between Rust microservices without maintaining a separate protobuf/IDL toolchain.
- In-process async message passing between components using the in-memory channel transport.
- Distributed backends that need end-to-end deadline propagation and cascading cancellation across chains of calls.
- Systems already instrumented with tracing/OpenTelemetry that want RPCs to appear in the same distributed trace.
Under The Hood
Architecture
tarpc splits cleanly into a compile-time layer and a runtime layer: the tarpc-plugins crate (a syn/quote-based proc macro) expands a #[tarpc::service]-annotated trait into a service trait, request/response enums, a client stub, and a server dispatch impl, while the tarpc crate’s runtime core stays generic over any transport implementing Stream<Item = Request> + Sink<Response>. client.rs and server.rs each carry an in_flight_requests submodule that tracks pending requests by ID, cancellations.rs implements cascading cancellation over a side channel, context.rs carries deadline and tracing-span propagation, and serde_transport.rs layers framed, serde-aware codecs (JSON/bincode via tokio-util/tokio-serde) on top of the same abstract transport. Because generated service code only ever depends on the small Transport/Channel abstractions, changes to the core dispatch logic apply uniformly to every macro-generated service without touching user code.
Tech Stack
Built for async Rust (edition 2024, MSRV 1.85) on top of tokio (feature-gated via tokio1) and futures for Stream/Sink combinators. Wire-level serialization is optional and additive: serde1 enables Serialize/Deserialize on requests and responses, serde-transport layers tokio-serde framing on top, and serde-transport-json/-bincode pick the concrete codec; tcp/unix gate the corresponding tokio::net socket types. Errors are typed via thiserror, and every RPC carries tracing spans extended with tracing-opentelemetry/opentelemetry for distributed traces. The repo is a three-crate Cargo workspace (tarpc core, plugins proc macro, example-service).
Code Quality
Testing is layered and unusually thorough for a proc-macro-heavy crate: inline unit tests live alongside the code they test (e.g. ChannelError up/downcast round-trips in lib.rs), integration tests cover functional service behavior and macro hygiene (tarpc/tests/service_functional.rs, proc_macro_hygene.rs), and a dedicated trybuild-driven harness (tests/compile_fail.rs) asserts that specific misuses of the service macro fail to compile with the expected diagnostics, split across serde1/no_serde1 feature variants. CI runs the full test suite across a matrix of every feature-flag combination (serde, tokio1, serde-transport, json/bincode, tcp, unix) with explicit exclude rules for invalid combinations, plus a separate job that enumerates and runs every example. Error handling is explicit and typed (ChannelError, ServerError) rather than swallowed, and the codebase is heavily documented with module- and item-level doc comments throughout.
API Design
The headline ergonomic win is that one macro call replaces a schema file, a codegen step, and hand-written client/server boilerplate — a working service is a trait plus an impl. Every generated method takes a context::Context as its first argument, which is a small but consistent piece of friction in exchange for deadline propagation and tracing coming for free. Getting productive requires comfort with async Rust and Stream/Sink generics, which raises the floor for newcomers, but extensive doc comments and a spread of runnable examples (compression, pub/sub, custom transports, TLS) substantially soften that curve.