gRPC & Protobufinfrastructure

Why gRPC & Protocol Buffers

Schema-first APIs with enforced contracts

v1.1·10 min read·Kenneth Pernyér
grpcprotobufapischemarpcstreaming

The Problem

Internal service communication needs different properties than public APIs. Clients are other services you control, not browsers or mobile apps.

JSON REST is optimized for the wrong things.

Human readability. Self-describing messages. Flexibility. These help when debugging public APIs. They hurt when services exchange millions of messages per second.

We needed internal communication that:

  • Enforces schema at compile time, not runtime
  • Minimizes serialization overhead
  • Supports streaming (especially for ML inference)
  • Generates client and server code from the same source of truth

Current Options

OptionProsCons
REST + JSONUniversal, human-readable, widely supported.
  • Works everywhere
  • Easy to debug
  • Huge ecosystem
  • Browser native
  • Schema optional (often missing)
  • Verbose wire format
  • No native streaming
  • Runtime type validation
gRPC + ProtobufBinary protocol with strong typing and streaming.
  • Schema-first design
  • Efficient binary format
  • Native streaming support
  • Generated code in many languages
  • Not browser-friendly (needs grpc-web)
  • Harder to debug (binary)
  • Requires code generation step
GraphQLQuery language for APIs. Client specifies what it needs.
  • Flexible queries
  • Single endpoint
  • Strong typing
  • Good for frontend needs
  • Overhead for simple cases
  • N+1 query problems
  • Caching complexity
  • Not ideal for service-to-service

Future Outlook

Protocol Buffers are infrastructure, not fashion.

Schema-first is winning.

OpenAPI, GraphQL, Protobuf—the industry is moving toward explicit schemas. The only question is which format.

gRPC handles what REST cannot.

Bidirectional streaming, deadline propagation, automatic retries, load balancing—these are built into gRPC. With REST, you implement them yourself (or don't).

The browser gap is closing.

gRPC-web and connect-protocol bring gRPC to browsers. The "use REST for public APIs" rule is becoming optional.

Our Decision

Why we chose this

  • Schema as code.proto files generate types in any language. The contract is explicit and versioned.
  • Efficient serializationBinary format is 3-10x smaller than JSON. Parsing is faster.
  • StreamingServer streaming, client streaming, bidirectional. Native to the protocol.
  • Code generationClients and servers generated from the same .proto. No drift possible.

×Trade-offs we accept

  • Browser complexityRequires grpc-web or connect. Not as simple as fetch().
  • Debugging difficultyBinary format needs tools to inspect. No curl for quick tests.
  • Build stepProtobuf compilation adds to build process. Must keep generated code in sync.

Motivation

Our internal services exchange high-volume messages. User events, ML predictions, state updates. JSON parsing became a measurable cost.

More importantly, we needed streaming. ML inference streams tokens. Event processing streams updates. REST requires long-polling or WebSockets—both add complexity.

gRPC gives us streaming natively. Server streams inference results. Clients stream input batches. The protocol handles it; we write business logic.

The schema enforcement is equally valuable. A .proto change that breaks compatibility fails at build time. No production surprises from schema drift.

Recommendation

Use gRPC for internal service-to-service communication:

  • High-volume message exchange
  • Streaming requirements
  • Strong schema enforcement
  • When both client and server are under your control

Use REST for:

  • Public APIs consumed by browsers
  • Webhooks and external integrations
  • When simplicity matters more than efficiency

Protobuf tips:

  • Version your .proto files in the same repo as services
  • Use buf for linting and breaking change detection
  • Generate code in CI, commit the results

In Rust, use Tonic for gRPC. It shares Tower middleware with Axum, so authentication and logging work across both.

Examples

proto/inference.protoprotobuf
syntax = "proto3";
package converge.inference.v1;

service InferenceService {
  // Unary: single request, single response
  rpc Predict(PredictRequest) returns (PredictResponse);

  // Server streaming: single request, stream of responses
  rpc StreamPredict(PredictRequest) returns (stream PredictChunk);

  // Bidirectional: stream both ways
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

message PredictRequest {
  string model_id = 1;
  repeated float features = 2;
  PredictOptions options = 3;
}

message PredictResponse {
  repeated float predictions = 1;
  float confidence = 2;
  int64 latency_ms = 3;
}

message PredictChunk {
  float partial_prediction = 1;
  bool is_final = 2;
}

message ChatMessage {
  string content = 1;
  string role = 2; // "user" or "assistant"
}

Protobuf defines the contract. gRPC provides unary, server-streaming, client-streaming, and bidirectional patterns. Tonic generates Rust types and traits.

Related Articles

Stockholm, Sweden

Version 1.1

Kenneth Pernyér signature