Why gRPC & Protocol Buffers
Schema-first APIs with enforced contracts
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
| Option | Pros | Cons |
|---|---|---|
| REST + JSONUniversal, human-readable, widely supported. |
|
|
| gRPC + ProtobufBinary protocol with strong typing and streaming. |
|
|
| GraphQLQuery language for APIs. Client specifies what it needs. |
|
|
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
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.