4 min read
On this page

gRPC & Protocol Buffers

What is gRPC?

gRPC (Google Remote Procedure Call) is a high-performance RPC framework that uses Protocol Buffers for serialization and HTTP/2 for transport. Unlike REST's resource-oriented style, gRPC is explicitly procedure-oriented: you define services with methods, and calling them feels like local function calls.

gRPC is designed for efficient, typed, service-to-service communication — not for browsers or public APIs.

Protocol Buffers

Protocol Buffers (protobuf) are Google's language-neutral, platform-neutral mechanism for serializing structured data. They serve as both the interface definition language (IDL) and the wire format for gRPC.

syntax = "proto3";

package order;

import "google/protobuf/timestamp.proto";

service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (Order);
  rpc GetOrder(GetOrderRequest) returns (Order);
  rpc ListOrders(ListOrdersRequest) returns (stream Order);        // Server streaming
  rpc UploadItems(stream OrderItem) returns (UploadSummary);       // Client streaming
  rpc LiveUpdates(stream OrderQuery) returns (stream OrderUpdate); // Bidirectional
}

message CreateOrderRequest {
  string user_id = 1;
  repeated OrderItem items = 2;
}

message GetOrderRequest {
  string id = 1;
}

message ListOrdersRequest {
  string user_id = 1;
  int32 page_size = 2;
  string page_token = 3;
}

message Order {
  string id = 1;
  string user_id = 2;
  repeated OrderItem items = 3;
  OrderStatus status = 4;
  google.protobuf.Timestamp created_at = 5;
}

message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
  int64 price_cents = 3;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING = 1;
  ORDER_STATUS_CONFIRMED = 2;
  ORDER_STATUS_SHIPPED = 3;
  ORDER_STATUS_DELIVERED = 4;
}

Key protobuf concepts:

  • Field numbers (1, 2, 3) are the wire identity — never reuse or change them
  • repeated = list/array
  • optional fields were reintroduced in proto3 for explicit presence tracking
  • oneof for mutually exclusive fields
  • Nested messages for grouping related fields
  • Use google.protobuf.Timestamp, Duration, Struct for common types
  • Enums must have a zero value (convention: _UNSPECIFIED = 0)

Why protobuf over JSON?

  • Binary format: 3-10x smaller than JSON on the wire
  • Schema-enforced: types are known at compile time, not runtime
  • Code generation: clients and servers generated from the same .proto file
  • Backward/forward compatible: add fields without breaking existing clients

Streaming Patterns

gRPC's use of HTTP/2 enables four communication patterns.

Unary RPC (Request-Response)

The simplest pattern, equivalent to a REST call.

Client --[CreateOrderRequest]--> Server
Client <--[Order]--------------- Server

Server Streaming

Server sends a stream of messages in response to a single client request. Use for: real-time feeds, large result sets, progress updates.

Client --[ListOrdersRequest]--> Server
Client <--[Order]-------------- Server
Client <--[Order]-------------- Server
Client <--[Order]-------------- Server

Client Streaming

Client sends a stream of messages; server responds once after the stream ends. Use for: file uploads, batch ingestion, aggregation.

Client --[OrderItem]--> Server
Client --[OrderItem]--> Server
Client --[OrderItem]--> Server
Client ---- END ------> Server
Client <--[Summary]---- Server

Bidirectional Streaming

Both sides send streams independently. Use for: chat, collaborative editing, real-time sync.

Client --[Query]-----> Server
Client <--[Update]---- Server
Client --[Query]-----> Server
Client <--[Update]---- Server
Client <--[Update]---- Server

tonic in Rust

tonic is the de facto gRPC framework for Rust. It generates Rust code from .proto files at build time.

Build configuration

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

[build-dependencies]
tonic-build = "0.12"
// build.rs
PROCEDURE MAIN():
    COMPILE_PROTOS("proto/order.proto")

Server implementation

// Include generated protobuf code for "order" package

STRUCTURE MyOrderService:
    db ← shared reference to Database

IMPLEMENT OrderService FOR MyOrderService:

    PROCEDURE CREATE_ORDER(request):
        req ← EXTRACT inner data FROM request

        IF req.items IS empty THEN
            RETURN Error("Order must have at least one item")

        order ← AWAIT self.db.CREATE_ORDER(req.user_id, req.items)
        IF order FAILED THEN
            RETURN Error("Failed to create order: " + error message)

        RETURN Response(order)

    PROCEDURE GET_ORDER(request):
        id ← EXTRACT inner data FROM request, GET id

        result ← AWAIT self.db.GET_ORDER(id)
        IF result IS present THEN
            RETURN Response(result)
        ELSE
            RETURN Error("Order {id} not found")

    // Server streaming: returns a stream of orders
    PROCEDURE LIST_ORDERS(request):
        req ← EXTRACT inner data FROM request
        CREATE channel (sender, receiver) with buffer size 32
        db ← CLONE self.db

        SPAWN async task:
            orders ← AWAIT db.LIST_ORDERS_FOR_USER(req.user_id)
            FOR EACH order IN orders:
                IF SEND(order) TO sender FAILS THEN
                    BREAK  // Client disconnected

        RETURN Response(stream FROM receiver)

PROCEDURE MAIN():
    addr ← "[::1]:50051"
    service ← NEW MyOrderService (default)

    BUILD gRPC Server
        ADD OrderServiceServer(service)
        SERVE at addr

Client usage

PROCEDURE MAIN():
    client ← AWAIT CONNECT OrderServiceClient TO "http://[::1]:50051"

    request ← NEW CreateOrderRequest {
        user_id ← "user_123",
        items ← [...]
    }

    response ← AWAIT client.CREATE_ORDER(request)
    PRINT "Created order:", response

When to Use gRPC

Use gRPC for:

  • Internal service-to-service communication (not browser-facing)
  • High-throughput, low-latency requirements (binary protocol, HTTP/2 multiplexing)
  • Streaming data (server-side, client-side, or bidirectional)
  • When type safety and code generation save development time
  • Polyglot environments (generate clients in any language from .proto)
  • Mobile clients on constrained networks (smaller payloads)

Don't use gRPC for:

  • Public APIs consumed by web browsers (gRPC-Web exists but adds complexity)
  • APIs where human readability of requests/responses matters (debugging, curl)
  • Simple CRUD where REST is sufficient and the team knows it well
  • When you need HTTP caching (CDNs, browser caches don't understand gRPC)

Real-World Usage

Google uses gRPC internally for virtually all inter-service communication. Google Cloud APIs expose gRPC alongside REST.

Netflix uses gRPC for communication between microservices, particularly where low-latency streaming is critical.

Dropbox migrated from a custom RPC framework to gRPC, citing code generation and cross-language support as key drivers.

Buf (buf.build) provides modern tooling for Protocol Buffers: linting, breaking change detection, and a schema registry. If you're using gRPC seriously, Buf replaces the aging protoc compiler workflow.

gRPC vs REST vs GraphQL

| Dimension | REST | GraphQL | gRPC | |-----------|------|---------|------| | Wire format | JSON (text) | JSON (text) | Protobuf (binary) | | Transport | HTTP/1.1 or 2 | HTTP/1.1 or 2 | HTTP/2 | | Schema | OpenAPI (optional) | GraphQL schema (required) | Protobuf (required) | | Streaming | No (WebSockets separate) | Subscriptions | Native (4 patterns) | | Browser support | Native | Native | Requires gRPC-Web | | Code generation | Optional | Optional | Built-in | | Human readable | Yes | Yes | No (binary) | | Best for | Public APIs | Flexible client queries | Service-to-service |

Proto Best Practices

  • Never reuse field numbers — even after deleting a field, reserve the number
  • Use reserved to prevent accidental reuse: reserved 3, 5; reserved "old_field";
  • Prefix enum values with the enum name: ORDER_STATUS_PENDING, not PENDING
  • Use wrapper types for optional primitives: google.protobuf.Int32Value when you need to distinguish 0 from "not set"
  • Design for evolution — add fields freely, never change field types or numbers
  • Keep .proto files in a shared repository or use Buf's schema registry