DEV Community

Mubashir Taj
Mubashir Taj

Posted on

gRPC vs REST: How to Choose for Internal APIs

Every few months a team decides to rebuild its internal APIs on gRPC. Ask why, and the answer is usually a benchmark graph from a blog post. Ask what problem it solves for them, and the room goes quiet.

gRPC is a good tool. REST is a good tool. They solve different problems, and the expensive mistake is picking one as an identity instead of picking per API, per consumer. Here is the honest comparison, based on the parts that actually bite in production.

The real difference: the contract

REST, as most teams practice it, is JSON over HTTP with conventions you hold in your head. The contract lives in documentation if you maintain an OpenAPI spec, and in shared memory if you do not. Nothing stops a handler from returning total as a string on Tuesdays.

gRPC starts from a schema file and generates the client and server code from it:

syntax = "proto3";

service Orders {
  rpc GetOrder (GetOrderRequest) returns (Order);
  rpc WatchOrders (WatchRequest) returns (stream Order);
}

message GetOrderRequest {
  int64 order_id = 1;
}

message Order {
  int64 id = 1;
  string status = 2;
  int64 total_cents = 3;
}
Enter fullscreen mode Exit fullscreen mode

That generated code is the feature people underestimate. Ten services in four languages all get typed clients from the same file, and a field type change fails at build time instead of at 2 AM. The discipline is forced, not voluntary. Field numbers make additive changes natural: you add field 4, old clients ignore it, nothing breaks. Retire a field and its number is reserved forever, so nobody reuses it by accident.

If your APIs suffer from drift between what producers send and consumers expect, this alone can pay for gRPC. If they do not, you are buying a solution to a problem you do not have yet.

Performance, honestly

Yes, protobuf payloads are smaller than JSON, and yes, HTTP/2 multiplexes many calls over one connection. Both are true, and both matter less than the sales pitch suggests.

Most internal APIs spend their time waiting: on a database query, on disk, on the next service down the chain. Serialization is rarely the line item that shows up in a flame graph next to a 40 millisecond query. Shrinking a 2 KB JSON payload to 700 bytes does not rescue a slow endpoint.

Where the performance gap becomes real is call volume and fan-out. A gateway making thousands of small calls per second to a fleet of services feels the per-call overhead of HTTP/1.1 connections and JSON parsing in aggregate. Streaming is the other genuine gap: gRPC gives you bidirectional streams as a first-class citizen, while REST makes you bolt on server-sent events or websockets and then maintain the seams.

The browser problem

This one is not a matter of degree. Browsers cannot speak gRPC directly.

The HTTP/2 features gRPC depends on are not exposed to browser JavaScript, so browser clients need grpc-web plus a translation proxy such as Envoy sitting in front of your services. That is real infrastructure to own: another hop, another config surface, another thing to debug when calls fail.

The rule that follows is simple. If browsers, mobile apps you do not fully control, or third parties consume the API, it should be REST:

curl https://api.internal/orders/42

{"id": 42, "status": "shipped", "total_cents": 12900}
Enter fullscreen mode Exit fullscreen mode

Anyone on any team can run that command, read the output, and check the result against the docs. Public and partner APIs live or die on that accessibility.

Operations: the costs nobody demos

Debugging. A failing REST call can be reproduced with curl in seconds and read with your eyes. A failing gRPC call is binary frames. You will learn grpcurl, you will add reflection to your servers, and your logs will need interceptors to show anything human-readable. All doable. None of it is free.

Load balancing. A gRPC client holds one long-lived HTTP/2 connection to one backend, and every call rides that connection. The simple load balancing that works fine for REST, where each request is a fresh decision, quietly stops working. Teams discover this when one instance melts while its neighbours idle. The fixes are client-side load balancing, a service mesh, or an L7 proxy that understands gRPC. Budget for one of them from day one.

Errors and observability. REST rides on HTTP status codes that every tool, proxy, and dashboard already understands. gRPC has its own status model, which gets translated and flattened at every boundary. Traces, metrics, and error budgets all need a little more care to stay readable.

Caching. REST gets HTTP caching, ETags and CDN behaviour, almost for free. gRPC gets none of it. For read-heavy internal data that many services request repeatedly, that difference shows up as real load on your origin services.

The middle path nobody prices in

Before paying the gRPC migration bill, price the cheaper version of its main benefit. Most of what teams actually want from gRPC is the contract discipline, and a large share of that is available on REST today. An OpenAPI spec, kept in the repo and checked in CI, can generate typed clients and server stubs in the same languages protobuf covers. Add contract tests that fail the build when a response shape changes, and drift stops being a runtime surprise.

What that does not give you is the binary encoding, the multiplexed transport, or native streaming. Those are transport properties, and only the protocol switch buys them. So the comparison becomes sharper: if your pain is contract drift, fix the contract where you stand. If your pain is call volume, fan-out overhead, or streaming, that is a transport problem, and gRPC is a reasonable answer to it. Teams that confuse the two end up migrating protocols to solve a documentation problem, and they carry the operational costs listed above for a benefit they could have had by Friday.

How to actually choose

Pick gRPC when the consumers are services you control, the contract changes often or spans several languages, call volume between services is high, or you genuinely need streaming. Service-to-service traffic inside one platform is its home turf.

Pick REST when browsers or external consumers are involved, the API is public, you want HTTP caching, call volume is modest, or the team values being able to debug with curl and browser devtools. Most CRUD-shaped internal APIs live happily here for years.

Pick based on consumers, not benchmarks. Write down who calls this API, from where, how often, and what breaks for them when it changes. The answer usually selects the protocol by itself. A benchmark graph never mentions your on-call experience, your load balancer, or your browser clients, and those are the things that generate incidents.

Expect to run both. A common end state is REST at the edge for browsers and partners, gRPC inside the platform for service-to-service calls. That is not indecision. That is each protocol doing the job it is shaped for.

The takeaway

The protocol is almost never the bottleneck. The contract discipline, the tooling, and the consumer list are what you will actually live with. gRPC earns its complexity when many services, many languages, or streaming are in play. REST earns its keep everywhere a human, a browser, or a cache is in the loop.

If you take one thing from this: before the next protocol debate, list the consumers of the API on a whiteboard. The debate usually ends right there.

Top comments (2)

Collapse
 
lin7c profile image
xcl •

One cost that belongs next to the load-balancing point is schema evolution discipline in reverse: protobuf makes adding fields easy, but removing or renumbering fields needs reserved ranges and a review process, otherwise the generated-code advantage can turn into a silent compatibility break across languages. For REST with OpenAPI, does the comparison change if contract tests run in CI against the actual JSON payloads, rather than only validating the spec file? That usually catches the 'total as a string on Tuesdays' drift without paying the gRPC-Web and client-side load-balancing costs at the edge.

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

This matches what I've seen — every 'should we switch to gRPC' debate I've sat through was really about contract discipline. Teams copy-paste REST clients, the drift lives in shared memory, and six months later there's a 200-with-empty-body nobody owns. The whiteboard-the-consumers rule is the sharpest line here. The one case where I'd push back: genuine streaming. I needed server-push for a status pipeline once and SSE felt like a workaround wearing a costume. But for everything else, OpenAPI-in-CI bought us most of gRPC's discipline. Do you gate breaking changes in CI, or is the OpenAPI check advisory?