---
title: tonic (gRPC)
url: https://doc.liz6.com/en/rust/12-async-ecosystem/02-tonic-gRPC
locale: en
area: rust
tags:
- rust
- async-ecosystem
date: 2026-06-30
modified: 2026-07-16
description: gRPC uses protobuf to define strongly-typed service interfaces—changing a .proto file requires all callers to update, otherwise compilation fails (a compiler-verified contract, not a convention). tonic is Rust’s gRPC implementation; codegen generates Rust traits, streaming types (unary/client/server/bidi), and an interceptor layer from .proto files, each corresponding to different usage scenarios and cross-cutting logic.
---

# tonic (gRPC)

> gRPC uses protobuf to define strongly-typed service interfaces—changing a .proto file requires all callers to update, otherwise compilation fails (a compiler-verified contract, not a convention). tonic is Rust’s gRPC implementation; codegen generates Rust traits, streaming types (unary/client/server/bidi), and an interceptor layer from .proto files, each corresponding to different usage scenarios and cross-cutting logic.

## Differences Between gRPC and REST

REST expresses operations via HTTP methods and URLs, with payloads typically in JSON. gRPC uses protobuf to define strongly-typed service interfaces, with payloads encoded as binary protobuf messages. The difference is not just format—it’s the **programming model**. REST interface definitions are "conventions" (API docs + boilerplate). gRPC interface definitions are **compiler-verified contracts**—changing a .proto file requires all callers to update, otherwise compilation fails.

tonic is the most mature gRPC implementation in the Rust ecosystem, built on tokio + tower + prost (the protobuf compiler).

## From .proto to Rust Types

```protobuf
syntax = "proto3";
package greeter;

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply);           // unary: 1 request → 1 response
    rpc StreamHello (HelloRequest) returns (stream HelloReply); // server streaming
    rpc Chat (stream ChatMessage) returns (stream ChatMessage); // bidi streaming
}

message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }
```

In `build.rs`, the `tonic_build::compile_protos()` call invokes protoc + prost to generate:
- `GreeterServer<T: Greeter>` — server wrapper (wraps the `Greeter` trait as a tower `Service`)
- `Greeter` trait — interface for business logic (methods you implement)
- `GreeterClient` — client stub (async methods for callers to use)

The generated code implements trait methods using `#[tonic::async_trait]`—this allows traits to contain `async fn` (native Rust traits do not yet support async fn).

## Server: Implementing the Trait

```rust
struct MyGreeter;
#[tonic::async_trait]
impl Greeter for MyGreeter {
    async fn say_hello(&self, req: Request<HelloRequest>) -> Result<Response<HelloReply>, Status> {
        let name = req.into_inner().name;
        if name.is_empty() {
            return Err(Status::invalid_argument("name is required"));
        }
        Ok(Response::new(HelloReply {
            message: format!("Hello {}!", name),
        }))
    }
}

tonic::transport::Server::builder()
    .add_service(GreeterServer::new(MyGreeter))
    .serve("[::1]:50051".parse().unwrap()).await?;
```

`Status` is gRPC’s standard error type—not an HTTP status code. gRPC defines 17 status codes: `Ok`, `Cancelled`, `InvalidArgument`, `NotFound`, `AlreadyExists`, `PermissionDenied`, `Unauthenticated`, `ResourceExhausted`, `Unimplemented`, `Internal`, `Unavailable`, `DeadlineExceeded`. Each code has a clear semantic meaning—unlike HTTP, where 400 might mean "malformed format" or "missing parameter."

## Streaming: More Than "One Request, One Response"

```rust
// Server streaming: server sends data incrementally
async fn stream_hello(&self, req: Request<HelloRequest>) -> Result<Response<Self::StreamHelloStream>, Status> {
    let mut stream = tokio_stream::iter(vec!["Hello", "Bonjour", "Hola"].into_iter()
        .map(|s| Ok(HelloReply { message: s.into() })));
    Ok(Response::new(Box::pin(stream)))
}

// Bidirectional streaming: both sides send simultaneously
async fn chat(&self, req: Request<Streaming<ChatMessage>>) -> Result<Response<Self::ChatStream>, Status> {
    let mut in_stream = req.into_inner();
    let (tx, rx) = tokio::sync::mpsc::channel(4);
    tokio::spawn(async move {
        while let Some(Ok(msg)) = in_stream.next().await {
            tx.send(Ok(ChatReply { response: format!("echo: {}", msg.text) })).await.unwrap();
        }
    });
    Ok(Response::new(Box::pin(tokio_stream::wrappers::ReceiverStream::new(rx))))
}
```

Key point about streaming: server streaming = server returns `stream T`, and the client receives items one by one. Bidirectional = both sides send `stream`s independently. This differs from WebSocket’s "bidirectional channel"—in gRPC streaming, each message is still an independent, strongly-typed protobuf message.

## Interceptors: Request-Level Middleware

```rust
fn auth_interceptor(mut req: Request<()>) -> Result<Request<()>, Status> {
    let token = req.metadata().get("authorization")
        .ok_or_else(|| Status::unauthenticated("missing token"))?;
    validate_token(token)?;
    Ok(req)
}

Server::builder()
    .add_service(GreeterServer::with_interceptor(MyGreeter, auth_interceptor))
```

Interceptors execute before handlers—enabling authentication, rate limiting, metadata injection, etc. The difference from tower `Layer`: interceptors are a tonic-specific concept, while `Layer` is tower’s generic abstraction. Under the hood, tonic interceptors are essentially thin wrappers around tower `Service`.

## References

- **tonic**: github.com/hyperium/tonic
- **gRPC**: grpc.io (spec, status codes)
- **protobuf**: protobuf.dev (encoding, proto3 language guide)

*Keywords: tonic, gRPC, protobuf, streaming, interceptor, Status, unary, bidirectional*
