All articles
APIsgRPCSystem Design13 min read

gRPC Explained: Protobuf, HTTP/2, Streaming and Deadlines

How gRPC works — Protobuf encoding, HTTP/2 framing, the four call types, deadlines and cancellation, status codes, load balancing, gRPC-Web and schema evolution — and when it beats REST.

What gRPC actually is

gRPC is a remote procedure call framework: you define services and messages in a .proto file, run a code generator, and call a remote method as if it were local. The transport is HTTP/2, the encoding is Protocol Buffers, and the contract is compulsory.

The reason it dominates internal service-to-service traffic is arithmetic. A JSON payload re-transmits every field name on every message and is parsed as text. Protobuf sends field numbers and packed binary values — typically 3–10× smaller, and an order of magnitude cheaper to encode and decode. At a million calls a minute between services, that is real money and real tail latency.

Protobuf: the wire format

syntax = "proto3";
package orders.v1;

service Orders {
  rpc GetOrder    (GetOrderRequest)     returns (Order);
  rpc ListOrders  (ListOrdersRequest)   returns (stream Order);
  rpc ImportBatch (stream Order)        returns (ImportSummary);
  rpc Watch       (stream WatchRequest) returns (stream OrderEvent);
}

message Order {
  string id        = 1;
  int64  total_min = 2;   // minor units — never float for money
  Status status    = 3;
  repeated Item items = 4;

  enum Status { STATUS_UNSPECIFIED = 0; PENDING = 1; SHIPPED = 2; }
}
The proto file is the single source of truth for both sides.
  • Field numbers, not names, go on the wire. Renaming a field is free; renumbering is a breaking change.
  • Every enum must have a zero value — proto3 has no way to distinguish “unset” from the default otherwise.
  • Fields are optional by default; there is no required. This is deliberate, and it is what makes evolution safe.
  • The binary payload is unreadable without the schema — plan for grpcurl and server reflection in dev.

Why HTTP/2 matters

HTTP/2 featureWhat gRPC gets from it
Multiplexed streamsThousands of concurrent calls on one TCP connection
Binary framingMessage boundaries without extra protocol work
HPACK header compressionMetadata is nearly free on repeated calls
Flow controlBackpressure on streams, so a slow consumer does not blow up memory
TrailersStatus is sent after the body — essential for streaming errors
Long-lived connectionsNo per-call handshake cost
Trailers are why gRPC needs HTTP/2
A streaming response cannot know it failed until halfway through. gRPC sends the final status in HTTP trailers, after the body. HTTP/1.1 has no usable trailer support — which is exactly why gRPC-Web needs a translating proxy.

The four call types

Unary1 request → 1 responseclientserverServer streaming1 request → N responses←←←clientserverClient streamingN requests → 1 response→→→clientserverBidirectionalN ⇄ N, independent→→→←←←clientserverAll four run over a single multiplexed HTTP/2 connection — no extra sockets, no upgrade dance.
Streaming is first-class, not a bolt-on. This is gRPC's biggest advantage over REST.
  • Unary — the ordinary request/response, ~90% of real usage.
  • Server streaming — large result sets, live feeds, progress updates, LLM tokens.
  • Client streaming — bulk uploads, telemetry ingestion, chunked files.
  • Bidirectional — chat, collaborative editing, real-time sync. Both sides send independently.

Deadlines, cancellation and metadata

Deadlines are the feature most REST stacks bolt on badly and gRPC has built in. A deadline is absolute, and it propagates: if A calls B with 300ms remaining and B calls C, C receives the remaining budget and gives up when it expires.

// Go
ctx, cancel := context.WithTimeout(ctx, 300*time.Millisecond)
defer cancel()

md := metadata.Pairs("x-request-id", reqID, "authorization", "Bearer "+tok)
ctx = metadata.NewOutgoingContext(ctx, md)

order, err := client.GetOrder(ctx, &pb.GetOrderRequest{Id: "8812"})
if status.Code(err) == codes.DeadlineExceeded { /* shed, do not retry blindly */ }
A deadline is a wall-clock instant, not a per-hop timeout.
  • Always set a deadline. A call without one can hang until the connection dies.
  • Cancellation propagates downstream, so abandoned work actually stops burning CPU.
  • Metadata is the header equivalent — auth tokens, trace context, tenant ids.

Status codes and error handling

gRPC codeRough HTTP equivalentRetryable?
OK200n/a
INVALID_ARGUMENT400No
UNAUTHENTICATED401After refreshing credentials
PERMISSION_DENIED403No
NOT_FOUND404No
ALREADY_EXISTS409No
FAILED_PRECONDITION412No — fix state first
ABORTED409Yes, at a higher level
RESOURCE_EXHAUSTED429Yes, with backoff
UNAVAILABLE503Yes, with backoff and jitter
DEADLINE_EXCEEDED504Carefully — the work may have completed
INTERNAL / UNKNOWN500No

Rich error details travel in google.rpc.Status as typed messages — BadRequest.FieldViolation, RetryInfo, QuotaFailure — so clients branch on structure rather than parsing strings.

Load balancing and service discovery

An L4 load balancer will ruin your day
gRPC holds one long-lived connection and multiplexes everything over it. A TCP load balancer assigns that single connection to one backend, and all your traffic pins to one pod while the rest idle. You need L7 (Envoy, a service mesh) or client-side balancing over resolved endpoints.
  • Client-side LB — resolve all endpoints, pick per call. Lowest latency, more client complexity.
  • Proxy / mesh — Envoy or linkerd terminates HTTP/2 and balances per request. Simplest operationally.
  • Set MAX_CONNECTION_AGE so connections recycle and new pods actually receive traffic.
  • Use the standard health-checking protocol so orchestrators can probe properly.

Schema evolution rules

ChangeSafe?Note
Add a new field with a new numberYesOld readers ignore it
Rename a fieldYes on the wireBreaks generated code — coordinate the deploy
Change a field's numberNoSilent data corruption
Change a field's typeUsually noSome int widenings are compatible; verify
Delete a fieldOnly with `reserved`Reserve the number and name forever
Add an enum valueYesClients must handle unknown values
Add an RPCYesOld clients simply never call it
message Order {
  reserved 5, 7 to 9;
  reserved "legacy_status";
}

gRPC-Web and the browser

Browsers cannot set HTTP/2 trailers or control framing, so they cannot speak native gRPC. gRPC-Web solves it with a proxy (Envoy or a framework middleware) that translates. The cost: client streaming and bidirectional streaming are not supported — you get unary and server streaming only.

Connect is a popular alternative protocol that speaks gRPC, gRPC-Web and a plain JSON-over-HTTP/1.1 mode from the same handlers, which means curl works again.

gRPC vs REST

gRPCREST + JSON
Payload size3–10× smallerVerbose, field names repeated
ContractMandatory .proto with codegenOptional OpenAPI
StreamingFirst-class, four modesSSE or WebSockets, bolted on
Browser supportNeeds gRPC-Web + proxyNative
CachingNoneFull HTTP caching
DebuggabilityNeeds grpcurl and the schemacurl and eyes
Deadlines / cancellationBuilt in and propagatingManual per hop
Best fitInternal, high-volume, polyglotPublic, cacheable, browser-facing

Interview framing

“Internal services talk gRPC over HTTP/2 with Protobuf: payloads are a fraction of the JSON size and one multiplexed connection removes per-call handshakes at our request volume. Every call carries an absolute deadline that propagates downstream, so a slow dependency sheds work instead of pinning threads across the fleet. We balance at L7 through the mesh, because an L4 balancer would pin every request to one backend. Schema changes only ever add fields with new numbers, and deleted numbers are reserved permanently. The public edge stays REST, so it remains cacheable at the CDN and callable from a browser.”

Keep reading

Suggested next articles based on this one.

Design it, don't just read it.

Practise LLD and system design problems with structured rubrics and AI feedback.

Start practising free