# gRPC, Explained

> What gRPC actually is - a contract-first way for internal services to call each other fast - how the .proto file, code generation, and HTTP/2 streaming fit together, and the real trade-offs versus REST and GraphQL.


---

# gRPC, Explained

You've shipped REST APIs. You know how to design a JSON endpoint, read a `404`, and `curl` something to
see what comes back. Then someone on your team says "we're putting this service behind gRPC" and suddenly
there's a `.proto` file, a code-generation step in the build, and a binary payload you can't read in your
browser. It feels like a different universe with its own rules nobody wrote down for you.

It isn't magic, and it isn't a replacement for everything you know. gRPC is a focused tool that solves one
problem really well: letting *internal services call each other* quickly, with a strict typed contract both
sides agree on in advance. This guide installs that mental model first, shows how the pieces actually
work, and ends with a clear-eyed look at when gRPC is the right call and when it absolutely isn't.

## How to read this
- **Just need the "REST vs GraphQL vs gRPC, when do I use which" answer?** Jump to the table at the top of
  [Phase 3: The Real Trade-offs](03-the-real-trade-offs.md).
- **Want it to finally make sense?** Read in order - each phase builds on the last. The mental model in
  Phase 1 is what makes the machinery in Phase 2 feel obvious.

## The phases
1. **[The Problem gRPC Solves](01-the-problem-grpc-solves.md)** - why services that call each other
   thousands of times need speed *and* a strict typed contract, and how gRPC delivers both.
2. **[How gRPC Works](02-how-grpc-works.md)** - the `.proto` file, code generation, binary serialization,
   and the four call types (unary plus three streaming modes), at a gentle level.
3. **[The Real Trade-offs](03-the-real-trade-offs.md)** - what you give up (human-readable payloads,
   easy browser support), what you gain, and a fair REST-vs-GraphQL-vs-gRPC "when to use which" table.

> Deep material - custom interceptors, deadlines and retries, load balancing, mutual TLS, and the wire
> format byte-by-byte - is deliberately left out so this stays a guide and not a reference manual. Once the
> model here clicks, the official docs at grpc.io read much more easily.

## Related guides
- [REST APIs, Explained](/guides/rest-apis-explained) - the model gRPC is most often compared to.
- [GraphQL, Explained](/guides/graphql-explained) - the other "beyond REST" option, with different goals.


---

# The Problem gRPC Solves

Picture the inside of a modern backend. It's rarely one program anymore - it's a dozen small services. The
checkout service calls the inventory service to confirm stock. Inventory calls the pricing service. Pricing
calls the currency service. A single user clicking "buy" can fan out into hundreds of calls *between your
own services*, all in milliseconds, all on your own network.

That inside-the-datacenter traffic has different pressures than the API your mobile app talks to, and those
pressures are exactly what gRPC was built for. Before any `.proto` syntax, name the two problems plainly - 
once you feel them, gRPC stops looking weird and starts looking like an obvious answer.

## The first problem: REST is comfortable, but it isn't cheap

For a public API, REST over JSON is a great default. It's readable, debuggable in a browser, and everybody
knows it. But look at what happens on *every single call* when one of your services talks to another using
JSON over HTTP/1.1:

```mermaid
sequenceDiagram
  participant Pricing as pricing service
  participant Currency as currency service
  Pricing->>Currency: open a fresh TCP connection
  Pricing->>Currency: send { "amount": 1299, "from": "USD", "to": "EUR" } (text, parsed)
  Note over Currency: read JSON as text, turn "1299" back into a number
  Currency-->>Pricing: { "amount": 1187, "currency": "EUR" }
  Note over Pricing,Currency: connection closed - next call starts over
```

The number `1299` was turned into the text `"1299"`, shipped as characters, then parsed back into a number
on the other side. The field names `"amount"`, `"from"`, `"to"` rode along as text on every request, and on
HTTP/1.1, each call often pays to set up and tear down a connection. None of this hurts much once - 
multiply it by hundreds of thousands of internal calls per second and the waste (CPU serializing text,
bytes on repeated field names, connection overhead) becomes real money and real latency.

📝 **Terminology - serialize / deserialize.** *Serializing* is turning an in-memory object (a struct, a
class instance) into a stream of bytes you can send over the network. *Deserializing* is rebuilding the
object from those bytes on the other end. JSON serialization produces human-readable text; that readability
is precisely what costs you size and parsing time.

## The second problem: JSON has no enforced contract

Here's the one that bites teams hardest, and it has nothing to do with speed.

When the pricing service calls the currency service over JSON, *nothing checks that the two agree on the
shape of the data.* The currency team renames `amount` to `amountInCents`. Their tests pass. They deploy.
Now every call from pricing silently reads `amount` as missing - `undefined`, `null`, or zero depending on
the language - and you find out from a customer, not a compiler.

```text
   what pricing SENDS          what currency now EXPECTS
   ┌──────────────────┐        ┌──────────────────────┐
   │ amount:  1299     │  ✗     │ amountInCents: ?      │   ← nobody warned anyone
   │ from:   "USD"     │        │ from:          "USD"  │
   │ to:     "EUR"     │        │ to:            "EUR"  │
   └──────────────────┘        └──────────────────────┘
```

Two services disagree about the data's shape and neither one knew at build time. With plain JSON, the
"contract" between services lives in a wiki page, a Slack thread, or someone's memory - not in anything a
machine enforces. ⚠️ **This is the quiet killer of microservice systems:** integration bugs that only
surface in production because nothing forced both sides to agree first.

## The mental model: define the functions once, both sides agree

gRPC's whole idea is to attack both problems with one move. You write down the *contract* - the available
functions and the exact shape of their inputs and outputs - in a single file that both services share.

That's the line to hold onto:

💡 **Key point.** gRPC is **contract-first**. You define the service's functions and message shapes once, in
a `.proto` file, and *both* the caller and the callee generate their code from that same file. The contract
isn't a wiki page - it's the source of truth that both sides are physically built from.

Once that contract exists, two good things fall out of it for free:

- **Speed.** Because both sides already know the exact shape of every message, the data can travel as a
  compact **binary** format (Protocol Buffers) instead of text. No field names on the wire, no parsing text
  into numbers - the layout is known in advance. And gRPC runs over **HTTP/2**, which reuses one connection
  for many calls instead of opening a new one each time.
- **Safety.** Calling a remote service feels like calling a normal function in your own code:
  `currency.Convert(request)`. If you pass the wrong type, your compiler complains *before* you ship,
  because the function signature was generated from the shared contract.

📝 **Terminology - RPC.** The "RPC" in gRPC stands for **Remote Procedure Call**: the idea of calling a
function that happens to run on another machine, as if it were local. REST makes you think in *resources and
verbs* (`GET /users/42`). RPC makes you think in *functions* (`GetUser(id: 42)`). gRPC is Google's modern
take on that old idea - the "g" is just Google's prefix, not "good" or "great."

Here's the same currency call, but as gRPC frames it. Don't worry about the syntax yet - Phase 2 walks
through it. Notice only the *feeling*:

```mermaid
sequenceDiagram
  participant Pricing as pricing service
  participant Currency as currency service
  Note over Pricing,Currency: one shared, persistent HTTP/2 connection
  Pricing->>Currency: convert(amount=1299, from=USD, to=EUR) - a function call, sent as compact binary
  Currency-->>Pricing: Money(amount=1187, currency=EUR)
```

From the developer's chair, pricing called a function and got a typed result back - no hand-written JSON,
no guessing field names. Underneath, gRPC serialized the call to binary, sent it over a connection that
stays open for the next call too, and the strong types on both ends came from the one contract file they
share.

## Why this saves you later

The next time a teammate renames a field on a service you depend on, you won't learn about it from an angry
customer - you'll learn about it when you pull the updated contract and your build breaks, which is exactly
when you *want* to learn about it. And when someone asks why the internal call graph is suddenly half the
latency it was on JSON, you'll know: smaller payloads, no repeated text parsing, one reused connection.

That's the trade gRPC offers. Phase 3 covers what it asks for in return - first, the machinery that turns
the contract into running code.

## Recap

1. **Internal service-to-service traffic** is high-volume and latency-sensitive in a way public APIs often
   aren't - the waste in text serialization and per-call connection setup adds up fast.
2. **Plain JSON has no enforced contract** - two services can silently disagree on data shape and you only
   find out in production.
3. **gRPC is contract-first:** define functions and message shapes once in a `.proto` file; both caller and
   callee are built from it.
4. **Speed** comes from compact binary Protocol Buffers over a reused **HTTP/2** connection; **safety** comes
   from calling remote services like typed local functions.
5. **RPC** means "call a function that runs elsewhere" - you think in functions, not resources and verbs.


---

# How gRPC Works

Phase 1 gave you the mental model: define the functions once, both sides agree. There are really only four
moving parts to how that becomes real, and once you've seen each one, the whole thing stops feeling like a
black box:

1. The **`.proto` file** - the shared contract.
2. **Code generation** - turning that contract into real code in your language.
3. **Binary serialization** - how messages actually travel.
4. The **four call types** - including the streaming modes that REST can't easily do.

## 1. The `.proto` file - the contract you write

This is the file both services share. It declares two things: the **messages** (the shapes of your data)
and the **service** (the functions you can call). Here's a small but complete one, annotated:

```protobuf
syntax = "proto3";              // which version of the proto language

package currency;              // a namespace, so names don't collide

// A "message" is the shape of a piece of data - like a struct.
message ConvertRequest {
  int64  amount = 1;           // an amount in minor units (e.g. cents)
  string from   = 2;           // ISO currency code, e.g. "USD"
  string to     = 3;           // ISO currency code, e.g. "EUR"
}

message Money {
  int64  amount   = 1;
  string currency = 2;
}

// A "service" is a set of callable functions (RPCs).
service Converter {
  rpc Convert(ConvertRequest) returns (Money);
}
```

You declared a data shape (`ConvertRequest`), a result shape (`Money`), and one function (`Convert`) that
takes the first and returns the second. That's the entire contract - any service with this file knows
exactly how to call `Convert` and what it gets back.

📝 **Terminology - those `= 1`, `= 2`, `= 3` numbers are field tags, not default values.** Each field gets a
small, permanent number. On the wire, Protocol Buffers identifies a field by *that number*, not by its name.
This is the secret behind two important properties:

- **Field names never travel.** The binary message says "field 1 is `1299`," not "amount is 1299." That's a
  big part of why the payload is so much smaller than JSON.
- **You rename fields freely.** Because identity is the number, renaming `amount` to `amountInCents` in the
  `.proto` doesn't break the wire format at all - old and new code still agree on field `1`.

⚠️ **Gotcha - never reuse or renumber a field tag.** The numbers are the contract. If you delete field `2`
and later add a different field as `2`, old clients will read the new data into the old slot and get
garbage. The rule is: add new fields with new numbers, and never recycle an old one. (proto3 even lets you
mark a number `reserved` so nobody reuses it by accident.)

## 2. Code generation - the contract becomes real code

You don't hand-write the networking code. You run the `.proto` file through the Protocol Buffer compiler,
`protoc` (usually via your build system or a gRPC plugin), and it generates source code in your language:

```console
$ protoc --go_out=. --go-grpc_out=. currency.proto
$ ls
currency.proto  currency.pb.go  currency_grpc.pb.go
```

From one `currency.proto`, the compiler wrote two files of Go code. `currency.pb.go` holds the message
types (`ConvertRequest`, `Money`) as real Go structs; `currency_grpc.pb.go` holds the **stubs** - the client
and server scaffolding for the `Convert` function. The exact filenames and flags differ per language
(`--python_out`, `--java_out`, and so on), but the idea is identical everywhere.

📝 **Terminology - a "stub" is generated glue code.** On the **client** side, the stub is an object with a
`Convert(...)` method that *looks* like a normal local function but actually packages your request into
binary, sends it over HTTP/2, waits for the reply, and hands you back a typed `Money`. On the **server**
side, the generated code is the opposite half: it receives the bytes, rebuilds the request object, and calls
the `Convert` function *you* wrote to do the real work.

This is the payoff of contract-first: because both stubs come from the same `.proto`, and you can generate
them in *different languages from the same file*, a Go service and a Python service can call each other
with full type safety on both ends - no hand-written serialization, no drift.

The client side in practice (Go, trimmed to the essentials):

```go
// client is the generated stub, already connected over HTTP/2.
resp, err := client.Convert(ctx, &currency.ConvertRequest{
    Amount: 1299,
    From:   "USD",
    To:     "EUR",
})
if err != nil {
    log.Fatalf("convert failed: %v", err)
}
fmt.Println(resp.Amount, resp.Currency) // 1187 EUR
```

You called `client.Convert(...)` as if `Convert` lived in your own program. Under the hood, the stub
serialized your request to binary, sent it over the open HTTP/2 connection to the currency service, and
deserialized the reply into a `Money` you can read with `resp.Amount` - the remote call hidden behind an
ordinary-looking function call, the "Remote Procedure Call" idea made real.

## 3. Binary serialization - what actually goes over the wire

When the stub serializes that request, it does *not* produce text. There's no `{ "amount": 1299 }`. It
produces a compact binary stream that, conceptually, looks like this:

```text
   JSON (text, ~46 bytes)        Protobuf (binary, far fewer bytes)
   ─────────────────────         ──────────────────────────────────
   {"amount":1299,               [field 1, varint] 1299
    "from":"USD",                [field 2, string] USD
    "to":"EUR"}                  [field 3, string] EUR
        ▲                                ▲
   field names + quotes +          no field names - just the tag
   braces all shipped as text      number, type hint, and value
```

Protocol Buffers writes each field as its *number* (the `= 1` tag), a hint of its type, and the raw value - 
nothing else. The field names and JSON punctuation aren't there at all, so the result is meaningfully
smaller and faster to read: the receiver already knows from the contract what field `1` means and doesn't
parse any text. (Exact byte counts depend on the values; the point is "fewer bytes, no text parsing," not a
fixed ratio.)

The real cost of this - which we'll face squarely in Phase 3 - is that you can't read that binary stream
with your eyes the way you can read JSON.

## 4. The four call types

REST gives you one basic shape: send a request, get a response. gRPC keeps that shape but adds three
**streaming** modes, because it runs over HTTP/2, which can keep a channel open and send many messages over
it in either direction.

📝 **Terminology - a "stream" here means a sequence of messages over one call.** Instead of one request and
one response, one side (or both) can send a series of messages over the same open call, over time.

```text
  1. Unary          client ── req ──▶ server          one request,
                    client ◀── res ── server          one response.  (like REST)

  2. Server stream  client ── req ──▶ server          one request,
                    client ◀── res ── server          many responses
                    client ◀── res ── server          trickling back over time.
                    client ◀── res ── server

  3. Client stream  client ── req ──▶ server          many requests
                    client ── req ──▶ server          sent up, then ...
                    client ── req ──▶ server
                    client ◀── res ── server          one response at the end.

  4. Bidirectional  client ── req ──▶ server          both sides send
                    client ◀── res ── server          freely, independently,
                    client ── req ──▶ server          over one open call.
                    client ◀── res ── server
```

What each is *for*, in plain terms:

- **Unary** - the everyday one. Convert a currency, fetch a user, place an order. If you're new to gRPC,
  almost everything you write will be unary, and it behaves just like the request/response you already know.
- **Server streaming** - the server has a lot to send back and you'd rather get it as it's ready than wait
  for all of it. Think "stream me search results as they're found" or "send me live price updates."
- **Client streaming** - you have a lot to send up and want one summary back. Think "here are 10,000 metrics
  rows; reply with the count you stored."
- **Bidirectional streaming** - both sides talk at once over one connection. Think live chat, or a
  back-and-forth where the server reacts to what the client sends while still receiving more.

You declare which kind you want right in the `.proto`, with the `stream` keyword:

```protobuf
service PriceFeed {
  rpc GetQuote(Symbol) returns (Quote);              // unary
  rpc WatchPrice(Symbol) returns (stream Quote);     // server streaming
}
```

`WatchPrice` returns `stream Quote` instead of a single `Quote`, so the generated client stub gives you
something you read from in a loop as new quotes arrive, rather than a single value. The contract itself
records that this call streams, so both sides generate the right shape of code for it.

💡 **Key point.** The streaming modes aren't an exotic add-on; they fall naturally out of building on
HTTP/2. But you don't have to use them. Reach for streaming when the data genuinely arrives over time;
otherwise plain unary is the right, boring default.

## Recap

1. The **`.proto` file** declares **messages** (data shapes) and a **service** (callable functions) - the
   shared contract.
2. **Field tag numbers** (`= 1`, `= 2`) are the real identity of each field on the wire - never reuse them;
   renaming a field is safe because names don't travel.
3. **`protoc` generates code** - message types plus client/server **stubs** - in many languages from the one
   file, so different-language services interoperate safely.
4. The client stub makes a remote call **look like a local function**; messages travel as compact **binary**,
   not text.
5. There are **four call types**: unary (the default), server streaming, client streaming, and bidirectional
 - declared with the `stream` keyword in the contract.

You now know what gRPC is and how it works. The last and most important question is the real one: when is
all this worth it, and when is it the wrong tool?


---

# The Real Trade-offs

Everything in Phases 1 and 2 was the case *for* gRPC. This phase is the part a battle-hardened friend owes
you: what it costs, and where it's the wrong tool. gRPC is genuinely excellent for one job and genuinely
awkward for others, and knowing the difference saves you from adopting it somewhere it'll fight you every
day. If you only read one thing, read the table below - then come back for the *why* underneath it.

## When to use which - REST vs GraphQL vs gRPC

> No tool here wins outright. Each made different choices for different problems. This table covers both the
> strengths and the costs of each, so you can match the tool to the situation.

| | REST | GraphQL | gRPC |
|---|---|---|---|
| **Core idea** | Resources + HTTP verbs (`GET /users/42`) | One query language; client asks for exactly the fields it wants | Typed function calls (RPC) from a shared contract |
| **Payload format** | JSON (text, human-readable) | JSON (text, human-readable) | Protocol Buffers (compact **binary**, not readable by eye) |
| **Typed contract** | Optional (OpenAPI, if you maintain it) | Yes - the schema | Yes - the `.proto`, enforced by code generation |
| **Browser support** | Native - any browser, `fetch`, `curl` | Native - it's HTTP + JSON | **Not directly** - needs grpc-web + a proxy |
| **Streaming** | Awkward (workarounds like SSE/long-poll) | Subscriptions (over WebSocket) | First-class - server, client, and bidirectional |
| **Debuggability** | Easy - read it in a browser or `curl` | Easy - readable queries and responses | Harder - binary needs special tooling to inspect |
| **Best fit** | Public APIs, simple CRUD, broad reach | Client-driven UIs that need flexible, exact data | Internal service-to-service, high call volume, low latency |
| **Weakest at** | Chatty multi-resource fetches; no enforced types | Server complexity; caching is harder than REST | Anything public or browser-facing |

💡 **Key point - the one-line rule of thumb.** Public or browser-facing? Reach for REST (or GraphQL if
clients need flexible queries). Internal services calling each other at high volume where you want speed and
a strict contract? That's gRPC's home turf. Many real systems use **both**: gRPC behind the scenes between
services, and a REST or GraphQL gateway at the public edge.

Now the costs in detail, because each row above hides a real day-at-work consequence.

## Cost #1: Binary isn't human-readable

This is the trade you make the moment you choose Protocol Buffers. With REST, when something goes wrong you just look:

```console
$ curl https://api.internal/convert -d '{"amount":1299,"from":"USD","to":"EUR"}'
{"amount":1187,"currency":"EUR"}
```

You saw the exact request and response in plain text, with your own eyes, in two seconds. That readability
is REST's superpower for debugging.

A gRPC call on the wire is binary. You can't `curl` it into something readable, and a packet capture shows
bytes, not fields. ⚠️ **Gotcha - don't try to debug gRPC with the JSON tools you know; they'll show you
noise.** gRPC has its own tooling that *does* make it readable - you point a tool at the service and call
methods by name, and it uses the contract to render requests and responses as text:

```console
$ grpcurl -d '{"amount":1299,"from":"USD","to":"EUR"}' \
    localhost:50051 currency.Converter/Convert
{
  "amount": "1187",
  "currency": "EUR"
}
```

`grpcurl` (the gRPC cousin of `curl`) used the service's contract to turn your text into the right binary
call, sent it, and turned the binary reply back into readable JSON for you. The point isn't that gRPC is
undebuggable - it's that the easy, universal `curl`-it reflex doesn't work, and you have to learn a new
tool to get the same comfort.

## Cost #2: Browsers can't speak gRPC directly

A browser cannot make a raw gRPC call: gRPC needs fine-grained control over HTTP/2 frames that browser
JavaScript isn't given access to, so a web page can't call your gRPC service the way it calls a REST
endpoint.

The workaround is **grpc-web**: a variant of the protocol that browsers *can* speak, paired with a small
**proxy** that translates between grpc-web and real gRPC.

```mermaid
flowchart LR
  Browser["browser (JS app)"] <-->|grpc-web| Proxy["proxy, e.g. Envoy (translates both ways)"]
  Proxy <-->|gRPC| Service["gRPC service (Converter)"]
```

The browser talks grpc-web to a proxy; the proxy talks native gRPC to your service and relays the answer
back. It works, and plenty of teams run it - but you now have an extra moving piece to deploy and operate,
and grpc-web doesn't support every streaming mode (client and bidirectional streaming are limited).
📝 **Terminology - a "proxy" here is a server that sits in the middle, receiving requests in one protocol
and forwarding them on in another.**

This is the single biggest reason gRPC is a poor fit for *public, browser-facing* APIs: you're signing up
for infrastructure and limitations to do something REST does for free.

## Cost #3: The tooling and learning curve are real

With REST, a new teammate can be productive in an afternoon - it's HTTP and JSON, tools they already know.
gRPC asks more up front:

- You add a **code-generation step** to your build, and everyone needs `protoc` and the right plugins set up.
- You and your team have to learn the `.proto` language, the field-tag rules, and the four call types.
- Your debugging, testing, and observability tools may need gRPC-aware versions or plugins.

None of this is hard once learned, and it's wholly worth it for the right use case. But it's a genuine cost,
and pretending otherwise would be the kind of hand-waving this guide exists to avoid. For a small service
with a handful of public JSON endpoints, that cost buys you very little. For a fleet of internal services
exchanging huge call volumes, it pays for itself quickly.

## Where gRPC genuinely shines

To end on the fair note: when the use case matches, gRPC is hard to beat.

- **Internal microservice-to-microservice traffic** - the exact scenario from Phase 1. High volume, latency
  matters, both ends are yours, and you want a contract a machine enforces.
- **Polyglot backends** - a Go service and a Python service and a Java service all generating type-safe
  stubs from one `.proto` is a real, daily win.
- **Streaming workloads** - live feeds, telemetry pipelines, long-lived bidirectional channels. gRPC's
  streaming is first-class where REST has only workarounds.

And where it doesn't:

- **Public APIs** you want third parties to consume with `curl` and zero special tooling.
- **Browser-facing endpoints**, unless you're ready to run grpc-web and a proxy.
- **Simple CRUD** where REST's readability and ubiquity already give you everything you need.

## Recap

1. **Binary payloads aren't human-readable** - you trade `curl`-it-and-read for tools like `grpcurl` that
   use the contract to render calls as text.
2. **Browsers can't speak gRPC directly** - you need **grpc-web** plus a translating **proxy**, with some
   streaming limits. That's why gRPC is a poor fit for public, browser-facing APIs.
3. **The learning and tooling curve is real** - code generation, `.proto` rules, gRPC-aware tools - and only
   pays off at the right scale.
4. **gRPC shines** for internal, high-volume, polyglot, and streaming service-to-service traffic; **REST and
   GraphQL win** at the public, browser-facing, readable-by-default edge.
5. Many systems use **both**: gRPC between services, REST/GraphQL at the public boundary.

You now have the whole picture - what gRPC is, how it works, and the real cost of using it. When someone
puts a `.proto` file in front of you, you'll know exactly what you're looking at and whether it belongs
there.

**Related guides:** [REST APIs, Explained](/guides/rest-apis-explained) · [GraphQL, Explained](/guides/graphql-explained)
