gRPC
gRPC — recursively expanded as “gRPC Remote Procedure Call” — is a high-performance, language-agnostic Remote Procedure Call (RPC) framework originally developed at Google as the public successor to its internal Stubby RPC system, and open-sourced under the Cloud Native Computing Foundation in February 2015. It pairs three components into a single coherent stack: an Interface Description Language (IDL) called Protocol Buffers (protobuf) in which a service’s methods and message shapes are declared; a code generator (
protoc) that emits client and server stubs in eleven officially supported languages from a single.protofile; and HyperText Transfer Protocol version 2 (HTTP/2) as the transport, which gives it stream multiplexing over a single connection, header compression, flow control, and binary framing — the properties that make it materially faster than REST-over-HTTP/1.1 at the same workload. The design rationale, articulated explicitly in Louis Ryan’s 2015 “gRPC Motivation and Design Principles”, is that Google’s internal experience with Stubby for over a decade had taught them that microservices in the tens of thousands need strict contracts, streaming primitives, deadlines and cancellation, binary efficiency, and language portability — all things REST over JSON either does not have or has only by convention. gRPC has since become the de facto standard for service-to-service communication in modern infrastructure: Kubernetes’ control plane, etcd, Envoy’s xDS configuration plane, the entire Istio service mesh, CockroachDB’s internal RPC, Square’s microservices, Netflix’s intra-DC traffic, and most modern data-platform systems. This note covers what gRPC is, why it exists, how it works on the wire, the four streaming modes, and the practical operational pitfalls that catch teams the first time they deploy it.
1. Plain-Language Definition
The mental model: in REST, you talk to a server by sending it an HTTP request whose URL names a resource and whose method describes what to do to it; in gRPC, you talk to a server by calling a function — a function whose name, argument types, and return type were agreed in advance via a .proto file, and whose call is then transmitted over the network as if it were a local function invocation. The framework hides the network from the programmer: the client invokes client.GetUser(request) and gets back a User object; the network transport, the binary serialization, the HTTP/2 framing, the deadline propagation, and the error mapping are all generated machinery that neither the client author nor the server author has to write.
The name “RPC” — Remote Procedure Call — is forty years old; the foundational paper is Birrell and Nelson’s Implementing Remote Procedure Calls (ACM Transactions on Computer Systems, 1984), which articulated the goal of “making a remote call look like a local call.” That goal has been pursued by many systems — Sun RPC (the basis of Network File System), Distributed Computing Environment (DCE) RPC, the Common Object Request Broker Architecture (CORBA), Microsoft’s DCOM, Apache Thrift, Google’s Stubby. gRPC is the latest entrant, and the one currently winning, because it solved three problems prior systems didn’t: cross-language code generation that actually works, a transport (HTTP/2) that traverses modern infrastructure (proxies, load balancers, firewalls expect HTTP), and an open governance model that brought ten-plus language ecosystems on board.
When an engineer says “this service uses gRPC,” they typically mean: there is a .proto file checked into source control; the service implements a generated server interface; the clients import a generated client; messages are protobuf-binary, not JSON; the transport is HTTP/2. They may or may not mean the service uses streaming — most internal services use only the unary mode, which is the call-and-return shape that mimics REST.
2. Mechanism — How gRPC Works on the Wire
The full picture has four moving parts: the IDL, the code generator, the wire format, and the HTTP/2 transport.
2.1 The Interface Description Language: Protocol Buffers (.proto)
The contract starts as a .proto file. Here is a representative service declaration in proto3, the current syntax:
syntax = "proto3";
package blog.v1;
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
rpc CreateUser(stream CreateUserChunk) returns (User);
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
message User {
string id = 1;
string name = 2;
string email = 3;
int64 created_at_unix_seconds = 4;
}
message GetUserRequest {
string id = 1;
}The .proto file is the source of truth: shape of every message, name of every method, presence and direction of streaming. The numbered fields (= 1, = 2) are wire tags, not ordinals — these numbers are encoded into every message on the wire, and they are forever-promises. Once a field has tag 4, it must always have tag 4 across all future versions of the schema, or old clients reading new servers (or vice versa) will misparse. This is the protobuf forward/backward-compatibility discipline: add fields with new numbers (never reuse), mark removed fields reserved (so the number is never accidentally repurposed), and keep field types stable. The schema can evolve forever as long as these rules hold; this is a hard property gRPC inherits from protobuf and that REST-with-JSON does not have automatically.
2.2 Code Generation: protoc and Language Plugins
The protoc compiler reads .proto files and emits target-language code via plugins:
$ protoc --go_out=. --go-grpc_out=. blog/v1/user.proto
For Go this produces user.pb.go (message types) and user_grpc.pb.go (client and server interfaces). The generated client looks roughly like:
type UserServiceClient interface {
GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error)
ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (UserService_ListUsersClient, error)
CreateUser(ctx context.Context, opts ...grpc.CallOption) (UserService_CreateUserClient, error)
Chat(ctx context.Context, opts ...grpc.CallOption) (UserService_ChatClient, error)
}The server side gets a corresponding UserServiceServer interface that the application implements. The framework wires them up; the application author never writes serialization, framing, or HTTP-handling code. This is the whole point of an IDL-based RPC system — the boilerplate that REST asks you to write per-endpoint (parsing query strings, marshalling JSON, handling content negotiation) is generated once and forgotten.
Officially supported languages as of writing: C++, Java, Python, Go, Ruby, C#, Node.js, Objective-C, PHP, Dart, Kotlin. Community plugins exist for Rust, Swift, and others. The cross-language reach is what makes gRPC viable as a polyglot company’s standard internal RPC.
2.3 The Wire Format: Protocol Buffers Binary Encoding
When client.GetUser({id: "42"}) is called, the runtime serializes the GetUserRequest message to its protobuf wire format: a sequence of (tag, wire-type, value) records. The tag is the field number from the .proto; the wire-type is one of varint (variable-length integer), 64-bit, length-delimited, or 32-bit; the value is the actual bytes. Strings are length-prefixed UTF-8; integers are varint-encoded (small numbers take one byte). Field names from the .proto are not on the wire — only the numbers — which is why the numbers are forever-promises and why protobuf payloads are so compact.
Concrete numbers from Google’s own measurements: a typical message that takes 200 bytes as JSON takes 60–80 bytes as protobuf — roughly a 3× shrink — and parses 5–10× faster because there is no string-tokenization step. For a service handling 50K requests per second, this is meaningful. The cost is human-readability: a wireshark capture of gRPC traffic without the .proto is gibberish, where REST traffic is at least squintable. (gRPC reflection and grpcurl recover most of this debuggability if reflection is enabled on the server.)
2.4 The Transport: HTTP/2
Every gRPC call is an HTTP/2 request. The HTTP path encodes the service and method:
:method = POST
:scheme = https
:path = /blog.v1.UserService/GetUser
content-type = application/grpc
te = trailers
grpc-timeout = 1S
The body is one or more length-prefixed protobuf frames (5-byte prefix: 1 byte compression flag, 4 bytes length, then the protobuf bytes). The response status comes in HTTP/2 trailers — a separate set of headers sent after the body — specifically grpc-status and grpc-message, which is why a gRPC client requires te: trailers advertisement.
HTTP/2 brings the four properties that make gRPC fast:
- Multiplexing: many concurrent RPCs share a single TCP/TLS connection, each as a separate HTTP/2 stream. There is no head-of-line blocking the way HTTP/1.1 pipelining had.
- Binary framing: HTTP/2 frames are binary, length-prefixed; no parsing of
\r\nlines as in HTTP/1.1. - Header compression (HPACK, RFC 7541): repeated headers (auth tokens, user-agent, etc.) compress to almost nothing across requests on the same connection.
- Flow control: per-stream and per-connection windows let one party throttle the other, preventing a fast producer from drowning a slow consumer at the framing layer.
These four are the reason a single gRPC connection sustains tens of thousands of in-flight RPCs without TCP teardown overhead, and the reason naive Layer-4 (TCP-level) load balancers do badly with gRPC — which we’ll come back to in §6.
2.5 Status Codes
gRPC defines its own status-code namespace (16 codes) rather than reusing HTTP status:
OK (0)
CANCELLED (1), UNKNOWN (2), INVALID_ARGUMENT (3), DEADLINE_EXCEEDED (4),
NOT_FOUND (5), ALREADY_EXISTS (6), PERMISSION_DENIED (7), RESOURCE_EXHAUSTED (8),
FAILED_PRECONDITION (9), ABORTED (10), OUT_OF_RANGE (11), UNIMPLEMENTED (12),
INTERNAL (13), UNAVAILABLE (14), DATA_LOSS (15), UNAUTHENTICATED (16)
These travel in the grpc-status trailer. Each maps to a specific exception class in the generated client. The semantics are deliberately tighter than HTTP’s: RESOURCE_EXHAUSTED is unambiguously “rate limited / quota hit”; FAILED_PRECONDITION is “system state forbids this right now”; ABORTED is “we tried but transient race lost.” This namespace guides retry policy: UNAVAILABLE and RESOURCE_EXHAUSTED are retryable with backoff; FAILED_PRECONDITION and INVALID_ARGUMENT are not (retrying won’t help).
3. The Four Method Types
gRPC’s most distinctive feature compared to REST is first-class streaming. Every RPC is one of four shapes, declared in the .proto with the stream keyword on the request side, response side, both, or neither.
3.1 Unary RPC
One request, one response. This is the call-and-return shape that mimics REST.
rpc GetUser(GetUserRequest) returns (User);Use case: read one record, perform one transactional write, validate one input. The vast majority of method definitions in any real service. Latency: one round trip.
3.2 Server Streaming RPC
One request, a stream of responses.
rpc ListUsers(ListUsersRequest) returns (stream User);The client sends a single message; the server returns many messages over a single HTTP/2 stream until it closes the stream with a grpc-status trailer. Use cases: paginated listings without the round-trip-per-page cost of REST cursor pagination; live feeds; streaming search results; anything where the result set is unbounded or arrives over time. Common pattern: a watch API. etcd’s Watch RPC is a server-streaming RPC that pushes key-change events to the client; the client opens it once and receives notifications until it cancels.
3.3 Client Streaming RPC
A stream of requests, one response.
rpc CreateUser(stream CreateUserChunk) returns (User);The client sends many messages, then half-closes the stream; the server processes them and returns one response. Use cases: file upload (each chunk a message); streaming aggregation where the client is the data source and the server is the aggregator (per-second metric points → a daily summary).
3.4 Bidirectional Streaming RPC
A stream of requests and a stream of responses, independently, on the same HTTP/2 stream.
rpc Chat(stream ChatMessage) returns (stream ChatMessage);Each side can send messages whenever it wants; the framing keeps them separate. Use cases: chat (each user sends as they type, sees others’ messages as they arrive); collaborative editing operational-transform protocols; real-time RPC where back-pressure flows in both directions. This is the mode where gRPC’s HTTP/2 flow control matters most: a slow consumer causes the producer’s Send() to block via the flow-control window, which is the back-pressure most ad-hoc message-queue solutions get wrong.
4. Origins — From Stubby to gRPC
Google had been running an internal RPC framework called Stubby since the mid-2000s. Stubby was Google-specific, dependent on internal infrastructure (GFS, Chubby, Borg), and used a non-standard transport. It was, by all accounts, the lifeblood of Google’s microservice architecture — every internal service called every other internal service via Stubby — and its design choices (binary protobuf bodies, rich status codes, deadlines, cancellation, structured metadata) reflected over a decade of operational experience at scale.
The motivation to extract and re-engineer Stubby as the open-source gRPC, articulated in Louis Ryan’s 2015 design-principles post, came from three observations: (1) the broader industry was reinventing the same wheel poorly (every company built its own RPC system, none of them as good as Stubby); (2) Google’s own move to public cloud (Google Cloud Platform) needed a customer-usable RPC story to compete with AWS’s offerings; (3) the standardization of HTTP/2 in 2015 (RFC 7540, now RFC 9113) finally provided a transport that had the properties Stubby needed — multiplexing, flow control, header compression — and that infrastructure already understood. gRPC’s first public release was February 2015. By 2017 it was a CNCF graduated project; by 2020 it was the dominant intra-cluster RPC of the cloud-native ecosystem.
The genealogy matters because it explains design choices: the rich error namespace, the deadline-and-cancellation propagation, the streaming primitives, the strict-by-default schema discipline are not invented features — they are properties that survived 12 years of internal Google production load and were preserved on extraction.
5. Worked Example — A UserService from .proto to Production
Let us trace a complete gRPC service end to end.
5.1 The .proto File
syntax = "proto3";
package blog.v1;
option go_package = "blog/v1;blogv1";
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
rpc CreateUser (CreateUserRequest) returns (User);
rpc UpdateUser (UpdateUserRequest) returns (User);
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
message User {
string id = 1;
string name = 2;
string email = 3;
int64 created_at_unix_seconds = 4;
reserved 5, 6; // were age and address; never reuse these tags
repeated string roles = 7; // added in v1.2 — old clients ignore unknown fields
}
message GetUserRequest { string id = 1; }
message ListUsersRequest { int32 page_size = 1; string page_token = 2; }
message CreateUserRequest { string name = 1; string email = 2; }
message UpdateUserRequest {
string id = 1;
User patch = 2;
google.protobuf.FieldMask update_mask = 3; // which fields to update
}
message ChatMessage { string from = 1; string text = 2; int64 sent_at = 3; }A few things to note. reserved 5, 6 is the declaration that those tag numbers were used historically and must never be reused — the protobuf compiler refuses to compile a file that adds a new field with those numbers, preventing accidental data corruption. repeated string roles was added in v1.2 of the schema; old clients receive it as an unknown field and silently drop it; new servers reading old client messages see no roles and treat it as the default empty list. Backward compatibility on both directions, by construction.
google.protobuf.FieldMask is the standard partial-update pattern: the client sends a list of field paths (["name", "email"]) plus a full User patch, and the server updates only the listed fields. This is gRPC’s analog to REST’s PATCH, with a strict and machine-readable shape.
5.2 Generated Client Usage (Go)
conn, err := grpc.NewClient(
"users.svc.cluster.local:9000",
grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg)),
)
if err != nil { return err }
defer conn.Close()
client := blogv1.NewUserServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
defer cancel()
user, err := client.GetUser(ctx, &blogv1.GetUserRequest{Id: "42"})
if status.Code(err) == codes.NotFound {
return ErrUserNotFound
}The context.WithTimeout is the gRPC deadline — the runtime serializes the deadline into the grpc-timeout header on the outgoing request, the server sees it, the server’s own runtime enforces it, and if the server makes a downstream gRPC call, the remaining time propagates again. This deadline propagation chains through arbitrarily many service hops and is one of gRPC’s most underappreciated features. Without it, a 250 ms client deadline can be silently ignored by a downstream service that takes 10 seconds; with it, every hop knows the budget and gives up correctly. (See Timeout and Deadline Pattern for the broader discussion.)
Cancellation works the same way: cancel the context, the runtime sends an HTTP/2 RST_STREAM to abort the call, the server’s context cancels, downstream calls cancel, and resources are freed promptly. REST has no equivalent — closing an HTTP/1.1 connection is a poor approximation that does not propagate.
5.3 Bidirectional Streaming: A Chat Service
The chat method’s server implementation:
func (s *userServer) Chat(stream blogv1.UserService_ChatServer) error {
for {
msg, err := stream.Recv()
if err == io.EOF { return nil }
if err != nil { return err }
s.broadcast(msg) // route to other connected clients
// we may also Send on the same stream to push back
if reply := s.replyFor(msg); reply != nil {
if err := stream.Send(reply); err != nil { return err }
}
}
}The single stream parameter has both Recv() and Send(). The two are independent — the server may send replies that have nothing to do with what the client just sent, or send nothing for many client messages, or send several replies for one. This is the bidirectional-streaming flexibility that REST does not have.
5.4 Schema Evolution Discipline
Before §5 closes, a worked discipline. Suppose the User schema must add a phone field:
message User {
string id = 1;
string name = 2;
string email = 3;
int64 created_at_unix_seconds = 4;
reserved 5, 6;
repeated string roles = 7;
string phone = 8; // new in v1.3
}What breaks? Nothing. Old servers receiving new client messages with a phone field encode it (the bytes are on the wire) but the old server’s parser ignores unknown tags by default. Old clients receiving new server messages with phone set: same thing — the field is on the wire, the old client’s parser drops it. New clients reading old server responses: phone is empty string (proto3 default). Everyone keeps working.
What does break: removing a field by deleting the line (rather than reserved); changing a field’s type (int32 to string); reusing a tag number; promoting a non-repeated field to repeated. Each of these can produce silent data corruption rather than a clean error, which is why protobuf evolution rules are taught with the same gravity as database migration discipline.
6. Tradeoffs and Comparison
6.1 gRPC vs. REST
gRPC is faster in absolute terms (binary, HTTP/2, generated stubs), supports streaming natively, has strict contracts and language portability, and has structured deadlines and cancellation. REST is simpler to debug (curl works), traverses any HTTP infrastructure including legacy proxies, has a richer ecosystem of caching and rate-limiting tools, and is the universal expectation for public APIs. The decision tree most teams use:
- Public API for third parties? → REST. Anyone can consume it; no proto file to ship.
- Internal service-to-service in a polyglot environment? → gRPC. The IDL pays for itself many times over.
- High-throughput, latency-sensitive internal RPC (control plane, data path)? → gRPC, especially with streaming.
- Browser client? → REST or GraphQL; gRPC requires gRPC-Web shim.
- Mobile client? → Either. gRPC’s compactness helps on cellular; REST is simpler.
6.2 gRPC vs. GraphQL
Both are alternatives to REST, but for opposite reasons. GraphQL solves over- and under-fetching by letting the client specify field selection. gRPC solves contract rigor and performance with binary encoding and code generation. They are not really competitors; they often coexist (GraphQL at the client edge, gRPC between backends).
6.3 gRPC-Web
Browsers cannot use raw gRPC because they cannot fully control HTTP/2 from JavaScript (no access to trailers, no manual stream framing). The gRPC-Web protocol is a translation: the browser speaks a slightly-modified HTTP/1.1- or HTTP/2-friendly variant, a proxy (Envoy or grpc-web-proxy) transcodes to real gRPC. The trade-off is loss of client-streaming and bidirectional-streaming — gRPC-Web supports unary and server-streaming only as of writing. For full duplex in a browser, WebSocket Protocol or Server-Sent Events remain the practical choice.
7. Real-World Examples
Google internal everything. The original use case; Stubby plus its successor handle Google’s intra-DC traffic on the order of trillions of RPCs per day.
etcd, the distributed key-value store at the heart of Kubernetes’ control plane, exposes its entire API as gRPC. The Watch RPC is server-streaming; transactions are unary. Every kubectl command ultimately becomes gRPC traffic to etcd via the API server.
Envoy’s xDS configuration plane. Envoy proxies pull configuration (routes, clusters, listeners) from a control plane via four gRPC bidirectional-streaming RPCs (LDS, RDS, CDS, EDS). This is the substrate Istio’s service mesh is built on.
CockroachDB and TiDB use gRPC for inter-node RPC (Raft messages, range RPCs, distributed SQL execution).
Netflix uses gRPC heavily for intra-DC traffic; their public API still leans REST/GraphQL.
Square’s microservices standardized on gRPC company-wide circa 2017.
TensorFlow Serving exposes inference endpoints as gRPC for low-latency model serving.
8. Pitfalls
Load balancing. This is the canonical operational pitfall. gRPC connections are long-lived HTTP/2 connections; a Layer-4 TCP load balancer (the simplest kind) chooses one backend at connection time and pins all subsequent RPCs there. With ten clients and ten servers behind an L4 LB, you can end up with all ten clients on three servers and seven servers idle. The fix is one of: (a) Layer-7 LB that load-balances per RPC rather than per connection — Envoy, NGINX with the gRPC module, the cloud providers’ L7 LBs; (b) client-side load balancing where the client opens connections to multiple backends and picks one per call (the canonical gRPC LB strategy, supported via the xds: resolver and DNS resolver with multiple A records); (c) headless services in Kubernetes that return all pod IPs so the client connects to each.
Deadlines and RESOURCE_EXHAUSTED. Without explicit deadlines, a slow server can hang clients indefinitely (the default in some clients is no deadline). Make deadline-setting a code-review checklist item; pair with the Timeout and Deadline Pattern.
Schema evolution mistakes. Reusing tag numbers, changing types, removing fields without reserved — each is a silent corruption hazard. Use buf lint or protolock in CI to enforce proto compatibility.
Reflection vs. schema availability. A grpcurl-style debug tool needs the .proto schema to format messages. Production servers often disable reflection for security (don’t expose method list); plan how operators get schemas — usually via a registry that ships the .proto files alongside the service.
Inadequate retry policy. Naive retries on INTERNAL or UNAVAILABLE without backoff can stampede a recovering service. gRPC’s retry policy supports configurable attempts, codes, and exponential backoff via service config; configure it explicitly rather than relying on the client default.
Browser support requires gRPC-Web. Teams sometimes adopt gRPC for a backend then realize a frontend SPA needs an Envoy proxy and gRPC-Web to call it, plus a workaround for the missing client-stream/bidirectional-stream modes.
Debugging without text payloads. A captured gRPC packet is opaque. Mitigation: enable reflection in dev/staging, use grpcurl, log message structures (with PII redaction) at the application layer; never rely on packet capture as the primary debugging path.
Connection limits and HTTP/2 max-streams. HTTP/2 has a per-connection SETTINGS_MAX_CONCURRENT_STREAMS (commonly 100). A high-fan-out client can exceed this and queue locally; multiple connections or a higher setting are the fix.
TLS termination at intermediaries. Some intermediaries (older AWS Application Load Balancers historically) couldn’t speak HTTP/2 to the backend; you’d terminate TLS at the LB and downgrade to HTTP/1.1 to the server, breaking gRPC. Always verify HTTP/2 end-to-end.
Verbose binary errors over JSON-style errors. Internal services should map gRPC status codes to HTTP for any external surface (an API gateway in front of a gRPC backend usually does this); without the mapping, browsers and tooling get confused statuses.
9. Interview Discussion
In a senior system-design interview, gRPC discussion runs along the following grooves:
- “Why gRPC over REST for this internal API?” Answer: contract rigor (proto), code generation, binary efficiency, streaming, deadline propagation. Concrete: 3× smaller payloads, sub-millisecond unary p50.
- “How do you load-balance gRPC?” Expect to know the L4 pitfall and the L7-or-client-side fix. Naming Envoy or xDS scores highly.
- “How do you evolve a proto schema without breaking clients?” The
reserveddiscipline; never reuse tags; additive changes only. - “What does deadline propagation buy you?” Without it, hop-by-hop timeouts compound and clients hang past their nominal budget; with it, the entire call graph honors a single budget.
- “When wouldn’t you use gRPC?” Public API (REST), browser client without gRPC-Web (WebSocket or SSE), or polyglot environments where one ecosystem’s gRPC support is weak.
- “How do you debug gRPC in production?”
grpcurl, reflection (or shipped protos), structured logging at the application layer, distributed tracing across hops.
The depth signal is whether you treat gRPC as “REST but binary” (shallow) or as a coherent system whose properties — streaming, deadlines, codes, code generation — interact (deep). Articulating why each property exists — what failure mode in pre-Stubby Google made it necessary — is the level interviewers are listening for.
10. See Also
- Representational State Transfer — the alternative for public APIs
- GraphQL — the client-shaped query alternative
- WebSocket Protocol — when streaming is needed in the browser
- Server-Sent Events — simpler unidirectional push alternative
- Service Mesh System Design — the data-plane layer that often manages gRPC traffic
- API Gateway System Design — frequently transcodes gRPC to REST at the edge
- Microservices Architecture — the architecture inside which gRPC is most commonly used
- Backend for Frontend Pattern — a BFF tier may speak REST/GraphQL outward and gRPC inward
- Client-Server Architecture — the foundational pattern gRPC instantiates
- Timeout and Deadline Pattern — gRPC’s deadline-propagation feature in context
- SWE Interview Preparation MOC
- Major System Designs MOC