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; }
}- 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
grpcurland server reflection in dev.
Why HTTP/2 matters
| HTTP/2 feature | What gRPC gets from it |
|---|---|
| Multiplexed streams | Thousands of concurrent calls on one TCP connection |
| Binary framing | Message boundaries without extra protocol work |
| HPACK header compression | Metadata is nearly free on repeated calls |
| Flow control | Backpressure on streams, so a slow consumer does not blow up memory |
| Trailers | Status is sent after the body — essential for streaming errors |
| Long-lived connections | No per-call handshake cost |
The four call types
- 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 */ }- 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 code | Rough HTTP equivalent | Retryable? |
|---|---|---|
| OK | 200 | n/a |
| INVALID_ARGUMENT | 400 | No |
| UNAUTHENTICATED | 401 | After refreshing credentials |
| PERMISSION_DENIED | 403 | No |
| NOT_FOUND | 404 | No |
| ALREADY_EXISTS | 409 | No |
| FAILED_PRECONDITION | 412 | No — fix state first |
| ABORTED | 409 | Yes, at a higher level |
| RESOURCE_EXHAUSTED | 429 | Yes, with backoff |
| UNAVAILABLE | 503 | Yes, with backoff and jitter |
| DEADLINE_EXCEEDED | 504 | Carefully — the work may have completed |
| INTERNAL / UNKNOWN | 500 | No |
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
- 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_AGEso connections recycle and new pods actually receive traffic. - Use the standard health-checking protocol so orchestrators can probe properly.
Schema evolution rules
| Change | Safe? | Note |
|---|---|---|
| Add a new field with a new number | Yes | Old readers ignore it |
| Rename a field | Yes on the wire | Breaks generated code — coordinate the deploy |
| Change a field's number | No | Silent data corruption |
| Change a field's type | Usually no | Some int widenings are compatible; verify |
| Delete a field | Only with `reserved` | Reserve the number and name forever |
| Add an enum value | Yes | Clients must handle unknown values |
| Add an RPC | Yes | Old 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
| gRPC | REST + JSON | |
|---|---|---|
| Payload size | 3–10× smaller | Verbose, field names repeated |
| Contract | Mandatory .proto with codegen | Optional OpenAPI |
| Streaming | First-class, four modes | SSE or WebSockets, bolted on |
| Browser support | Needs gRPC-Web + proxy | Native |
| Caching | None | Full HTTP caching |
| Debuggability | Needs grpcurl and the schema | curl and eyes |
| Deadlines / cancellation | Built in and propagating | Manual per hop |
| Best fit | Internal, high-volume, polyglot | Public, 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.”