If you have spent time building services that talk to each other, you know the friction. One service needs to call another, and suddenly there are decisions to make that have nothing to do with the actual problem. What does the URL look like. What goes in the body. How do errors get surfaced. How do you version this without breaking clients that already depend on it. REST made all of this approachable, and for a long time that was the right tradeoff. The ecosystem matured around it, and it became the default.
The default has limits, though. At a certain scale, approachable and rigorous stop being the same thing.
By the mid-2000s, Google was running a distributed system at a scale nobody else had reached. Search alone was an orchestration of dozens of subsystems working in parallel, each service calling several others, each of those calling several more. To manage all of it, the infrastructure team had built an internal RPC framework called Stubby. It was opinionated, fast, and completely tied to Google's own infrastructure stack.
Stubby was never going to be shareable as-is. But as the industry started wrestling with microservices at scale, Google took the ideas behind it, rebuilt them on open standards like HTTP/2 and Protocol Buffers, and open sourced the result in 2015. The timing landed well. Companies were splitting monoliths into dozens of services and discovering that the communication layer between those services was a harder problem than it first appeared.
REST is genuinely good at what it was designed for. Public APIs, browser-facing endpoints, anything where human readability and broad compatibility matter. JSON is easy to inspect, curl works everywhere, and every language has an HTTP client. For that use case, REST is hard to beat.
The gaps show up at the seams.
There is no formal contract. You might write OpenAPI docs, or you might not. The docs might be current, or they might have drifted. A client consuming your API is trusting that the field names and types in the documentation match what the server actually sends, with no mechanism to enforce that trust. When it breaks, it breaks at runtime in ways that take time to track down.
There is also more overhead than necessary for internal traffic. JSON is text, and parsing text is slower than reading a binary format. HTTP/1.1 either opens a new connection per request or requires a connection pool, and both options carry complexity. For a public API serving diverse clients, that overhead is a reasonable cost. For two internal services in the same data center making hundreds of thousands of calls per day, it accumulates into something worth fixing.
Streaming is a separate problem. WebSockets and server-sent events both work, but they are distinct protocols with their own tooling and their own failure modes. REST was not designed with streaming in mind, and trying to add it later always feels like it.

Three decisions at the foundation level explain most of what makes gRPC behave differently from REST.
Protocol Buffers replace JSON as the serialization format. You define messages and services in a .proto file, and a code generator produces client and server code in your target language. The generated code handles serialization and deserialization automatically, and the schema acts as a compile-time contract. If the server changes a field name without updating the client, the build breaks. That is a dramatically better place to catch the problem than a production incident at 2am.
HTTP/2 is the transport. Unlike HTTP/1.1, which processes requests sequentially on a connection, HTTP/2 multiplexes them, sending multiple requests in flight over a single connection in parallel, without head-of-line blocking. Header compression also helps when you are making large numbers of small requests, which is exactly the pattern of internal service calls.
Streaming becomes a first-class concern rather than an afterthought. gRPC exposes four patterns: unary (the standard request-response), server streaming (one request, a stream of responses), client streaming (a stream of requests, one response), and bidirectional streaming (both sides streaming concurrently). All four use the same code generation pipeline and the same tooling, so you are not learning a new protocol each time you need a different communication shape.
Everything starts with the proto file. This is where you define the messages your service accepts and returns, and the methods it exposes.
syntax = "proto3";
package user;
service UserService {
rpc GetUser (GetUserRequest) returns (UserResponse);
rpc CreateUser (CreateUserRequest) returns (UserResponse);
}
message GetUserRequest {
string user_id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message UserResponse {
string user_id = 1;
string name = 2;
string email = 3;
string created_at = 4;
}