apis · level 8

gRPC vs REST

Schema-first, streaming, and when each fits.

200 XP

gRPC vs REST

The "REST vs gRPC" debate is mostly a category error. They solve overlapping but distinct problems. The right answer in most modern stacks is both — REST + JSON for the public edge, gRPC + Protobuf inside the trust boundary, and a thin proxy bridging them when browsers need to reach gRPC services.

The two philosophies

REST + JSON gRPC + Protobuf
Spec ordering Spec-first (OpenAPI) Schema-first (.proto files)
Wire format Text (JSON) Binary (length-prefixed Protobuf)
Transport HTTP/1.1 or HTTP/2 HTTP/2 only
Streaming Server-Sent Events, chunked transfer First-class: unary, server, client, bidi
Browser support Native Needs gRPC-Web proxy
Tooling for clients Generated from OpenAPI (50+ languages) Generated from .proto (10+ first-party)
Debugging Curl, browser devtools, JSON eyeballs grpcurl, Wireshark + Protobuf decoder

REST optimises for ubiquity. Every browser, every proxy, every CDN, every monitoring tool understands JSON over HTTP. The cost is a few KB of text instead of a few hundred bytes of binary, plus a forever-debate about whether DELETE should accept a body.

gRPC optimises for rigour and throughput. The schema is the source of truth and the linter enforces it across every language. Streaming is built in. The wire format is small and CPU-cheap to decode. The cost is browsers that can't talk to your services natively and a learning curve for ops teams used to seeing JSON.

When each fits

Use REST when…

  • The API is public — third parties, mobile apps, browsers will hit it directly.
  • Discoverability matters more than perf — anyone can curl your endpoint and see the shape.
  • You need to work through arbitrary proxies, CDNs, WAFs that have no idea what gRPC is.
  • The ecosystem of tools (Postman, Stripe-style sandboxes, OpenAPI-generated clients) is your ergonomics moat.

Use gRPC when…

  • The traffic is service-to-service inside a controlled network.
  • You need streaming — real-time updates, log tails, AI/voice token streams, bidi chat.
  • You need strict schema enforcement across multiple languages and many services.
  • Throughput per CPU matters — gRPC + Protobuf is roughly 2–10× cheaper to serialise/deserialise than JSON.

Use both when…

  • You have an internal mesh of microservices (gRPC) and a public API gateway (REST) that translates.
  • You publish a public REST API, then add a gRPC API for high-volume integrations once a partner asks for it.

This third case is the most common modern shape.

The four streaming patterns

gRPC's streaming is the feature REST struggles to match without ergonomic compromises (long-poll, SSE, chunked transfer):

service Chat {
  // Unary — request, response (like REST)
  rpc GetMessage(MessageRequest) returns (Message);

  // Server-streaming — one request, many responses
  rpc ListMessages(ListRequest) returns (stream Message);

  // Client-streaming — many requests, one response
  rpc UploadAudio(stream AudioChunk) returns (Transcript);

  // Bidirectional — both sides stream concurrently
  rpc Chat(stream ClientMessage) returns (stream ServerMessage);
}

Use cases:

  • Unary — most operations.
  • Server-streaming — search-as-you-type, log tail, OpenAI's chat completion (token-by-token).
  • Client-streaming — file upload in chunks, audio capture, batch insert with running progress.
  • Bidirectional — multi-player game state, real-time collaboration, chat between humans and an LLM.

REST equivalents exist (SSE for server-streaming, chunked transfer for both directions) but each has rough edges: no out-of-band frame metadata, harder backpressure, no native protobuf decoding.

gRPC-Web — the browser bridge

Browsers cannot terminate native gRPC because it depends on HTTP/2 trailers, which the Fetch API does not expose. The fix is gRPC-Web:

Browser ──[gRPC-Web]──▶ Envoy ──[native gRPC]──▶ backend
        (HTTP/1.1 or                       (HTTP/2)
         HTTP/2 sans trailers,              full streaming
         no client-streaming,               (uni + bidi)
         server-streaming OK)

You write the .proto, generate a TypeScript client with protoc-gen-grpc-web, run an Envoy proxy in front of your services. Browsers call the proxy; the proxy calls native gRPC.

Two limits to know:

  1. Client-streaming and full bidi-streaming are not supported in gRPC-Web (browser API limitations). Server-streaming is fine.
  2. The wire bytes are slightly different from native gRPC (base64 framing) — your backend doesn't care because Envoy translates.

Schema-first vs spec-first — a worked example

A .proto definition is loud about what you must commit to:

syntax = "proto3";
package users.v1;

service Users {
  rpc Get(GetUserRequest) returns (User);
  rpc Update(UpdateUserRequest) returns (User);
}

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

message GetUserRequest { int64 id = 1; }
message UpdateUserRequest {
  int64 id = 1;
  google.protobuf.FieldMask update_mask = 2;
  User user = 3;
}

Field numbers (= 1, = 2, = 3) are part of the wire contract. Renumbering breaks every old client. Removing a field requires a multi-step migration (deprecate → release → remove). The compiler catches everything.

OpenAPI's equivalent:

paths:
  /users/{id}:
    get:
      operationId: getUser
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer, format: int64 }
      responses:
        '200':
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
components:
  schemas:
    User:
      type: object
      required: [id, name, email]
      properties:
        id:    { type: integer, format: int64 }
        name:  { type: string }
        email: { type: string, format: email }

OpenAPI is more flexible — fields don't have stable numbers, you can add and remove them by JSON name. That's a strength (lower change cost) and a weakness (easier to break clients accidentally).

A linter on either side (Buf for proto, Spectral for OpenAPI) catches breaking changes before they ship. Use one. Don't merge a PR that changes the schema without a lint pass.

Performance — the numbers people quote

The "gRPC is 7-10× faster than REST" benchmarks are real but contextual:

  • Wire size: Protobuf is roughly 5× smaller than equivalent JSON for typical messages. Less for tiny ones.
  • Serialise/deserialise CPU: Protobuf is roughly 5–10× cheaper than JSON in most languages.
  • Latency: HTTP/2 multiplexing eliminates head-of-line blocking. For services that send many concurrent calls, p99 drops noticeably.

These wins matter when you're serving millions of RPCs/sec from a fixed CPU budget. They are noise for an internal CRUD app serving 10 RPS.

Where REST stays better

  • Public APIs that mobile and web teams will integrate. No one wants to deploy an Envoy sidecar to talk to your service.
  • Cacheable reads. GET requests cache at every CDN layer; gRPC is invisible to a CDN.
  • Idempotent retries with HTTP semantics — proxies, retry middleware, and load balancers all understand 5xx-then-retry.
  • Documentation discoverability. OpenAPI plus a Swagger UI is a single URL where any partner can read the API.

Picking a default

Given a greenfield service inside a Kubernetes cluster, default to gRPC + Protobuf with Buf for the schema, and OpenAPI + Spectral for the gateway that translates to JSON for outside callers.

Given a greenfield public API, default to REST + OpenAPI, generate clients with OpenAPI Generator, and ship Swagger UI at /docs.

Don't burn a sprint on this decision. Pick correctly for the audience, lint for breaking changes, and move on.

Tools in the wild

5 tools
  • gRPCfree tier

    Reference implementations in C++/Go/Java/Python/Node — generate clients from .proto.

    library
  • Generates clients/servers in 50+ languages from an OpenAPI spec.

    library
  • Buffree tier

    Modern Protobuf toolchain — lint, breaking-change detection, schema registry.

    service
  • Lint OpenAPI specs against rule sets — naming, status codes, security.

    cli
  • Proxy that translates gRPC-Web ↔ native gRPC for browser callers.

    service