gRPC and HTTP/2
A typical backend makes dozens of internal calls to fulfill one user-facing request — an order service calling an inventory service, which calls a pricing service, which calls a currency-conversion service. Each of those calls is machine-to-machine, happens far more often than any single browser request, and has none of a browser's need for human-readable text. A REST API built on JSON over HTTP/1.1 works for this, and plenty of production systems run on exactly that, but it carries costs that start to matter at that call volume: JSON is verbose to parse and serialize, there's no enforced contract between caller and callee beyond whatever documentation exists, and — as the previous article ended by naming — a raw stream gives you no defined method signatures at all. gRPC, built by Google and released as an open standard in 2015, is a remote procedure call framework designed specifically for this internal, high-volume, service-to-service traffic, and it's built directly on top of HTTP/2 rather than around it.
Remote procedure calls: making a network call look like a function call
The idea behind any RPC framework, gRPC included, is to let a client invoke a method on a remote service the same way it would call a local function — inventoryClient.CheckStock(itemId) — while the framework handles serializing the arguments, sending them over the network, and turning the response back into a typed return value. This is a different mental model from REST, where a client constructs an HTTP request against a resource URL (GET /items/42/stock) and interprets a status code and a JSON body itself. RPC hides the network call behind a method signature; REST exposes the network call as the interface.
gRPC formalizes this with a service definition written in Protocol Buffers' interface definition language, typically a .proto file:
syntax = "proto3";
service InventoryService {
rpc CheckStock (StockRequest) returns (StockResponse);
}
message StockRequest {
int32 item_id = 1;
}
message StockResponse {
int32 quantity_available = 1;
bool in_stock = 2;
}
This single file is the actual contract between client and server. A code generator reads it and produces client and server code in whatever language each side is written in — Go, Java, Python, and others can all call the same CheckStock method against generated code tailored to their own language, with no hand-written HTTP client or JSON parsing on either side. Changing the contract means changing this file and regenerating code, which is a meaningfully stronger guarantee than a REST API's documentation staying in sync with its actual behavior by convention alone.
Protocol Buffers: a binary format instead of text
The messages gRPC exchanges are encoded with Protocol Buffers (protobuf), a binary serialization format, rather than JSON. Each field in a .proto message is assigned a small integer tag (the = 1, = 2 in the example above), and on the wire, a value is encoded as that tag plus the value itself, with no field name, no braces, and no quotation marks ever transmitted.
The size difference is real and it's the point: JSON re-sends the field name "quantity_available" in every single message, forever, even though both sides already agree on the schema from the shared .proto file. Protobuf only sends the tag number, because the schema — which tag means which field, and what type it holds — was already established when the code was generated, not something that needs restating on every message. At the volume of a busy internal service mesh, where the same message shape crosses the network millions of times a day, this difference compounds into real bandwidth and CPU savings on both serialization and parsing.
The trade-off is exactly what you'd expect: a protobuf-encoded message is unreadable without the .proto schema that defines it, where a JSON body can be read and debugged with nothing more than a text editor. This is a genuine cost during development and debugging, and it's part of why gRPC is a strong fit for internal service-to-service calls and a poor fit for a public API a third-party developer is expected to explore and debug by hand.
Why gRPC needs HTTP/2, specifically
gRPC isn't merely compatible with HTTP/2 — it depends on features HTTP/1.1 simply doesn't have, which is why gRPC was designed against HTTP/2 from the start rather than retrofitted onto it.
Multiplexing. A single gRPC client typically has many calls in flight to the same service at once — checking stock for several items, say, as part of one order. Over HTTP/1.1, that means either queuing those calls onto a small number of separate TCP connections or serializing them one after another; over HTTP/2, they travel as separate streams on one connection, genuinely concurrent, with none of them blocking another.
Header compression. Every gRPC call carries metadata in HTTP/2 headers — the method being called, authentication tokens, deadlines. HPACK means that metadata, much of which repeats identically across thousands of calls to the same service, costs almost nothing after the first call establishes it in the compression table.
Streaming built on top of streams. This is the feature REST-over-HTTP/1.1 has no real equivalent for. A gRPC method can be declared to send or receive a sequence of messages instead of exactly one:
stream StockResponse means the server keeps sending StockResponse messages over time on the same call — a live feed of stock-level changes for one item, all riding one HTTP/2 stream, closed by the server only when it's genuinely done. gRPC supports this in either direction independently: server streaming (as above), client streaming (a client uploading a sequence of messages before getting one final response), and bidirectional streaming, where both sides send a sequence of messages on the same call at the same time. None of this needs a separate protocol upgrade the way WebSocket does — it's a direct, deliberate use of an HTTP/2 stream's ability to carry an open-ended sequence of frames rather than exactly one request and one response.
sequenceDiagram
participant C as Client
participant S as InventoryService
C->>S: WatchStockLevel(item_id=42) [opens one HTTP/2 stream]
S-->>C: StockResponse(quantity=100)
S-->>C: StockResponse(quantity=97)
S-->>C: StockResponse(quantity=95)
Note over C,S: Same stream stays open until the server ends it
Status codes: gRPC doesn't reuse HTTP's
A detail that trips up developers coming from REST: a gRPC call that fails at the application level — item not found, invalid argument, permission denied — does not come back as an HTTP 404 or 403. HTTP/2's own status code on a gRPC response is almost always 200, because as far as HTTP/2 is concerned, the stream carried its data successfully. gRPC layers its own status code on top, sent as a trailing header after the message body:
grpc-status: 5 is gRPC's NOT_FOUND, one of a fixed set of status codes gRPC defines independently of HTTP's. This separation exists because gRPC's status codes need to mean the same thing across every language binding, regardless of how that language's generated code happens to map errors — a Python client and a Go client both see NOT_FOUND as a well-defined constant, rather than each interpreting an HTTP status code by convention. Debugging a gRPC failure by looking only at the HTTP-level response code will almost always show "200 OK, but the call failed" and mislead you; the actual outcome is in grpc-status.
Where gRPC is the wrong choice
gRPC's strengths are specifically the strengths of a controlled, internal environment: both ends compiled from the same .proto file, both ends usually on the same private network, calls made by code rather than by a human exploring an API. That's also exactly where it stops being the right choice. A public-facing API consumed by third-party developers benefits far more from JSON's human-readability, from being explorable with nothing more than curl and a browser, and from the massive existing tooling built around REST. Browser JavaScript, historically, couldn't speak gRPC directly at all — browsers don't expose the low-level HTTP/2 trailer support gRPC needs — which is why gRPC-Web, a restricted variant proxied through a translating layer, exists specifically to bridge browser clients into a gRPC backend rather than browsers speaking gRPC natively.
Note
Don't read "gRPC is faster" as a blanket claim. For a handful of calls a minute, the difference between JSON and protobuf is noise. The case for gRPC strengthens specifically with call volume, strict contract requirements between services owned by different teams, and streaming needs a plain REST API has no clean way to express — not as a default upgrade for every internal API regardless of its traffic pattern.
Practical scenario: a gRPC deadline that a REST-trained team didn't expect
A team migrating an internal REST API to gRPC starts seeing calls fail with grpc-status: 4 (DEADLINE_EXCEEDED) under load, on calls that used to just run slow over REST and eventually succeed. Their REST client had no timeout configured at all; it simply waited.
Investigating the client code turns up that whoever generated the gRPC client bindings had set an explicit timeout on every call, following the generated code's own defaults and examples — and gRPC treats a deadline as a first-class part of a call, sent to the server as metadata, rather than an optional client-side setting a caller might or might not configure. The server, once it also received the client's 2.0-second deadline via the call's own metadata, correctly aborted work still running past that point, matching gRPC's design intent: the server can stop doing wasted work for a client that has already stopped waiting, something plain HTTP has no standard mechanism to communicate at all. The actual fix isn't disabling the deadline — that reintroduces the plain-REST problem of a slow call hanging indefinitely and holding server resources for a caller that may not even still care about the answer — it's raising the deadline to a value that reflects what CheckStock genuinely takes under real load, and treating DEADLINE_EXCEEDED in monitoring as a signal that the service is too slow, not a bug in the client's timeout logic.
Practice exercises
- A public-facing API will be consumed by external partners who need to explore and debug it using nothing more than a browser and
curl. Using the trade-offs described above, would you recommend gRPC or a JSON-based REST API, and why specifically? - Explain why a gRPC call that fails with "item not found" returns HTTP status
200at the HTTP/2 layer, and where the actual error information is carried instead. - Design, at a high level, the
.protoservice definition for aNotificationServicethat needs to push an open-ended sequence of notification messages to a single subscribed client. Which of gRPC's four call shapes (unary, server streaming, client streaming, bidirectional) fits, and why?
This module has covered three ways to move data beyond a single request and response — WebSocket's symmetric channel, SSE's one-way push, and gRPC's typed, streamable calls between services. All three assume the two endpoints know exactly who they're talking to: one client, one server, over one connection. The next article breaks that assumption and asks a different question: when a single request needs to reach one of many possible servers, or even every server on a network at once, how does the network itself decide which one actually receives it?
Sources
- gRPC Authors, Introduction to gRPC
- gRPC Authors, Core concepts, architecture and lifecycle
- Protocol Buffers, Protocol Buffers Overview
- gRPC Authors, Status codes and their use in gRPC