Dev.to WebDev 🛠 Dev 👁 0 📖 6 min read

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 t

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;
}

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}

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.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.