What This Article Is About
You're building an API. Two services need to talk to each other. Or your mobile app needs to talk to your backend. Or you're exposing a public interface for partners. The question: REST or gRPC?
The internet is full of "REST is dead, use gRPC" posts and "gRPC is overkill, use REST" posts. Both are wrong as universal advice. They solve overlapping but different problems. The right answer depends on who your clients are, what your network looks like, and what tooling you're willing to invest in.
This article covers both honestly: how each works, where each shines, where each falls down, the surprising places where the choice doesn't actually matter, and the hybrid pattern most large companies converge on.
The Quick Summary
REST and gRPC are both ways to call functions on a remote server. They differ in protocol, format, and ergonomics.
REST uses HTTP verbs and JSON. Resource-oriented (operate on URLs that represent things). Browser-friendly. Self-documenting through OpenAPI specs.
gRPC uses HTTP/2 and Protocol Buffers. Function-oriented (call methods on services). Faster, with strong typing across languages. Less browser-friendly directly, requires a proxy for browser clients.
Past that, the comparison gets nuanced. Let's go.
REST Up Close
REST stands for Representational State Transfer. Roy Fielding's 2000 dissertation defined it. The core ideas:
Model your API as resources (users, orders, posts). Each resource has a URL.
Use HTTP verbs as actions on those resources (GET to read, POST to create, PUT/PATCH to update, DELETE to remove).
Use HTTP status codes to indicate outcomes (200 OK, 404 Not Found, 500 Internal Server Error).
Be stateless: the server holds no client session state. Each request contains everything it needs.
Use standard media types (JSON, XML, form-encoded).
A typical REST interaction:
GET /api/users/42
Accept: application/json
200 OK
Content-Type: application/json
{"id": 42, "name": "Alice", "email": "[email protected]"}
Most "REST" APIs in the wild are not strictly RESTful (Fielding would call them "HTTP APIs"). They use JSON over HTTP with verbs that vaguely match resource operations. Pragmatic enough for almost everyone.
OpenAPI (formerly Swagger) provides a schema language to describe REST APIs. Tools generate client SDKs, documentation, mock servers, validation, and more from the OpenAPI spec. This is the closest REST gets to gRPC's strong-typing story.
gRPC Up Close
gRPC was developed at Google and released in 2015. It's an opinionated framework for service-to-service RPC.
You define services and methods in a Protocol Buffer (.proto) file:
syntax = "proto3";
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (User);
rpc StreamUsers(StreamUsersRequest) returns (stream User);
}
message User {
int64 id = 1;
string name = 2;
string email = 3;
}
message GetUserRequest {
int64 id = 1;
}
The .proto file is the contract. Both client and server generate code from it (in Go, Java, Python, C++, Rust, Node, C#, Ruby, others). The generated code looks like a normal local function call.
The wire format is binary Protocol Buffers, much smaller and faster to parse than JSON. The transport is HTTP/2 (required, not optional).
A gRPC method call from the client's perspective:
// Looks like a normal function call
user, err := userClient.GetUser(ctx, &pb.GetUserRequest{Id: 42})
No URLs, no HTTP verbs, no JSON parsing. The framework handles serialization, transport, and deserialization.
The Wire Format Difference
This is bigger than it sounds.
JSON is text. Human-readable. Self-describing (you can look at it and understand). But verbose: field names repeat in every message. {"user_id": 42, "is_active": true} is 30 bytes for what's really 2 small values.
Protocol Buffers are binary. Each field is identified by a tag number (1, 2, 3, ...) instead of a name. The schema lives in the .proto file, not in the message. The same data is maybe 10 bytes.
Implications:
Bandwidth: Protocol Buffers are typically 3-5x smaller than equivalent JSON. Matters on mobile or high-volume internal traffic.
Parse speed: Protocol Buffers are 5-10x faster to parse. JSON parsing is dominant CPU work in many JSON-heavy services.
Debuggability: JSON you can cat a captured request and read it. Protocol Buffers need decoding tools.
Schema evolution: Protocol Buffers have explicit forward and backward compatibility rules built in. JSON requires conventions.
Side-by-Side Comparison
| REST | gRPC | |
|---|---|---|
| Format | JSON (text) | Protocol Buffers (binary) |
| Transport | HTTP/1.1 or HTTP/2 | HTTP/2 (required) |
| Schema | Optional (OpenAPI) | Required (.proto) |
| Code generation | Optional (from OpenAPI) | Mandatory (from .proto) |
| Browser support | Native | Needs gRPC-Web proxy |
| Streaming | Limited (SSE, chunked) | Native: client, server, bidirectional |
| Caching | Native (HTTP caches, CDNs) | Hard (binary, opaque) |
| Performance | Good | Better (5-10x in some benchmarks) |
| Debuggability | Easy (curl, browser DevTools) | Harder (need grpcurl, etc.) |
| Tooling ecosystem | Universal | Strong but specialized |
| Typical use case | Public APIs, browser apps | Service-to-service, mobile |
Streaming: Where gRPC Pulls Ahead
gRPC has four method types built in:
Unary: normal request-response. Client sends one message, server returns one.
Server streaming: client sends one request, server returns a stream of messages. Useful for "get me all events since X" or "watch this resource for updates".
Client streaming: client sends a stream of messages, server returns one response. Useful for upload, batched ingestion.
Bidirectional streaming: both sides stream concurrently. Useful for chat, real-time games, collaborative editing.
This works because gRPC is on HTTP/2, which natively supports long-running streams in both directions.
REST can do streaming too, but it's awkward:
Server streaming over REST is usually Server-Sent Events (SSE) or chunked HTTP. Works.
Client streaming is unusual; people usually fall back to multiple POSTs or WebSockets.
Bidirectional streaming over REST is essentially WebSockets, a different protocol.
If your use case is heavy on streaming, gRPC is much more natural.
Error Handling
REST uses HTTP status codes. 4xx for client errors, 5xx for server errors. Body usually contains JSON with details.
404 Not Found
Content-Type: application/json
{"error": "user_not_found", "message": "User 42 does not exist"}
The HTTP status code is the primary error signal. Bodies vary by API.
gRPC has its own status codes (NOT_FOUND, INVALID_ARGUMENT, PERMISSION_DENIED, INTERNAL, ...) plus a string message and rich details (you can attach arbitrary structured data to errors). gRPC errors don't map perfectly to HTTP status codes; the framework handles the translation.
Both approaches are reasonable. gRPC's is more structured but locks you into its model. REST's is more flexible but inconsistent across APIs.
Versioning
How do you change an API without breaking existing clients?
REST patterns:
URL versioning: /v1/users, /v2/users. Simple but you maintain multiple parallel surface areas.
Header versioning: Accept: application/vnd.example.v2+json. Cleaner URLs but harder to test.
Additive evolution: don't break things, just add new optional fields. Most realistic at scale.
gRPC patterns:
Protocol Buffers have explicit compatibility rules: never reuse a field tag, always make new fields optional, never remove fields, only add. Following these rules, you can evolve a service indefinitely without breaking clients.
If you need a hard break, you create a new service (UserServiceV2) and run it alongside the old.
gRPC's discipline is mechanically enforced by the schema. REST's discipline is mostly cultural.
Browser Support
This is the dealbreaker for many gRPC adoption decisions.
REST works in any browser via fetch() or XMLHttpRequest. JSON parsing is built into JavaScript. Cookies, CORS, caching, all the web-standard infrastructure works.
gRPC does not work directly in browsers. Browsers don't expose enough of HTTP/2 to do gRPC properly. The workaround is gRPC-Web: a slightly different wire format that sits on regular HTTP/1.1 or HTTP/2 (without the streaming features), with a proxy on the server side that translates to real gRPC.
gRPC-Web works but loses some features (no client streaming, no bidirectional streaming). Most browser apps still use REST or GraphQL for this reason.
Caching
REST is cache-friendly. HTTP caches (browser cache, CDN, reverse proxies like Varnish) understand GET requests. Cache-Control headers, ETag revalidation, If-Modified-Since: all the standard machinery works for free.
gRPC is hard to cache. Methods don't have a notion of "safe" or "idempotent" at the protocol level (the framework doesn't know which methods are reads). Bodies are opaque binary. CDNs don't natively understand gRPC.
For high-read APIs (blog posts, product catalogs), REST's caching story is a real advantage.
When to Use REST
Public APIs. External developers will use any language and many tools. JSON over HTTP is the lowest common denominator. Asking partners to install protoc and learn gRPC is a non-starter for most.
Browser-first apps. Direct browser access without a translation proxy.
Heavy caching. Read-mostly APIs benefit from CDN caching natively.
Simple CRUD. The resource model fits naturally. CRUD on resources is what REST was designed for.
Quick prototypes. Less ceremony to start. curl is your debugger. JSON is your wire format. No code generation needed.
When debuggability matters more than performance. Logs that show plain JSON are easier to grep and read than binary captures.
When to Use gRPC
Internal microservices. Performance matters. The schema discipline pays off when many services depend on each other's APIs.
Polyglot teams. Generated clients in 10+ languages stay in sync automatically. Adding a new language to your stack is "run protoc against the existing .proto files".
Streaming-heavy use cases. Real-time updates, telemetry ingestion, chat. gRPC streaming is much nicer than fighting with WebSockets or SSE.
Mobile apps. Smaller payloads matter on cellular. Native iOS and Android gRPC libraries exist.
Service mesh integration. Many service meshes (Istio, Linkerd) integrate well with gRPC. Distributed tracing, retries, timeouts come naturally.
High-throughput, low-latency. The 5-10x performance edge over JSON+HTTP/1 actually matters when you're doing 100k+ RPS.
The GraphQL Aside
Both REST and gRPC are server-driven (the API tells the client what shapes are available; the client picks one).
GraphQL flips this: the client describes exactly what it wants in a query, and the server returns just that.
query {
user(id: 42) {
name
email
posts(limit: 5) {
title
}
}
}
Use GraphQL when: clients have varied data needs and want to avoid over-fetching/under-fetching. Common for mobile apps backing onto a complex schema. The Facebook origin story.
Skip GraphQL when: simple CRUD, or you need fine control over query cost. Letting clients craft arbitrary queries means a single client can craft an expensive one and bring down your DB.
GraphQL has its own ecosystem (Apollo, Relay) and its own pain points (N+1 queries, caching is harder than REST). It's a third option, not a replacement for either.
Other Considerations
Rate limiting: easier to apply per-URL in REST. Per-method gRPC rate limiting needs additional infrastructure (interceptors, service mesh policies).
Authentication: REST uses headers (Bearer tokens, cookies). gRPC uses metadata (similar concept). Both work fine.
Tracing and observability: both integrate with OpenTelemetry. gRPC has slightly nicer integration because everything is a method call with structured metadata.
Operational visibility: REST shows up in nginx logs, browser DevTools, any HTTP-aware tool. gRPC is more opaque; you need gRPC-aware tools (grpcurl, BloomRPC, Postman's gRPC support, server reflection).
Backwards compatibility tooling: Buf and similar tools detect breaking changes in .proto files. The equivalent for OpenAPI exists but is less standardized.
The Hybrid Approach (What Most Companies Actually Do)
Large companies typically don't pick one. They run both:
REST for public APIs and browser clients. The lowest common denominator. Maximum reach.
gRPC for internal service-to-service. Performance, type safety, code generation.
An API gateway at the edge translates external REST into internal gRPC. Or both stand on their own.
Tools that automate this: gRPC-Gateway (generates a REST proxy from a .proto file with HTTP annotations). Connect (a newer protocol that works as both gRPC and REST without translation, designed by Buf).
The "hybrid" pattern is the realistic answer. You get universal accessibility where it matters and high performance where it matters. They're not really competitors; they're tools for different jobs.
Edge Cases and Gotchas
gRPC connections are sticky. Once a client opens a gRPC connection to a server, it tends to keep using it. Without active load balancing, traffic concentrates on a few backends. Service meshes solve this; load balancers without HTTP/2 awareness don't.
Long-lived gRPC connections and proxies. Many proxies have idle timeouts (60 seconds is common). Long-running streams can get cut. Configure keepalives.
Protocol Buffers are not human-friendly under the hood. You will eventually need to decode a captured message and curse at binary. protoc --decode_raw helps but needs the .proto file.
OpenAPI spec drift. The OpenAPI spec is supposed to match the actual API. Often, after some time, it doesn't. Code generation from the OpenAPI spec helps; doc-only OpenAPI rots.
JSON ambiguity: JSON has no native int64 (browsers parse to floats, losing precision past 2^53). REST APIs have to send big numbers as strings. Protocol Buffers don't have this problem.
Versioning .proto files: never reuse field tag numbers. Always treat removed fields as reserved. Tools like Buf catch this; manual review doesn't always.
gRPC HTTP/2 requirement: some load balancers and proxies don't support HTTP/2 end-to-end. You might end up with HTTP/2 at the edge and HTTP/1 internally, which breaks gRPC. Verify the full path.
Cardinality of methods. Real services have hundreds of methods. Both REST and gRPC handle this, but REST surface area can become inconsistent over time (no enforced schema). gRPC stays disciplined because the .proto is the contract.
How to Decide for a New Service
Ask yourself:
1. Who are the clients? Browsers? Mobile apps? Other services? Public partners?
2. Is performance a real bottleneck or just a "wouldn't it be nice"?
3. Do you have streaming requirements?
4. Will the team invest in gRPC tooling and discipline?
5. Are existing services in your company gRPC or REST? Match unless you have a strong reason.
Default heuristic: internal Go/Java/Python service-to-service traffic, gRPC. Public API or browser client, REST. If you're unsure, REST is the safer default. You can always introduce gRPC later for the hot paths that need it.
The One Thing to Remember
REST is the universal default; gRPC is the specialist. For internal microservices where performance, type safety, and streaming matter, gRPC wins. For public APIs and browser-first applications, REST is still the right call. They're not competitors; they solve overlapping but distinct problems. Most large companies run both, with REST at the public edge and gRPC behind it. The choice between them is rarely a religious one; it's a practical one based on who your clients are and what tooling you're willing to invest in.