gRPC vs REST
Pick REST when the caller is a browser or a team you do not control, and gRPC when both ends are yours and the call happens often enough for the encoding to matter. One trades speed for reach; the other trades reach for speed.
What is the difference between writing your order out in plain words anybody behind the counter can read, and handing over a numbered slip both sides agreed on beforehand? The slip is quicker to pass and useless to anyone without the key. Two services choose between them the same way.
Most of what gets written about this choice compares numbers a reader has no way to check: gRPC is faster, REST is simpler, pick one. That framing skips the one property of your situation that actually decides it, and it has nothing to do with either technology’s top speed. It is who is going to call the service you have not built yet — a browser, a partner integrating from outside, or a team on the other side of the same deploy pipeline. Answer that first and the rest of the comparison mostly answers itself.
What the contract changes#
Both sides agree on a schema before the first request is sent — a .proto file, compiled into Protocol Buffers. Because the schema is fixed ahead of time, a message on the wire does not need to carry its field names; each field travels as a number the schema already assigns it, and both ends decode the message against the same compiled definition rather than by reading a label off the payload. A REST endpoint carrying JSON makes the opposite trade: the payload is self-describing, so anyone with the URL can read the shape of a response without consulting anything else first.
The schema also produces the client. Instead of a hand-written function that builds a URL, sets headers and parses a body, a team calling a gRPC service compiles the same schema into a client for its own language and calls a method that reads like any other function call. That generated client is where a breaking change gets caught: rename a field or drop a method from the schema, regenerate the client, and the code that referenced the old shape stops compiling — before it ever reaches a caller. A renamed JSON field breaks a REST integration in principle the same way, but nothing forces that discovery before deploy; the first sign is usually a caller’s own parser failing to find a key that used to be there.
# generated from the .proto — field names never travel on the wire
reply = orders_client.GetOrder(GetOrderRequest(order_id="ord-abcde"))
# the REST equivalent — you write the request and trust the shape back
resp = requests.get(f"{base_url}/orders/ord-abcde")
order = resp.json() # nothing here was checked until this line ran Same order, two ways of asking for it. The gRPC line calls a method; the REST line still has to build a URL and then trust that whatever comes back has the shape the code expects.
None of this is free. The schema is now a dependency the two teams share at build time as well as at runtime — a service cannot ship a breaking change without its callers regenerating against the new definition, which turns an independent deploy into a coordinated one. gRPC also lets a service configure a retry policy that its generated clients apply automatically: a transient failure can be retried by the client library itself, inside code the calling team never wrote and may never look at. Whether that automatic retry is safe is a question for the server, not the client, and it is the same question idempotency keys and safe retries answers for a retried request generally, whichever protocol produced it.
Who can actually call you#
A browser cannot call a gRPC service directly. gRPC’s messages travel inside HTTP/2 frames and depend on one part of HTTP/2 a browser’s own request APIs do not expose: the trailers that carry a call’s final status after the body has already streamed. Fetch and XMLHttpRequest give a script no way to read them, so a browser talking to a gRPC service needs something in front of it — a proxy, or middleware doing the same job in the same process — translating the call into something the browser can actually send and read. That layer is one more thing to deploy and keep working, not a rounding error.
A partner integrating against an API for the first time is optimising for something different: trying a call before committing engineering time to it. An HTTP endpoint returning JSON can be opened in a terminal, watched in a browser’s network tab, or pasted into a request tool with nothing else installed. A gRPC service asks for the schema and a generated client before the first call can even be shaped — a reasonable ask of a team you already coordinate releases with, and a much larger one of a team still deciding whether to integrate with you at all.
Streaming is where the two stop being close cousins. gRPC defines four kinds of call inside the same model: a single request and response, a stream in either direction alone, and a stream running in both directions over one open call at once. A service that needs to push a sequence of updates to a caller, or exchange messages on both sides of a long-lived call, is using the protocol as it was built rather than bolting something like a WebSocket onto a request-response protocol that was never meant to hold a connection open that way. REST has nothing built in that does the same job; getting the same shape means reaching for a separate mechanism next to it, not inside it.
Choosing the encoding before the caller#
The recurring mistake is picking the encoding first and the situation second. A team compares gRPC’s binary format against JSON, decides the difference is worth having, and adopts gRPC for a service a browser calls a handful of times per page load. The encoding saving is real and it is paid on every call — but a call that happens rarely returns a saving nobody using the product will ever perceive, while the choice still costs everything the previous section named: a proxy layer for the browser traffic, a build-time schema dependency between teams, and a payload nobody can read directly during an incident without the same schema the client was built from.
The tell shows up at the first serious incident: the team spends longer working out what a failing call actually contained — decoding a binary payload against a schema, matching a client version to a service version — than the encoding has saved them in total up to that point. That is not a flaw in gRPC; it is a cost the team accepted for a caller relationship that was never going to pay it back.
Nothing here is an argument against gRPC: the same choice is clearly right one layer down, between two internal services that call each other constantly and already share a release process. The mistake was never the technology — it was reasoning that holds between two internal services and applying it to a caller that is a browser or an outside partner instead. Retries are only one part of what crossing that boundary costs; ordering, partial failure and timeouts are a larger topic than either protocol’s encoding, and the distributed-systems path is the fuller treatment of that ground, on its own terms.
| Situation | Take | Because |
|---|---|---|
| A browser calls it directly | REST | No proxy layer needed to be reachable. |
| Both ends are yours, called constantly | gRPC | The encoding saving is paid every call. |
| A partner integrates against it | REST | They can try it before they trust it. |
| Streaming in both directions | gRPC | It is in the model rather than bolted on. |
IF YOU REMEMBER ONE THING
The encoding is the smallest part of this decision. Who is going to call the service — a browser, an outside partner, or a team on the other side of your own deploy pipeline — is what settles it, and picking the encoding first is how a team ends up paying for a schema dependency and a proxy layer to buy a saving nobody downstream can perceive.
Questions people also ask
5 QUESTIONSCan a browser call gRPC directly?
No. A browser's fetch and XMLHttpRequest APIs do not expose HTTP/2 trailers, which is where gRPC carries a call's final status after the response body, so reaching a gRPC service from a browser needs a translation layer in front of it — a proxy such as Envoy, or middleware doing the same job inside the same process. That arrangement has a name, gRPC-Web, but it is not something a browser can do on its own.
Is gRPC always faster?
Not in a way most calls will ever show you. Its binary encoding is smaller and cheaper to parse than JSON, but that saving is paid per call — for a service invoked rarely it comes to nothing anyone downstream would notice, while the schema dependency and the browser proxy it usually needs are costs paid whether or not the saving ever adds up to anything.
What happens when the schema changes?
If the change is compatible — a new field added, say — code built against the old schema and code built against the new one keep working side by side, because that is what the format is designed to allow. If the change breaks the contract, the client generated from the old schema simply stops compiling the moment someone regenerates it against the new one, which is usually well before the change would have reached a caller.
Can you use both in one system?
Yes, and a lot of systems do exactly that: gRPC between internal services that call each other constantly and already share a release process, REST at the edge where a browser or an outside partner connects. Choosing one for internal traffic does not commit you to the same choice at the boundary a caller you do not control has to cross.
What about GraphQL?
It answers a different question. GraphQL lets a caller ask a single endpoint for exactly the fields it needs, which solves over-fetching from a browser — it does not change the wire encoding this comparison is about, and a GraphQL server still typically answers in JSON over ordinary HTTP, with the same reach a plain REST endpoint has.