The API protocol question is not “which is best” but “which is best for this specific communication pattern.” After years of teams making this choice wrong - migrating to GraphQL for internal services, using REST for high-throughput microservices, or adopting gRPC for public APIs - the decision criteria are now clear.

Here is the decision tree, backed by real performance data and hard-won lessons.

The 30-Second Decision

  • Internal service-to-service, high throughput - gRPC
  • Public-facing API consumed by third parties - REST
  • Client applications with varied data needs - GraphQL
  • Simple CRUD with minimal clients - REST

If your use case maps clearly to one of these, you probably do not need to read further. If it does not, the details below will help.

Protocol Fundamentals

REST

REST is not a protocol - it is an architectural style over HTTP. Resources have URLs, you use HTTP methods (GET, POST, PUT, DELETE), and responses are typically JSON. Its strength is universality: every language, every platform, every developer knows how to call a REST API.

GET /api/users/123
Accept: application/json

200 OK
Content-Type: application/json
{
  "id": 123,
  "name": "Alice",
  "email": "[email protected]",
  "orders": [1, 2, 3]  // Now you need another call for order details
}

GraphQL

GraphQL is a query language for APIs. The client specifies exactly what data it needs, and the server returns that exact shape. One endpoint, flexible queries.

query {
  user(id: 123) {
    name
    email
    orders(last: 5) {
      id
      total
      items {
        name
        price
      }
    }
  }
}

gRPC

gRPC is a binary RPC framework built on HTTP/2 and Protocol Buffers. You define services and messages in .proto files, generate client and server code, and call remote methods as if they were local functions.

syntax = "proto3";

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc ListOrders(ListOrdersRequest) returns (stream Order);
}

message GetUserRequest {
  int32 id = 1;
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

Performance - Protobuf vs JSON

This is where gRPC separates from the pack. Protocol Buffers is a binary serialization format that is smaller and faster to parse than JSON.

Metric JSON (REST/GraphQL) Protobuf (gRPC) Difference
Payload size (typical) 100 bytes 45 bytes 55% smaller
Serialization time ~500ns ~100ns 5x faster
Deserialization time ~800ns ~150ns 5x faster
Schema validation Runtime (if at all) Compile-time Safer
Human readable Yes No (binary) Tradeoff

For a single API call, this difference is irrelevant. For a microservices architecture processing millions of inter-service calls per second, the difference in CPU time and network bandwidth is substantial.

A concrete example: a payment processing service at a fintech company handles 50,000 requests per second. Each request triggers 8 internal service calls. That is 400,000 internal calls per second. Switching from JSON to Protobuf for internal communication reduced serialization CPU usage by 60% and network bandwidth by 40%. The infrastructure cost savings paid for the migration in three months.

HTTP/2 Streaming - gRPC’s Killer Feature

REST and GraphQL typically operate in request-response mode over HTTP/1.1. gRPC uses HTTP/2 natively, which gives it three streaming patterns that REST cannot match:

Server streaming: The client sends one request, the server sends back a stream of responses.

rpc ListOrders(ListOrdersRequest) returns (stream Order);
// Server-side streaming
func (s *server) ListOrders(req *pb.ListOrdersRequest, stream pb.UserService_ListOrdersServer) error {
    orders := fetchOrders(req.UserId)
    for _, order := range orders {
        if err := stream.Send(order); err != nil {
            return err
        }
    }
    return nil
}

Client streaming: The client sends a stream of messages, the server responds once.

rpc UploadLogs(stream LogEntry) returns (UploadResponse);

Bidirectional streaming: Both sides send streams simultaneously. This is the pattern for real-time communication - chat, live updates, collaborative editing.

rpc Chat(stream ChatMessage) returns (stream ChatMessage);

REST can approximate streaming with Server-Sent Events (SSE) or WebSockets, but these are bolted on, not native. GraphQL has subscriptions, which use WebSockets under the hood. gRPC streaming is built into the protocol with proper flow control, backpressure, and cancellation.

Developer Experience Comparison

Performance matters, but developer experience determines adoption speed and maintenance cost.

Aspect REST GraphQL gRPC
Learning curve Low Moderate High
Tooling (browser) Excellent (curl, Postman) Good (GraphiQL, Apollo Studio) Poor (needs grpcurl or Evans)
Code generation Optional (OpenAPI) Optional (codegen) Required (protoc)
Type safety Optional (if using OpenAPI) Schema-enforced Compile-time enforced
Documentation Swagger/OpenAPI Self-documenting (introspection) Proto files are the docs
Error handling HTTP status codes errors array in response Status codes + details
Caching HTTP caching (CDN, browser) Complex (no GET by default) No HTTP caching
File uploads Multipart form data Complex Not native (use HTTP fallback)
Browser support Native Native Requires grpc-web proxy

The browser support row deserves emphasis. gRPC uses HTTP/2 features that browsers do not expose to JavaScript. To call gRPC services from a browser, you need grpc-web, which requires a proxy (Envoy or grpc-web-proxy) that translates HTTP/1.1 to HTTP/2. This is not a minor inconvenience - it adds infrastructure, latency, and operational complexity.

This single limitation is why gRPC is rarely used for public-facing APIs consumed by web browsers. REST and GraphQL work natively in browsers with a simple fetch() call.

When Each Wins Definitively

REST Wins When:

Your API is public. Third-party developers expect REST. They know how to use curl, Postman, and fetch(). The onboarding cost is near zero. Stripe, Twilio, and GitHub built massive developer ecosystems on REST APIs.

Caching matters. HTTP caching is mature and widely deployed. CDNs cache GET requests automatically. Browsers cache responses. Reverse proxies cache at the edge. GraphQL’s single endpoint and POST-based queries make caching significantly harder. gRPC’s binary protocol is not cacheable by standard HTTP infrastructure.

Simplicity is the priority. A CRUD API with 10-20 endpoints does not need the complexity of GraphQL schemas or Protobuf definitions. REST with OpenAPI documentation is the right fit.

GraphQL Wins When:

Clients have varied data needs. A mobile app needs a user’s name and avatar. A web dashboard needs the user’s name, email, order history, and account settings. With REST, you either over-fetch (send all data to all clients) or create client-specific endpoints. GraphQL lets each client request exactly what it needs.

You are building a BFF (Backend for Frontend). GraphQL is an excellent aggregation layer that sits in front of multiple backend services and presents a unified API to frontend clients. The resolver pattern maps naturally to fetching data from different sources.

Rapid frontend iteration. When the frontend team needs to add a new field to a page, they change the query. No backend deployment required (assuming the field exists in the schema). This reduces coordination overhead between frontend and backend teams.

gRPC Wins When:

Internal microservices. Service-to-service communication is where gRPC shines. Both sides are controlled by the same organization, browser support is irrelevant, and the performance and type-safety benefits are fully realized.

Streaming is a core requirement. If your system needs server-push, client streaming, or bidirectional streaming, gRPC handles this natively with proper flow control. Building equivalent functionality with REST requires WebSockets and custom protocol handling.

Polyglot environments. A single .proto file generates client and server code in Go, Java, Python, Rust, C++, and more. The generated code handles serialization, deserialization, and transport. This is more reliable than maintaining OpenAPI specs and hoping that each language’s code generator produces compatible clients.

Real Migration Stories

Shopify: REST to GraphQL. Shopify migrated its public API from REST to GraphQL in 2018 and continued expanding it through 2025. The motivation was not performance - it was developer experience. Merchants building storefronts needed different data on every page. REST required dozens of calls per page load. GraphQL reduced this to one query per page. The result: mobile storefronts loaded 50% faster due to reduced over-fetching.

Netflix: REST to gRPC for internal services. Netflix moved internal service communication from REST/JSON to gRPC/Protobuf. The performance gains were measurable - lower latency, lower CPU usage, smaller payloads - but the bigger win was reliability. Protobuf’s strict typing caught interface mismatches at compile time instead of in production. The number of incidents caused by API contract violations dropped by over 70%.

GitHub: REST and GraphQL coexisting. GitHub offers both REST and GraphQL APIs. The REST API is stable and widely used. The GraphQL API gives power users the ability to fetch complex data in a single request. Neither replaced the other. This is the mature approach - use the right protocol for the right use case.

The Decision Tree

Is this a public API for third-party developers?
  YES -> REST (with OpenAPI documentation)
  NO  -> Continue

Is this internal service-to-service communication?
  YES -> Do you need streaming?
    YES -> gRPC
    NO  -> Is throughput > 10K requests/second?
      YES -> gRPC (performance matters at scale)
      NO  -> REST or gRPC (team preference)
  NO  -> Continue

Is this a client-facing API with varied data needs?
  YES -> Do clients need different subsets of data?
    YES -> GraphQL
    NO  -> REST
  NO  -> REST (the default that always works)

The Pragmatic Take

The worst architectural decision is choosing a protocol to seem modern. GraphQL for a two-endpoint internal API is over-engineering. gRPC for a public API consumed by JavaScript SPAs is a poor fit. REST for a high-throughput microservices backbone is leaving performance on the table.

Most systems should use REST as the default and introduce GraphQL or gRPC when the problem specifically demands it. Start simple. Measure. Migrate the parts that need it. The protocol that works and ships beats the protocol that is technically superior and takes six months to adopt.