If you remember five things
The whole answer in one glance. Everything below defends these five calls.
- Binary wire, strict contract. Protobuf encodes field tags plus values, no field names, and a .proto file is the single source of truth both sides generate from.
- HTTP/2 underneath. You get multiplexing, header compression, and four call types (unary plus three streaming modes) for free, no WebSockets bolted on.
- Inside, not at the edge. gRPC for service-to-service where you own both ends, REST or GraphQL for anything a browser or third party touches.
- Field tags are the identity. Never reuse a tag number, mark removed ones reserved, and know which schema changes are wire-compatible before you ship.
- Long-lived connections break naive load balancing. One pinned HTTP/2 connection carries every multiplexed call, so you need L7 or client-side balancing.
1The core idea#
gRPC lets one program call a function in another program on a different machine, and have it feel almost like a local call. You call getUser(42), a service across the network runs the code, and you get a User back. That pattern (RPC, remote procedure call) is decades old. What makes gRPC specific is two choices it bakes in.
Two choices that define it:
- Messages are protobuf, a binary format. Not text, not JSON. A tight, compact byte layout.
- It runs on HTTP/2. That buys multiplexing, streaming, and header compression at no extra cost.
The mental model I use:
- You write a contract in a
.protofile: this service has these methods, each takes this message, returns that message. - A code generator reads the contract and emits real client and server code in your language (Go, Java, Python, Rust).
- The client stub looks like a plain object with methods. You call one, gRPC handles serialize, network, deserialize, and hands you a typed object back.
- The
.protois the single source of truth. Both sides generate from it, so they agree on every message shape before a byte moves.
A tiny example of what that contract looks like:
syntax = "proto3";
message GetUserRequest {
int64 user_id = 1;
}
message User {
int64 id = 1;
string name = 2;
string email = 3;
}
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
Those = 1, = 2 numbers are field tags, and they are the actual identity of a field on the wire. The field name is only for your source code. Half the gotchas later in this guide trace back to that single fact.
2How it works#
Let me trace one call end to end. The payoff is understanding why the wire stays so small.
The serialize step (the headline feature):
- Your client calls
GetUser(req)and the generated stub serializes it to protobuf bytes. - Protobuf does not write field names. It writes the tag number plus the value.
- So instead of
{"user_id": 42}(18 bytes of text) you get roughly 2 bytes: one for the tag, one for the value.
The transport step:
- Those bytes ride an HTTP/2 stream.
- HTTP/2 lets many requests share one TCP connection at once without blocking each other.
- A single connection can carry hundreds of in-flight calls.
- The server runtime deserializes back to a
GetUserRequest, runs your handler, and theUsercomes back the same way in reverse.
The part people miss: gRPC has four call types, not just request-response.
- Unary. One request, one response. The normal case.
- Server streaming. One request, a stream of responses. Good for "give me all events since timestamp X."
- Client streaming. A stream of requests, one response. Good for uploading chunks and getting a final summary.
- Bidirectional streaming. Both sides stream independently over the same connection. Live chat, telemetry, a trading feed.
The streaming comes from HTTP/2 natively. You do not bolt on WebSockets or long polling. It is part of the model.
Here is where gRPC usually sits in a system:
3A worked example: what the wire savings actually buy you#
Numbers make this concrete, and interviewers love when you reach for them instead of hand-waving "it is faster." Let me size a real path: the Order Service fans out to the Inventory Service on every checkout, at 50,000 requests per second on a big sale day.
One User-style record (int64 id, 20-char name, 30-char email, a couple of small numeric fields):
- As JSON: field names repeated every message, quotes, braces, commas. Around 180 bytes.
- As protobuf: tag bytes plus raw values, no field names. Around 70 bytes.
- That is roughly a 60 percent cut per message.
Now scale it across that one hop:
- Request plus response is about 100,000 messages per second.
- Savings per message: 110 bytes.
- 100,000 messages/s times 110 bytes = 11 MB/s saved on the wire.
- Over a day that is about 950 GB of traffic you simply do not pay for on that single hop.
The parse cost is the other half, and the bigger win on hot paths:
- JSON parsing scans text, matches field-name strings, and allocates.
- Protobuf reads tag numbers and copies bytes into fixed struct fields.
- On typical service workloads protobuf decode runs several times faster and allocates far less, so less garbage collector pressure.
- On a service pinned at 50,000 req/s, cutting deserialization CPU can be the difference between running 8 pods and running 12.
The honest caveat I want to hear next: for one request on a cold path, nobody feels 110 bytes or a few microseconds. These wins only matter in aggregate, on hot internal hops where you control both ends. A candidate who adds that caveat unprompted has clearly run gRPC, not just read about it.
4Trade-offs#
Nothing is free. Here is what you give up and what you gain, side by side.
| Dimension | gRPC + protobuf | REST + JSON |
|---|---|---|
| Payload size | Small, binary (around 70 bytes in the example) | Larger, text (around 180 bytes) |
| Speed to parse | Fast, low allocation | Slower, scans text and allocates |
| Human readable on the wire | No | Yes |
| Browser support | Needs gRPC-Web proxy | Native |
| Contract enforcement | Strong, from .proto, fails at build time | Loose unless you add OpenAPI |
| Streaming | First class, bidirectional | Needs SSE or WebSockets |
| Debuggability with curl | Painful, needs grpcurl plus the proto | Trivial |
| Learning curve | Steeper, codegen and tooling | Everyone knows it |
gRPC + protobuf
- Wins on
- Hot internal hops, strict contracts, streaming, polyglot services from one .proto.
- Costs you
- No browser without a proxy, no curl, logs show bytes, steeper tooling.
- Pick when
- You own both ends and the path is hot.
- Breaks when
- A browser or third party needs to touch it directly.
REST + JSON
- Wins on
- Universal reach, human-readable wire, trivial to curl and test.
- Costs you
- Bigger payloads, slower parse, loose contract unless you add OpenAPI.
- Pick when
- A browser or partner is the client.
- Breaks when
- You need tight bytes and streaming on a hot internal path.
The big win is the pairing: a strict contract plus a compact fast wire format.
- With 40 microservices calling each other, the contract kills a whole class of bugs where one team changes a field and another team's parser quietly breaks.
- Code generation often turns a breaking change into a compile error instead of a 3am page.
The big cost is friction with the outside world.
- Browsers cannot speak raw gRPC: they cannot open arbitrary HTTP/2 streams with trailers the way gRPC needs.
- You add a proxy layer called gRPC-Web, which does not even support all streaming modes (client streaming and bidirectional streaming fall off).
- You cannot
curlan endpoint and eyeball the response. Logs show bytes, not readable JSON. New engineers have to learn the tooling.
My rule of thumb, and I would say it in the room: gRPC for service-to-service inside your own network, REST or GraphQL for anything a browser or third party touches. Binary speed matters most on the hot internal paths where you control both ends. Human readability and universal reach matter most at the edge.
One trade-off people forget: load balancing.
- HTTP/2 holds a long-lived connection open and multiplexes requests over it.
- A naive L4 load balancer pins one client to one backend, so traffic never spreads.
- You need an L7 balancer that understands HTTP/2, or client-side load balancing. This bites teams constantly, and there is more below.
5Where it is used#
gRPC was open sourced by Google out of an internal RPC system they ran for years, so the heaviest users are backends with many internal services.
Who runs it:
- Service meshes like Istio and Linkerd treat it as a first-class citizen.
- Kubernetes components talk to each other with it.
- Databases like CockroachDB and etcd use it for node-to-node traffic.
Reach for gRPC when:
- You own both client and server: internal microservices, sidecars, control planes.
- The call path is hot. At a million calls a second, trimming 110 bytes per message is more than 100 MB/s of bandwidth plus a real slice of CPU.
- You need streaming: live metrics, log tailing, chat, multiplayer state, order book feeds.
- You want polyglot services to share one contract: a Go, a Python, and a Java service all generated from the same
.proto.
Do not reach for gRPC when:
- A browser is the primary client and you do not want a proxy.
- You are building a public API where third parties expect REST and want curl and Postman.
- Traffic is low and human debuggability is worth more than microseconds.
Reveal the answer
6Common pitfalls and gotchas#
These are where interviews (and production) actually go.
Reusing or reordering field tags.
- The tag numbers are the identity of a field on the wire; the name is just source code.
- Delete field 2, later reuse tag 2 for something else, and old clients read the new bytes as the old type: silent corruption, not an error.
- Rule: never reuse a tag number. Mark removed ones
reserved. This is the single most common protobuf mistake I see.
Thinking a schema change is automatically safe.
- Protobuf is good at backward compatibility, but only if you follow the rules.
- Add a new optional field: safe. Remove a field: usually safe if you reserve the tag.
- Change a field's type: often a trap.
int32toint64is fine,int32tostringis not. Know which changes are wire-compatible before you ship.
The load balancing surprise.
- Deploy three replicas behind a standard balancer, roll out, and watch one pod melt at 90 percent CPU while the other two idle.
- The long-lived HTTP/2 connection is the culprit: every multiplexed call rides the one connection pinned to one pod.
- Fix it with an L7 proxy like Envoy, or gRPC's built-in client-side load balancing with a resolver that knows all backends.
Forgetting deadlines.
- gRPC gives you deadlines, a time budget that propagates across the call chain.
- If A calls B calls C with a 2 second deadline and C is slow, the whole chain knows to give up.
- Skip them and requests pile up for 30 seconds behind a stalled dependency, cascading into thread-pool exhaustion upstream. Set them. Propagate them.
Message size limits.
- gRPC caps message size at 4MB by default.
- Someone stuffs a large payload through and gets a confusing
RESOURCE_EXHAUSTEDerror. - Either raise the limit deliberately or, better, use streaming to chunk it.
Treating protobuf as self-describing.
- JSON carries its field names, so a human can read it cold.
- Protobuf bytes are meaningless without the
.proto. - Log raw protobuf or store it without versioning the schema and future-you has a bad day decoding it.
Saying "protobuf handles backward compatibility for you" and leaving it there. It does not, not automatically. If you cannot name reserved tags, the int32-to-string trap, and why reusing a tag silently corrupts old clients, the interviewer reads it as never having evolved a real schema under load.
7How it shows up in interviews#
Interviewers rarely ask "explain gRPC" cold. It shows up when you are designing a system and say "the services talk over gRPC." Then they probe.
Every follow-up below is checking one thing: have you felt the trade-off, or memorized a bullet list? The fix is always the same. Attach a number to the win, name the cost, and tie it to the system in front of you.
"Why gRPC here and not REST?"
"How does the browser talk to this?"
"How do you load balance it?"
"How do you evolve the schema without breaking clients?"
"What happens when a downstream service is slow?"
The difference between answers, in one line:
- Weak: "gRPC because it is faster than REST and uses binary, it is more efficient." Names a benefit with no size, no cost, no system tie-in. The follow-up "faster by how much, and where does that matter?" catches you flat.
- Strong: the win with a number, the cost (browsers, curl, tooling), the edge case (gRPC-Web at the gateway), and the failure mode (L7 balancing, deadlines). That is the difference between "I read about gRPC" and "I ran gRPC."
If they push on serialization formats specifically, show range:
- Protobuf's edge is tag-based compact encoding plus tight code generation.
- Avro leans harder into schema evolution for data pipelines and carries its schema with the data.
- Thrift is the older cousin from the same RPC family. Knowing all three exist signals range.
Attach a number to the win, name the cost, and tie it to the system. That is the whole interview.
What you can now explain in the room
- Why protobuf stays small: tag numbers plus values on the wire, no field names, and why that is roughly 70 bytes versus 180 for JSON.
- What HTTP/2 buys gRPC: multiplexing on one connection plus four call types, including bidirectional streaming without WebSockets.
- Where to put gRPC (hot internal hops you own both ends of) and where not to (anything a browser or third party touches).
- Why long-lived HTTP/2 connections break naive L4 load balancing, and how L7 or client-side balancing fixes it.
- How to evolve a schema safely: reserved tags, never reuse a number, and which type changes are wire-compatible.
- Why deadlines matter, and how skipping them cascades into thread-pool exhaustion upstream.
Want to see this in a full design? Read the System Design Question walkthrough on designing a real-time order processing pipeline, where the gRPC-versus-REST boundary is one of the first decisions you make.
Get a prep plan built around your gRPC and Protocol Buffers interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.