Web Development · APIs
gRPC in 2026: When to Use It Instead of REST or GraphQL
gRPC has been available for years but many teams default to REST without thinking through the tradeoffs. Here's how gRPC works, where it fits, and where it doesn't.
Anurag Verma
8 min read
Sponsored
REST is the default. If you’re building an HTTP API and not thinking hard about the choice, you’re writing REST. That’s fine most of the time. But it means gRPC (which solves specific problems better than REST can) tends to get skipped even when it would be the better fit.
gRPC is not a trendy alternative to REST. It’s a different tool for a different job. The clearest sign you should consider it: you’re building services that talk to other services, not to browsers, and you care about efficiency and strict interface contracts.
What gRPC Actually Is
gRPC is an RPC (Remote Procedure Call) framework built by Google, open-sourced in 2015, and now part of the CNCF ecosystem. It uses HTTP/2 as its transport and Protocol Buffers (protobuf) as its default serialization format.
The core idea: you define your API in a .proto file, and gRPC generates client and server code in your target languages. Both sides agree on the interface at compile time, not at runtime.
A simple service definition:
syntax = "proto3";
package orders;
service OrderService {
rpc GetOrder (GetOrderRequest) returns (Order);
rpc CreateOrder (CreateOrderRequest) returns (Order);
rpc StreamOrderUpdates (OrderStatusRequest) returns (stream OrderStatus);
}
message GetOrderRequest {
string order_id = 1;
}
message Order {
string id = 1;
string customer_id = 2;
double total = 3;
string status = 4;
}
message CreateOrderRequest {
string customer_id = 1;
repeated OrderItem items = 2;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
}
message OrderStatusRequest {
string order_id = 1;
}
message OrderStatus {
string order_id = 1;
string status = 2;
string timestamp = 3;
}
Run the protobuf compiler, and you get generated code for Python, Go, Java, TypeScript, or any other supported language. The server implements the interface. Clients call it as if it’s a local function.
The Four Call Types
gRPC supports four patterns, which is where it diverges most clearly from REST:
Unary: One request, one response. The direct equivalent of a REST POST or GET.
response = stub.GetOrder(GetOrderRequest(order_id="ord-123"))
print(response.status)
Server streaming: One request, a stream of responses. The server sends data continuously until it’s done. Useful for pushing real-time updates or paginating large result sets without the client polling.
for status_update in stub.StreamOrderUpdates(OrderStatusRequest(order_id="ord-123")):
print(f"Status: {status_update.status} at {status_update.timestamp}")
Client streaming: The client sends a stream of requests, the server responds once when done. Batch ingestion, uploading chunks of a file, accumulating telemetry.
Bidirectional streaming: Both sides stream simultaneously. Real-time collaborative tools, live dashboards, bidirectional audio/video signaling.
REST doesn’t have a clean equivalent to any of the streaming modes. You end up bolting on WebSockets or SSE as a separate layer. gRPC’s streaming is first-class and works over the same HTTP/2 connection as unary calls.
Why Protobuf Matters
JSON is human-readable, schemaless, and large. Protobuf is binary, schema-enforced, and small.
A JSON payload representing a product:
{
"product_id": "prod-abc123",
"name": "Wireless Keyboard",
"price": 79.99,
"in_stock": true
}
The equivalent protobuf message, serialized, strips the field names entirely. The schema (field numbers and types) is compiled into both sides. The wire format contains only values. Typical protobuf payloads are 3-10x smaller than equivalent JSON, and serialization/deserialization is proportionally faster.
For services exchanging millions of messages per day, the bandwidth and CPU savings compound. For an internal microservices cluster handling thousands of requests per second, this matters.
The downside: you lose human-readability. You can’t curl a gRPC endpoint and read the response in a terminal without tooling. grpcurl (the gRPC equivalent of curl) and Postman’s gRPC support fill the gap, but the debugging experience is still more friction than JSON over REST.
Where gRPC Fits Well
Internal microservices: Services that never talk to browsers are the sweet spot. No JSON parsing overhead, strict interface contracts enforced at compile time, generated clients that stay in sync with the server, and streaming support without additional protocols. Go microservices communicating with Python services via gRPC is a common and well-supported pattern.
High-throughput data pipelines: Streaming data between services (ingestion workers feeding into processing services, telemetry collectors sending to aggregators) benefits from gRPC’s streaming call types and binary serialization.
Multi-language teams: When Go services, Python services, and Java services need to talk to each other, the generated clients from a shared .proto file mean the interface contract is enforced in all three languages from a single source of truth. Compare this to REST, where you’d need to maintain an OpenAPI spec and separately generate clients, or just hope everyone implements the same field names.
Real-time bidirectional communication between services: Chat systems at the service layer, live document sync backends, multiplayer game servers. gRPC bidirectional streaming is built for this; REST + WebSockets is a patchwork.
Where gRPC Fits Poorly
Public-facing APIs: Browsers can’t call gRPC directly. gRPC-Web, a proxy layer, translates gRPC to a format browsers can use, but it adds operational complexity and loses bidirectional streaming. If your API is consumed by browser JavaScript, REST or GraphQL remains the simpler choice.
Simple CRUD APIs with infrequent changes: The overhead of maintaining .proto files, running the code generator, and managing schema evolution is overhead. If your API has five endpoints that change once a quarter, REST with an OpenAPI spec is faster to maintain.
Teams without tooling familiarity: gRPC has a steeper initial setup curve. Reflection, tooling like grpcurl, error handling (gRPC has its own status codes), and load balancer configuration (HTTP/2 load balancing behaves differently from HTTP/1.1) all need attention.
Setting Up a gRPC Server in Python
Install the dependencies:
pip install grpcio grpcio-tools
Generate server and client code from the proto file:
python -m grpc_tools.protoc \
-I. \
--python_out=. \
--grpc_python_out=. \
orders.proto
This produces orders_pb2.py (message classes) and orders_pb2_grpc.py (service stubs and base classes).
Implement the server:
import grpc
from concurrent import futures
import orders_pb2
import orders_pb2_grpc
class OrderServicer(orders_pb2_grpc.OrderServiceServicer):
def GetOrder(self, request, context):
# In production, you'd query a database here
return orders_pb2.Order(
id=request.order_id,
customer_id="cust-456",
total=149.99,
status="processing",
)
def StreamOrderUpdates(self, request, context):
# Yield updates as they occur
statuses = ["received", "processing", "shipped", "delivered"]
for status in statuses:
yield orders_pb2.OrderStatus(
order_id=request.order_id,
status=status,
timestamp="2026-05-18T10:00:00Z",
)
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
orders_pb2_grpc.add_OrderServiceServicer_to_server(OrderServicer(), server)
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()
if __name__ == "__main__":
serve()
And a client:
import grpc
import orders_pb2
import orders_pb2_grpc
with grpc.insecure_channel("localhost:50051") as channel:
stub = orders_pb2_grpc.OrderServiceStub(channel)
# Unary call
order = stub.GetOrder(orders_pb2.GetOrderRequest(order_id="ord-123"))
print(f"Order status: {order.status}")
# Server streaming
for update in stub.StreamOrderUpdates(orders_pb2.OrderStatusRequest(order_id="ord-123")):
print(f"Update: {update.status}")
gRPC vs REST vs GraphQL
| Criteria | REST | GraphQL | gRPC |
|---|---|---|---|
| Browser support | Native | Native | Needs proxy |
| Schema enforcement | Optional (OpenAPI) | Yes | Yes (protobuf) |
| Streaming | Via WebSocket/SSE | Subscriptions | First-class |
| Payload size | JSON (larger) | JSON (larger) | Binary (smaller) |
| Code generation | Via OpenAPI tools | Via schema tools | Built-in |
| Learning curve | Low | Medium | Medium-High |
| Debugging | Easy (JSON) | Moderate | Harder (binary) |
| Ecosystem maturity | Highest | High | High |
The tl;dr: use REST for public APIs and browser clients. Use GraphQL when the client needs flexible querying control over what fields to fetch. Use gRPC for service-to-service communication where efficiency and strict contracts matter.
Schema Evolution
Protocol Buffers handles backward compatibility through field numbers, not names. Adding a new field with a new number doesn’t break existing clients; they ignore unknown fields. Removing a field is safe as long as you never reuse that field number. Changing a field type is usually not safe.
message Order {
string id = 1;
string customer_id = 2;
double total = 3;
string status = 4;
// New in v2, existing clients safely ignore this
string tracking_number = 5;
// NEVER reuse field number 3 if you deprecate `total`
}
Mark fields you’re deprecating as reserved to prevent accidental reuse:
message Order {
reserved 3; // was `total`, now removed
reserved "total"; // prevent reuse by name too
string id = 1;
string customer_id = 2;
string status = 4;
int64 amount_cents = 6; // replaced `total` with a new field and number
}
This schema evolution model is more explicit than REST’s “just add a new JSON field and hope clients handle it gracefully.”
If your next project involves services talking to services (not browsers talking to services), gRPC is worth evaluating properly rather than defaulting to REST out of habit.
Sponsored
More from this category
More from Web Development
CSS Anchor Positioning: Tooltips and Popovers Without JavaScript
k6 Load Testing: Performance Testing Your APIs Before Users Find the Problems
Tauri 2.0: Build Desktop and Mobile Apps with Web Tech, Without the Electron Bloat
Sponsored
The dispatch
Working notes from
the studio.
A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored