DEV Community

Cover image for Why Netflix, Google and Uber Don't Use REST Between Their Own Services
MANGESH MANDLIK
MANGESH MANDLIK

Posted on

Why Netflix, Google and Uber Don't Use REST Between Their Own Services

Picture an order service that has to check inventory, calculate pricing, look up the user's account, and queue a notification, all before it can answer one request from your app. That's not one HTTP call. That's five, fired off to five other services, each of which might call two or three more.

At ten requests a second, nobody notices. At ten thousand, you start noticing everything: the JSON parsing on both ends, the field names repeated in every single payload, the TCP handshake and TLS negotiation for every new connection, the runtime type errors that only show up in production because nothing checked the schema at compile time.

This is the exact problem Google ran into internally, at a scale most of us will never touch. Their answer was gRPC: a remote-procedure-call framework built on Protocol Buffers and HTTP/2, designed to make service-to-service calls smaller, faster, and strongly typed. It's now the default choice for internal communication inside a lot of large distributed systems, not because REST is bad, but because REST's costs, tiny and invisible at low volume, compound badly at scale.

Calling another service like it's a local function

The pitch behind gRPC is that a call to another service should look almost like calling a function in your own code:

user, err := client.GetUser(ctx, &GetUserRequest{UserId: 42})
Enter fullscreen mode Exit fullscreen mode

That line hides a fair amount of machinery. The request gets serialized into Protocol Buffer binary, travels over an HTTP/2 connection, arrives at the other service, gets deserialized back into a typed object, and the response makes the same trip in reverse. None of that is visible to the person writing client.GetUser(...). It reads like a local call because the framework generated all the networking code for you, from a contract you wrote once.

That contract is a .proto file:

syntax = "proto3";

message User {
  int32 user_id = 1;
  string name = 2;
  string role = 3;
}

service UserService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
Enter fullscreen mode Exit fullscreen mode

From this one file, gRPC generates client and server code, in Go, Python, Java, Node, whatever you need, all speaking the exact same schema. A Go service and a Python service can talk to each other without either team hand-writing a client, and a huge class of "the API changed and nobody told us" bugs get caught at compile time instead of in production.

Why Protobuf actually matters

Compare the two representations of the same user object. As JSON:

{ "userId": 42, "name": "John", "role": "admin" }
Enter fullscreen mode Exit fullscreen mode

Every field name travels over the wire on every single call: userId, name, role, spelled out, every time, forever. As a Protobuf binary, only the encoded values and short field identifiers go over the wire, the field names live in the shared schema, not in each message. Individually that difference is a handful of bytes. At millions of calls a second, it's real CPU time spent serializing and parsing text, real bandwidth, real latency.

Protobuf buys you four things at once: smaller payloads, faster serialization since machines handle binary faster than text parsing, type safety since a user_id typo is caught when you generate the code, not when a request 500s in production, and cross-language compatibility, since the .proto file is the single source of truth every language's generated client agrees on.

Streaming is the part people underrate

Everyone talks about gRPC's speed. The feature that actually changes what you can build is streaming.

REST is built around one shape: request, then response. gRPC supports three others.

Server streaming is one request, many responses:

rpc WatchStockPrice(StockRequest) returns (stream StockUpdate);
Enter fullscreen mode Exit fullscreen mode

The client asks once, and the server keeps pushing updates for as long as the connection stays open. Stock tickers, live dashboards, monitoring feeds, anything where the server has more to say after the first response, fits this pattern.

Client streaming flips it: many requests, one response.

rpc UploadRecords(stream Record) returns (UploadSummary);
Enter fullscreen mode Exit fullscreen mode

Useful for bulk uploads or telemetry ingestion, where the client has a continuous flow of data and just wants a summary back at the end.

Bidirectional streaming lets both sides talk whenever they have something to say, independent of each other:

rpc Chat(stream ChatMessage) returns (stream ChatMessage);
Enter fullscreen mode Exit fullscreen mode

This is what chat systems, multiplayer games, and live collaborative tools are built on. There's no request-response ordering to fight against, both sides just send messages as they're ready.

None of these three patterns have a clean REST equivalent. You can fake server streaming with polling or Server-Sent Events, but you're building around REST's constraints instead of using a transport designed for this from the start.

Where the speed actually comes from

No single trick makes gRPC fast. It's five things stacking on top of each other.

Protobuf's binary encoding means less to send and less to parse. HTTP/2 multiplexing means many RPCs share one TCP connection instead of opening a new one per call, so instead of three separate connections carrying one request each, you get one connection carrying three streams at once. Persistent connections mean the TCP handshake and TLS negotiation happen once, not on every call. Generated serialization code means there's no runtime reflection figuring out how to encode a message, that logic was compiled in advance. And native streaming means a long-running exchange doesn't pay the cost of a new request-response cycle for every message.

Take away HTTP/2 and most of this collapses. Multiplexing, header compression, and independent streams (so one slow request doesn't block the others behind it on the same connection) are what make streaming practical rather than theoretical. Protobuf gets most of the credit in casual conversation, but HTTP/2 is doing at least half the work.

Where it bites you in production

gRPC trades one set of failure modes for another, and a few of them are worth knowing before they find you first.

Reusing a Protobuf field number is the one that should scare you most, because it fails silently. Say field 3 used to be email:

message User {
  string email = 3;
}
Enter fullscreen mode Exit fullscreen mode

Then someone removes it and, months later, adds a new field and it happens to land on 3 again:

message User {
  string phone = 3;
}
Enter fullscreen mode Exit fullscreen mode

Any client still running the old schema will read incoming phone numbers as email addresses. No error, no crash, just wrong data flowing into whatever trusts that field. The fix is to never let a number get reused: mark it reserved 3; and move on.

Missing deadline propagation is the quieter one. Service A calls B, B calls C, and C gets slow. If nobody's passing the deadline down the chain, A doesn't know to give up early, it just sits there until its own timeout fires, long after the real problem started. Passing context and deadlines through every hop is what turns "one slow dependency" into "one slow dependency that fails fast" instead of a pileup.

Opening a new connection per call defeats most of what you switched to gRPC for. The persistent-connection benefit only exists if you actually persist the connection. Create the channel once, reuse it, and let HTTP/2 multiplex your calls through it.

Turning one request into dozens of tiny RPCs just moves your chattiness problem from HTTP to gRPC instead of solving it. If a single business operation needs to make fifteen calls to five services, gRPC will do that a bit faster than REST would, but you're still paying network overhead fifteen times. The fix is designing APIs around business operations, not around individual fields or objects.

REST, gRPC or GraphQL

None of these three actually compete for the same job.

REST gRPC GraphQL
Human readable on the wire Yes No Yes
Streaming Limited Excellent Good
Browser-native Yes No, needs a proxy Yes
Best for Public/partner APIs Internal services Flexible client queries
Learning curve Low Medium High

Most systems at any real scale end up using more than one of these at once: REST or GraphQL facing the browser and outside world, since both work with curl, dev tools, and any language under the sun, and gRPC between internal services, where every caller is a service you control and raw speed and streaming matter more than being debuggable from a browser tab.

What it costs you

Binary payloads are fast and also opaque. You can't curl a gRPC endpoint and read the response the way you can with JSON, debugging usually means reaching for a tool like grpcurl or checking generated logs instead of glancing at a browser network tab. Strong schemas catch drift early, but they also mean a breaking change to a .proto file needs real coordination across every service that depends on it, the same problem REST has with field renames, just enforced earlier and more strictly. And long-lived streams are powerful, but they bring their own operational questions: how do you load-balance a connection that's supposed to stay open for an hour, what happens on reconnect, how do you handle backpressure if one side produces faster than the other consumes.

None of that makes gRPC the wrong choice. It makes it a tool with a shape, same as REST has a shape, and the shape matters more once you're operating it in production rather than just building the first version.

The actual decision

gRPC isn't a replacement for REST, it solves a problem REST never tried to solve: fast, strongly typed, streaming-capable communication between services you control. Reach for it when you're building internal microservices talking to each other at real volume, when you need streaming, or when a polyglot team needs one contract that every language can trust.

Stick with REST when your API faces a browser, a partner, or the public internet, where curl-ability, caching, and broad compatibility matter more than shaving milliseconds off an internal call.

The mistake isn't picking REST. The mistake isn't picking gRPC either. It's picking either one because it's the trendy choice this year instead of because it actually matches what you're building.

Explore It Visually

I put together an interactive walkthrough of everything above, the request lifecycle, Protobuf encoding, HTTP/2 multiplexing, all three streaming patterns, so you can watch a gRPC call happen step by step instead of just reading about it: gRPC on SeeItFlow.

Curious where you've actually run into this. Has your team standardized on gRPC internally, or are you still on REST everywhere? And if you've made the switch, what actually broke first?

Top comments (0)