gRPC vs REST: When to Use What
A practical comparison of gRPC and REST for backend services: performance, developer experience, streaming, and the tradeoffs that decide between them.

REST is the default API style for web services. gRPC is the default for high-performance inter-service communication. But the choice is not always obvious — many teams choose REST when gRPC would save them real engineering pain, and others adopt gRPC when REST would have been perfectly adequate.
The answer depends on who your clients are, what performance you need, whether you need streaming, and how much you value browser compatibility versus type safety.
The Fundamental Difference
REST uses HTTP/1.1 (or HTTP/2), sends JSON (typically), and relies on URL paths and HTTP verbs for semantics. gRPC uses HTTP/2, sends Protocol Buffer binary payloads, and defines services and methods in .proto files that generate typed client and server code.
// REST: JSON over HTTP — human-readable, widely supported
// POST /api/users
// Content-Type: application/json
// { "name": "Alice", "email": "alice@example.com" }
// Response: 201 Created
// { "id": "user-123", "name": "Alice", "email": "alice@example.com" }
// gRPC: Protocol Buffers over HTTP/2 — binary, type-safe, fast
// Defined in a .proto file:// user_service.proto
syntax = "proto3";
package users;
service UserService {
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (stream User); // Server streaming
rpc UpdateUsers(stream UpdateUserRequest) returns (UpdateUsersResponse); // Client streaming
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message CreateUserResponse {
string id = 1;
string name = 2;
string email = 3;
}
message User {
string id = 1;
string name = 2;
string email = 3;
int64 created_at = 4; // Unix timestamp
}
message GetUserRequest {
string id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message UpdateUserRequest {
string id = 1;
string name = 2;
string email = 3;
}
message UpdateUsersResponse {
int32 updated_count = 1;
}Performance Comparison
gRPC's binary serialization and HTTP/2 multiplexing give it a significant performance advantage for service-to-service communication.
// JSON serialization vs Protocol Buffers
// A typical user object in JSON: ~150 bytes
const jsonPayload = {
id: "user-123",
name: "Alice Johnson",
email: "alice@example.com",
createdAt: 1648684800,
roles: ["admin", "editor"],
};
// JSON.stringify: ~120 bytes
// Same data in Protocol Buffers: ~45 bytes
// Binary encoding: field numbers + wire types + values
// No field names transmitted — just indices
// 60-70% smaller on average for typical payloads
// Performance implications at scale:
// 10,000 requests/second × 150 bytes (JSON) = 1.5 MB/s network
// 10,000 requests/second × 45 bytes (Protobuf) = 0.45 MB/s network
// 3x less bandwidth, 2-5x faster serialization/deserialization// ❌ Using REST/JSON for high-throughput internal services
interface InternalServiceCall {
protocol: 'REST/JSON';
serialization: '~5ms per large payload';
networkOverhead: 'HTTP/1.1 head-of-line blocking, text headers';
typesSafety: 'Manual — OpenAPI spec may drift from implementation';
// At 50k req/s between services, this adds up significantly
}
// ✅ Using gRPC for high-throughput internal services
interface OptimizedServiceCall {
protocol: 'gRPC/Protobuf';
serialization: '~0.5ms per large payload';
networkOverhead: 'HTTP/2 multiplexing, binary headers';
typeSafety: 'Generated code — proto file IS the implementation contract';
// 10x serialization improvement, multiplexed connections
}Streaming: Where gRPC Shines
gRPC natively supports four communication patterns. REST supports request-response. Everything else requires workarounds like WebSockets, Server-Sent Events, or long polling.
// gRPC streaming patterns
// 1. Unary — standard request/response (like REST)
// Client sends one message, server sends one response
async function getUser(id: string): Promise<User> {
return client.getUser({ id });
}
// 2. Server streaming — server sends a stream of responses
// Client sends one request, server sends many responses
async function* listUsers(pageSize: number): AsyncGenerator<User> {
const stream = client.listUsers({ pageSize, pageToken: '' });
for await (const user of stream) {
yield user;
}
}
// 3. Client streaming — client sends a stream of requests
// Client sends many messages, server sends one response
async function batchUpdateUsers(
updates: AsyncIterable<UpdateUserRequest>
): Promise<UpdateUsersResponse> {
return client.updateUsers(updates);
}
// 4. Bidirectional streaming — both sides stream simultaneously
// Client and server send messages independently
async function syncUsers(
localChanges: AsyncIterable<UserChange>,
onRemoteChange: (change: UserChange) => void
): Promise<void> {
const stream = client.syncUsers();
// Send local changes
for await (const change of localChanges) {
stream.write(change);
}
// Receive remote changes
stream.on('data', onRemoteChange);
}Developer Experience Tradeoffs
// REST developer experience
// Pros:
// - cURL-friendly: curl http://localhost:3000/api/users/123
// - Browser-testable: just open the URL
// - Massive ecosystem: Express, Fastify, every HTTP library
// - Human-readable payloads: JSON is text
// - Low barrier to entry: everyone knows HTTP + JSON
// Cons:
// - No generated types: you write clients manually
// - Schema drift: OpenAPI spec and code get out of sync
// - Verbose: HTTP headers, JSON keys repeated in every message
// - No native streaming: need WebSockets or SSE for real-time
// gRPC developer experience
// Pros:
// - Generated clients: proto file → TypeScript/Go/Python/etc.
// - Type safety: compiler catches mismatches between services
// - Built-in streaming: first-class support for all 4 patterns
// - Performance: binary serialization, HTTP/2 multiplexing
// Cons:
// - Not browser-native: need gRPC-Web proxy or Connect
// - Harder to debug: binary payloads aren't human-readable
// - Tooling complexity: protoc compiler, code generation pipeline
// - Smaller ecosystem: fewer middleware options than REST// REST: easy to debug with standard tools
// $ curl -s http://localhost:3000/api/users/123 | jq
// {
// "id": "user-123",
// "name": "Alice",
// "email": "alice@example.com"
// }
// gRPC: need specialized tools
// $ grpcurl -plaintext localhost:50051 users.UserService/GetUser
// Requires grpcurl, grpc_cli, or Postman gRPC support
// Binary payload must be decoded to be readableDecision Framework
// When to choose REST
const chooseREST = {
publicAPI: true, // Clients are external developers
browserClients: true, // Frontend applications consume the API
simpleCRUD: true, // Standard create/read/update/delete
teamFamiliarity: 'REST', // Team knows REST, not gRPC
debuggability: 'important', // Need to curl and inspect easily
ecosystem: 'broad', // Need extensive middleware options
};
// When to choose gRPC
const chooseGRPC = {
serviceToService: true, // Internal communication between services
highThroughput: true, // 10k+ requests/second between services
streaming: true, // Real-time data feeds, event streams
polyglot: true, // Services in different languages need type safety
latencySensitive: true, // Every millisecond matters
strictContracts: true, // Schema-first development with enforcement
};
// The hybrid approach: REST for external, gRPC for internal
const hybridArchitecture = {
externalAPI: 'REST/JSON', // Public-facing, browser-friendly
apiGateway: 'translates', // Converts REST ↔ gRPC at the boundary
internalServices: 'gRPC', // Type-safe, high-performance
// Best of both worlds — but adds gateway complexity
};Key Takeaways
- REST for external APIs, gRPC for internal services — the most common and practical pattern for modern architectures
- gRPC's type safety prevents schema drift — the
.protofile is both the documentation and the code generation source - Binary serialization matters at scale — Protocol Buffers are 3-10x smaller and 2-5x faster to serialize than JSON
- Streaming is gRPC's killer feature — server streaming, client streaming, and bidirectional streaming are first-class citizens
- REST is easier to debug and adopt — cURL, browser DevTools, and universal familiarity make REST lower friction for most teams
- Consider a hybrid approach — REST gateway for external clients, gRPC between internal services gives the best tradeoffs


