Synchronous Communication

System Design · Synchronous Communication

gRPC: Architecture & When to Use It

A strongly-typed, binary RPC protocol built on HTTP/2 — why it beats REST for internal service-to-service calls, and why it struggles outside that world.

9 min read
gRPCProtocol BuffersRPC

Most engineers meet gRPC after they've already felt the friction it's designed to remove: a REST endpoint whose JSON shape drifted from its documentation, a client that silently swallows a typo'd field name, or a service mesh sending the same small payload thousands of times a second with far more overhead than the payload deserves. gRPC is Google's answer to that friction — a remote procedure call framework built on two deliberate choices: Protocol Buffers for the contract, and HTTP/2 for the transport. Neither choice is exotic on its own, but together they make gRPC feel less like "calling an API" and more like calling a local function that happens to live on another machine.

The contract: Protocol Buffers

With REST, the "contract" between client and server is usually a JSON shape described in a Swagger/OpenAPI file, a wiki page, or — often enough — nothing at all beyond "whatever the server happens to send today." Nothing forces the client and server to agree; a renamed field or a string that quietly became a number is a runtime surprise, not a build-time error.

gRPC starts from the opposite end. You write a .proto file that defines every message and every method a service exposes, and the Protocol Buffers compiler generates strongly-typed client and server stubs from it — in Go, Java, Python, C++, whatever the consuming service is written in. The.proto file is the contract, checked into source control and versioned like code. Change a field's type or remove one that's still in use, and you find out at compile time, not in production.

On the wire, Protobuf messages are also binary and densely packed — each field is a numbered tag plus a value, with no repeated key names, no quotes, and no whitespace. A Protobuf payload is routinely a fraction of the size of the equivalent JSON, and encoding/decoding it is a tight, schema-driven operation rather than a general-purpose JSON parse.

protobuf
syntax = "proto3";

package orders.v1;

service OrderService {
  // Unary: place a single order, get a single confirmation back.
  rpc PlaceOrder (PlaceOrderRequest) returns (PlaceOrderResponse);

  // Server streaming: subscribe once, receive a stream of updates.
  rpc TrackOrder (TrackOrderRequest) returns (stream OrderUpdate);

  // Client streaming: send many events, get one summary back.
  rpc ReportTelemetry (stream TelemetryEvent) returns (TelemetryAck);

  // Bidirectional streaming: both sides send whenever they need to.
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

message PlaceOrderRequest {
  string customer_id = 1;
  repeated string sku_ids = 2;
}

message PlaceOrderResponse {
  string order_id = 1;
  OrderStatus status = 2;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_CONFIRMED = 1;
  ORDER_STATUS_REJECTED = 2;
}

The transport: HTTP/2

The other half of gRPC's design is its transport. REST-over-JSON typically rides on HTTP/1.1, where each connection handles one request at a time (or relies on awkward workarounds like pipelining). gRPC is built on HTTP/2, which multiplexes many independent request/response streams over a single TCP connection. That single design choice is what makes gRPC's streaming call types possible at all: the client and server can keep frames flowing in both directions on the same connection, interleaved with other calls, without opening a new socket for every request or blocking one call behind another.

HTTP/2's header compression and long-lived connections also cut the per-call overhead that adds up fast in a microservices mesh where one user request can fan out into dozens of internal calls — exactly the setting gRPC was built for.

Four call shapes, one framework

Because the transport supports multiplexed, bidirectional streams, gRPC exposes four distinct call types instead of REST's single request/response model:

  • Unary — one request, one response, the same shape as a REST call. A PlaceOrder call that takes an order and returns a confirmation is a natural fit.
  • Server streaming — one request, a stream of responses. A client subscribing to TrackOrder and receiving a status update every time the order moves through fulfillment is a clean example — no polling required.
  • Client streaming — a stream of requests, one final response. A device uploading a continuous run of telemetry readings and getting back a single acknowledgment once the stream closes fits this shape well.
  • Bidirectional streaming — both sides stream independently over the same connection. A live chat or a collaborative editing session, where either party can send at any time without waiting on the other, is the canonical case.

REST can approximate some of this with Server-Sent Events or WebSockets bolted on the side, but those are add-ons layered onto a protocol that wasn't designed for it. In gRPC, all four shapes are first-class citizens of the same .proto definition.

Strong typing cuts both ways

Generated stubs mean a Go service calling a Java service gets a native, type-checked client for free — no hand-written HTTP client, no guessing at field names. The cost is rigidity: every consumer needs the generated code, and a field type change is a breaking change that has to be coordinated, not something you can patch around at the JSON layer with an optional field.

Where gRPC wins

gRPC is at its best inside a system — service-to- service traffic in a polyglot microservices environment, where dozens of internal services written in different languages need to call each other constantly, and where the round-trip latency and payload size of each call actually matters at scale. Anywhere a service needs to stream — live updates, telemetry ingestion, chat — gRPC gives you that natively instead of as an afterthought. This is why it's become the default choice for internal RPC inside large distributed systems, often sitting behind a REST or GraphQL edge that translates public traffic into gRPC calls once it's inside the cluster.

Where gRPC struggles

gRPC is a poor fit for public-facing APIs. Browsers can't speak raw gRPC — it depends on HTTP/2 trailers that the browser fetch and XHR APIs don't expose — so reaching gRPC from a browser means adding grpc-web plus a proxy that translates between the two, extra infrastructure that a plain REST endpoint never needs. It's also a worse fit whenever human-readability matters more than raw performance: a support engineer curling an endpoint, a payload pasted into a bug report, or a webhook a third party needs to inspect and debug are all easier with self-describing JSON than with a binary Protobuf frame that needs the exact .proto file to decode.

Key takeaways

  • gRPC pairs Protocol Buffers (a strongly-typed, binary contract) with HTTP/2 (a multiplexed transport) to get low overhead and native streaming.
  • The .proto file is the source of truth — client and server stubs are generated from it, catching contract drift at compile time instead of at runtime.
  • Four call types — unary, server streaming, client streaming, bidirectional streaming — cover request/response and continuous-data use cases in one framework.
  • Strong typing and codegen trade flexibility for rigor: great for coordinated internal services, painful for loosely coupled or evolving public contracts.
  • gRPC shines for internal, polyglot, latency- or streaming-sensitive service-to-service calls; REST/JSON still wins for public APIs and anything that needs to stay human-readable.
2026 — Built by Arpit Mishra