gRPC Fundamentals: RPC Types, HTTP/2, and the Browser Gap
Concept
gRPC is a Remote Procedure Call framework, originally developed at Google and open-sourced in 2015, that lets a client invoke a method on a remote server as if it were a local function call. It's built on two specific technology choices that define most of its behavior: HTTP/2 as the transport, and Protocol Buffers (protobuf) as the interface definition language and default wire format. A .proto file defines a service and its methods with strongly-typed request/response messages; the protoc compiler generates client stubs and server skeletons in whichever language each side uses, so a Go server and a Python client can call each other through generated code that never touches raw bytes or hand-written JSON parsing.
Because it rides on HTTP/2, gRPC gets multiplexing — many concurrent calls over one TCP connection, without the head-of-line blocking HTTP/1.1 pipelining suffered from — and native support for streaming, which produces four distinct RPC shapes: unary (one request, one response — the REST-equivalent default), server-streaming (one request, a stream of responses, e.g. a large result set delivered incrementally), client-streaming (a stream of requests, one response at the end, e.g. uploading chunks before returning a final result), and bidirectional-streaming (both sides stream independently over the same call, e.g. a live exchange between two backend services). Every RPC — streaming or not — can carry a deadline the client sets; once it elapses, the call fails with DEADLINE_EXCEEDED on both ends. Unlike an ad-hoc client-side timeout, that deadline is a first-class part of the call's metadata and gets propagated if the server itself makes downstream calls, so a chain of services can share one overall time budget instead of each hop inventing its own.
Tradeoffs
| Approach | Benefit | Cost |
|---|---|---|
| REST (JSON over HTTP/1.1) | Human-readable payloads, universal tooling (curl, browsers, API gateways), no codegen required |
No native streaming; JSON parsing/serialization overhead; no compiler-enforced contract, only whatever an OpenAPI doc informally promises |
| GraphQL | Client controls exactly which fields it gets back, single endpoint for varied queries | Not built for streaming or binary payloads; server-side complexity (resolvers, N+1 query risk) |
| gRPC | Strong typing enforced by the compiler on both ends, low-latency binary encoding, native streaming in all four shapes, deadline propagation across service hops | No native browser support (see below); binary payloads aren't human-readable on the wire, complicating debugging/observability tooling built around text; proto contract changes need real discipline to stay backward-compatible |
When to use / when not to
- Use for internal service-to-service calls once a system has split into multiple backend services and call volume/latency matter more than human-readability of the wire format — this is gRPC's home turf.
- The streaming shapes are the clear win when the interaction genuinely isn't one-shot request/response: incremental large-result delivery (server-streaming), chunked uploads (client-streaming), or a persistent bidirectional exchange between two backend services.
- Don't expose gRPC directly to a browser client — browsers have no API with the fine-grained control over a request that real HTTP/2 framing needs, so a direct gRPC call from browser JavaScript isn't possible at all.
grpc-web(a JS client speaking a modified, browser-compatible wire format) plus a proxy that translates it to real gRPC on the backend is the standard workaround, but it's an extra moving part most REST or GraphQL APIs don't need. - Skip it for a small public-facing API where broad client compatibility (mobile SDKs some teams don't control, third-party integrators, simple
curldebugging) matters more than the performance gain — REST/JSON's universal tooling story still wins there.
Common pitfall
Treating a .proto file like a REST OpenAPI spec that can be casually edited. Because generated stubs are compiled directly into both client and server binaries, a breaking proto change (reusing a field number, changing a field's type, removing a field a client still sends) can silently corrupt data on the wire rather than failing loudly the way a REST client hitting a changed JSON shape often does — protobuf's wire format identifies fields by number, not name, so field-number reuse after a field is removed is the single most common way this actually breaks in production.
Engineering Lens
gRPC's real value in a design review isn't "it's faster than JSON" — raw serialization speed rarely dominates a service's actual latency budget. It's the compiler-enforced contract between services and deadline propagation across a call chain: when service A calls B calls C, one deadline set at A bounds the whole chain instead of each hop needing its own timeout tuned independently, which is exactly the kind of cross-service coordination that's easy to get wrong by hand and expensive to debug when a chain of ad-hoc timeouts doesn't add up to the SLA the top-level caller actually needs.