Home › High-Level Design › gRPC and Protocol Buffers
G

gRPC and Protocol Buffers

When an interviewer asks how two internal services should talk, gRPC is one of the two answers they expect you to reach for. The other is REST over JSON. Knowing when each one wins, and saying why in numbers, is the whole point of this guide. I have shipped gRPC in a mesh doing millions of requests per second, and I have also ripped it out of a public API where it was the wrong tool. Both decisions taught me more than any spec.

High-Level DesignIntermediate11 min read

If you remember five things

The whole answer in one glance. Everything below defends these five calls.

  1. 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.
  2. HTTP/2 underneath. You get multiplexing, header compression, and four call types (unary plus three streaming modes) for free, no WebSockets bolted on.
  3. 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.
  4. 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.
  5. 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 .proto file: 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 .proto is 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);
}
The one line that explains everything

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 the User comes back the same way in reverse.
sequenceDiagram participant C as Client Stub participant H2 as HTTP2 Connection participant S as Server Handler C->>C: Serialize request to protobuf C->>H2: Send bytes on a stream H2->>S: Deliver bytes S->>S: Deserialize and run handler S->>H2: Serialize response H2->>C: Deliver response bytes C->>C: Deserialize to User object
One unary call. Notice the two serialize/deserialize hops at the ends do the real work; the HTTP/2 connection in the middle is shared with many other in-flight calls, which is why one connection can saturate a link.

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:

flowchart LR A[Mobile Client] --> B[API Gateway] B --> C[Order Service] C --> D[Payment Service] C --> E[Inventory Service] C --> F[(Orders DB)] D --> G[(Cache)]
The common real-world split. The mobile client reaches the gateway over REST or gRPC-Web, and only behind the gateway do services talk raw gRPC to each other. Saying this boundary out loud is a strong move in an interview.

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.
~11 MB/s
Saved on one internal hopAt 50,000 req/s, cutting each message from 180 to 70 bytes is 11 MB/s off that link, about 950 GB a day. Multiply by every hot hop in the mesh and it becomes real money.

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.
From the interviewer's chair

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
Inside your network
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.
At the edge
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 curl an 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.
Pause before you read on: you put raw gRPC at the edge and the web team says their browser app cannot call it. What is actually blocking them, and what is the fix?
Reveal the answer
Browsers cannot open the raw HTTP/2 streams with trailers that gRPC needs, so the browser physically cannot speak the protocol. The fix is a gRPC-Web proxy (Envoy or similar) that translates between what the browser can send and real gRPC, with the caveat that client streaming and bidirectional streaming do not survive the translation. Naming this unprompted is the difference between having read about gRPC and having shipped it to a browser.

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. int32 to int64 is fine, int32 to string is 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_EXHAUSTED error.
  • 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.
Red flag that fails you here

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.

What the probing is really testing

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.

Follow-ups the interviewer will ask (and your answers)
"Why gRPC here and not REST?"
We own both ends, the paths are hot, and the .proto contract catches breaking changes at build time instead of 3am. On the order-to-inventory hop at 50,000 req/s, protobuf cuts each message from roughly 180 to 70 bytes, about 11 MB/s off that one hop, and the faster decode lets us run fewer pods. REST buys none of that inside the mesh.
"How does the browser talk to this?"
It does not speak raw gRPC. Browsers cannot open the HTTP/2 streams with trailers gRPC needs, so the web and mobile clients hit a gateway over REST or gRPC-Web, and only behind the gateway do services use raw gRPC. gRPC-Web also drops client streaming and bidirectional streaming, so I keep those server-side only.
"How do you load balance it?"
Not with a naive L4 balancer. HTTP/2 holds one long-lived connection and multiplexes every call over it, so L4 pins all that traffic to one pod and melts it. I use an L7 proxy like Envoy, or gRPC's client-side load balancing with a resolver that knows all backends, so calls actually spread across replicas.
"How do you evolve the schema without breaking clients?"
Add fields as new optional tags, never reuse a tag number, and mark removed ones reserved. Type changes only when they are wire-compatible: int32 to int64 is fine, int32 to string is not. Because both sides generate from the .proto, a genuinely breaking change tends to fail at compile time rather than silently on the wire.
"What happens when a downstream service is slow?"
Deadlines. gRPC propagates a time budget across the chain, so if A calls B calls C with a 2 second deadline and C stalls, the whole chain cancels instead of piling up. Skip deadlines and a slow dependency exhausts thread pools upstream. I set and propagate them, and pair them with retries that respect the remaining budget.

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.

Thanks. Check your inbox, your gRPC and Protocol Buffers prep plan is on the way.