Not "which is better" β which fits where. gRPC and REST/JSON make opposite trades on typing, transport, and reach. Pick per boundary.
| Dimension | gRPC | REST / JSON |
|---|---|---|
| Payload | Protobuf β compact binary | JSON β verbose text |
| Transport | HTTP/2 required | Any HTTP (1.1 fine) |
| Contract | .proto, enforced, codegen'd | OpenAPI β optional, often drifts |
| Streaming | First-class, all 4 shapes | Awkward (SSE / chunked / polling) |
| Browser support | Needs gRPC-Web + proxy | Native, universal |
| Human-debuggable | No β binary, needs tooling | Yes β curl, browser, logs |
| Caching (intermediaries) | Not really (opaque POSTs) | Strong β HTTP caching semantics |
| Tooling maturity | Great codegen; weaker ecosystem breadth | Enormous, ubiquitous |
| Latency / CPU overhead | Low (binary, one conn, header compression) | Higher (text parse, headers) |
| Schema evolution | Strong, field-number based | Convention-based, easy to break |
// gRPC: contract-first, binary on the wire
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
// REST: URL + verb + JSON body
// GET /v1/users/u_123
{ "user_id": "u_123", "display_name": "Ada", "email": "ada@x.dev" }
The gRPC side ships a generated, type-checked client in every language. The REST side you can hit from a browser address bar. That's the trade in one screen.
GET + ETag + Cache-Control.Browsers can't speak raw gRPC β JavaScript has no access to HTTP/2 trailers or frame-level control. gRPC-Web is a browser-compatible dialect that a proxy (Envoy, or a built-in gateway) translates to real gRPC behind the scenes.
.proto, so you write the contract once and get both. The status-code mapping is what makes that translation clean.