---
title: How to Use gRPC in Rust (tonic)
summary: Use the tonic crate with Protocol Buffers to define and implement high-performance, asynchronous gRPC services in Rust.
lang: en
slug: how-to-use-grpc-in-rust-tonic
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-use-grpc-in-rust-tonic/
---

# How to Use gRPC in Rust (tonic)



## When JSON isn't fast enough

You have a backend service in Rust that processes heavy data. A microservice in Go needs that data, and it needs it fast. You tried HTTP with JSON, but the serialization overhead is eating your latency budget. You're also tired of writing boilerplate parsers and manually mapping fields between languages. You need a contract that both sides share, binary encoding for speed, and HTTP/2 multiplexing so one connection can handle thousands of requests without head-of-line blocking.

That is exactly what gRPC does. In Rust, the tool for the job is `tonic`. It implements gRPC over HTTP/2 using Protocol Buffers for serialization. You define your service in a `.proto` file, generate Rust code, and implement the logic. `tonic` handles the networking, framing, and serialization. You focus on the business rules.

## The gRPC contract

gRPC stands for Google Remote Procedure Call. It lets you call a function on a remote server as if it were local. The magic comes from Protocol Buffers, or Protobuf. Think of Protobuf as a shared blueprint language. You write a `.proto` file that defines your data structures and the functions you want to expose. Both the client and the server read this blueprint.

The compiler generates Rust code from the blueprint. This code includes structs for your messages and traits for your services. You don't write the networking boilerplate yourself. You just implement the trait. `tonic` takes this generated code and plugs it into an async runtime, usually Tokio, over HTTP/2. HTTP/2 gives you multiplexing, meaning multiple requests can ride on a single TCP connection without blocking each other. This reduces latency and connection overhead significantly compared to HTTP/1.1.

## Minimal server

Start with a standard Rust project. You need `tonic` for the gRPC implementation, `prost` for the message serialization, and `tokio` for the async runtime. You also need a build dependency to generate the code from your `.proto` files.

```toml
# Cargo.toml
[dependencies]
tonic = "0.12"
prost = "0.13"
tokio = { version = "1", features = ["full"] }

[build-dependencies]
tonic-build = "0.12"
```

Create a `build.rs` file in the root of your project. This script runs during compilation to generate Rust code from your `.proto` files.

```rust
// build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Tell tonic-build to look for .proto files in the proto directory.
    // It generates Rust code into the OUT_DIR automatically.
    tonic_build::compile_protos("proto/hello.proto")?;
    Ok(())
}
```

Create a `proto/hello.proto` file. This defines your service and messages.

```protobuf
// proto/hello.proto
syntax = "proto3";

package hello;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

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

Now implement the server in `src/main.rs`. The `tonic::include_proto!` macro includes the generated code. You implement the trait that `tonic-build` created.

```rust
// src/main.rs
use tonic::transport::Server;
use tonic::{Request, Response, Status};

// Include the generated code. The path matches the .proto file name.
mod hello {
    tonic::include_proto!("hello");
}

#[derive(Default)]
struct MyGreeter;

/// Implements the Greeter service generated from hello.proto.
#[tonic::async_trait]
impl hello::greeter_server::Greeter for MyGreeter {
    /// Handles the SayHello RPC call.
    async fn say_hello(
        &self,
        request: Request<hello::HelloRequest>,
    ) -> Result<Response<hello::HelloReply>, Status> {
        // Extract the inner message from the tonic Request wrapper.
        // The Request type holds metadata and the message.
        let name = request.into_inner().name;

        // Construct the reply using the generated message type.
        let reply = hello::HelloReply {
            message: format!("Hello {}!", name),
        };

        // Wrap the reply in a tonic Response.
        Ok(Response::new(reply))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Parse the address string into a SocketAddr.
    // This binds to all interfaces on port 50051.
    let addr = "[::]:50051".parse()?;

    // Create the service implementation.
    let greeter = MyGreeter::default();

    // Build the server, add the service, and start listening.
    // serve() blocks until the server is shut down.
    Server::builder()
        .add_service(hello::greeter_server::GreeterServer::new(greeter))
        .serve(addr)
        .await?;

    Ok(())
}
```

Run `cargo run`. The server starts and listens on port 50051. It waits for gRPC requests.

**Convention aside:** The community prefers `tonic-build` over `prost-build` for gRPC projects. `prost-build` only generates message structs. `tonic-build` wraps `prost-build` and adds the gRPC server and client traits. Using `tonic-build` reduces dependency friction and ensures the generated code matches the `tonic` version.

## How the pieces fit

When you run `cargo build`, the `build.rs` script executes first. It reads `proto/hello.proto` and generates Rust code. This code lives in a temporary directory during the build. The `tonic::include_proto!` macro in `main.rs` includes this generated code into your module.

At runtime, `tokio` starts the event loop. `Server::builder` configures the HTTP/2 transport. `add_service` registers your `MyGreeter` implementation. `serve` binds to the socket and waits for connections.

When a client sends a request, `tonic` deserializes the binary payload into your `HelloRequest` struct. It wraps the struct in a `Request` type that carries metadata like headers and timeouts. `tonic` calls your `say_hello` method. You return a `Response` wrapping your `HelloReply`. `tonic` serializes the reply back to binary and sends it over HTTP/2.

**Convention aside:** Keep `unsafe` blocks out of gRPC handlers. `tonic` and `prost` are safe abstractions. If you find yourself needing `unsafe` inside a handler, isolate it in a small helper function with a `// SAFETY:` comment. The handler should focus on logic, not memory management.

## Realistic service with validation

Real services need validation and error handling. `tonic` uses `Status` for errors, which maps to gRPC status codes. Don't return `std::io::Error` or panic. Return a `Status` so the client gets a proper error code.

```rust
// src/main.rs (updated handler)
#[tonic::async_trait]
impl hello::greeter_server::Greeter for MyGreeter {
    async fn say_hello(
        &self,
        request: Request<hello::HelloRequest>,
    ) -> Result<Response<hello::HelloReply>, Status> {
        let name = request.into_inner().name;

        // Validate input. Return a proper gRPC error if invalid.
        // Status::invalid_argument maps to the gRPC code INVALID_ARGUMENT.
        if name.is_empty() {
            return Err(Status::invalid_argument("Name cannot be empty"));
        }

        // Check for forbidden characters.
        if name.contains('!') {
            return Err(Status::permission_denied("Name contains forbidden characters"));
        }

        Ok(Response::new(hello::HelloReply {
            message: format!("Hello {}!", name),
        }))
    }
}
```

The client receives the `Status` and can inspect the code and message. This allows clients to handle errors programmatically.

**Convention aside:** Use specific `Status` codes. `Status::internal` is for server bugs. `Status::invalid_argument` is for bad client input. `Status::not_found` is for missing resources. Choosing the right code helps clients debug issues faster.

## Client side

Clients connect to the server using a `Channel`. `tonic` generates a client struct for each service. You create the client, build a request, and call the method.

```rust
// src/client.rs
use tonic::{Request, transport::Channel};
use hello::greeter_client::GreeterClient;
use hello::HelloRequest;

mod hello {
    tonic::include_proto!("hello");
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Connect to the server.
    // The URL must use http:// even though gRPC uses HTTP/2.
    let mut client = GreeterClient::connect("http://[::]:50051").await?;

    // Create the request message.
    let request = Request::new(HelloRequest {
        name: "Rust".to_string(),
    });

    // Call the RPC.
    let response = client.say_hello(request).await?;

    // Extract the inner message from the response.
    println!("Response: {}", response.into_inner().message);

    Ok(())
}
```

The client code mirrors the server. You use the generated types and methods. The `Channel` handles connection pooling and HTTP/2 multiplexing automatically.

**Convention aside:** Reuse the `Channel` for multiple requests. Creating a new channel for every request is expensive. The channel maintains a pool of connections. Clone the client struct if you need to call RPCs from multiple tasks. The client struct is cheap to clone.

## Pitfalls and compiler errors

If you forget `#[tonic::async_trait]`, the compiler rejects your implementation. You'll see errors about `async fn` not being supported in traits without the macro. The macro rewrites the trait to use `Box<dyn Future>` behind the scenes. Always add the attribute to your impl block.

If you hold a `Rc` inside your service struct, the compiler complains with `E0277` (type does not implement `Send`). gRPC requires all types to be `Send` because requests can be processed on different threads. Use `Arc` instead of `Rc` for shared state.

If your `build.rs` path is wrong, the generated code won't be found. You'll get a compilation error about missing modules. Check the path in `compile_protos` and ensure the `.proto` file exists relative to the project root.

If you try to return a non-`Send` type from an async handler, you get `E0277`. Ensure all data inside your handler is `Send`. This includes closures and captured variables.

**Convention aside:** Use `tonic::Status` for all errors. Don't map `std::io::Error` to `Status` manually unless necessary. `tonic` provides `Into` implementations for common errors. Use `?` to propagate errors and let `tonic` convert them.

## Streaming and advanced patterns

gRPC supports streaming. You can send multiple messages in a single call. `tonic` uses `Receiver` and `Sender` from `tokio::sync::mpsc` for streaming.

For server streaming, the handler returns a `Receiver`. You send messages into the channel, and the client receives them.

```rust
// Example of server streaming signature
async fn list_items(
    &self,
    request: Request<ListRequest>,
) -> Result<Response<tonic::Streaming<HelloReply>>, Status> {
    // Implementation uses a channel to send multiple replies.
    // The client iterates over the Streaming object.
    todo!()
}
```

For client streaming, the request is a `Receiver`. You receive messages from the client and process them.

For bidirectional streaming, both request and response are streams. You can read and write simultaneously.

Streaming is useful for large datasets or real-time updates. Use unary calls for simple request-response patterns. Use streaming when you need to transfer many messages or maintain a long-lived connection.

**Convention aside:** Limit stream sizes. Don't send infinite streams without backpressure. `tonic` respects backpressure from the channel, but unbounded streams can exhaust memory. Use bounded channels with `tokio::sync::mpsc::channel` and a reasonable capacity.

## Decision matrix

Use `tonic` when you need high-performance RPC with Protocol Buffers and want idiomatic async Rust. Use `reqwest` with `serde_json` when you are building a REST API and prefer human-readable payloads over binary efficiency. Use `hyper` directly when you need to manipulate HTTP headers or frames at a lower level than gRPC provides. Reach for `grpcio` only if you are maintaining legacy code; the crate is deprecated in favor of `tonic`.

Trust the generated code. If the compiler complains about the trait, check your `build.rs` paths first. The contract is the source of truth.

## Where to go next

- [How to use tonic crate in Rust gRPC](/en/how-to-use-tonic-crate-in-rust-grpc/)
- [Protocol Buffers with prost](/en/protocol-buffers-with-prost/)
- [Async Rust with Tokio](/en/async-rust-with-tokio/)