gRPC in Practice
Choosing gRPC is the beginning, not the end. The real work is integrating it into your architecture: deciding where gRPC fits and where REST stays, handling cross-cutting concerns like authentication and observability, and building the operational tooling that makes gRPC debuggable in production.
This is a guide to the practical decisions teams face after they decide to use gRPC.
When gRPC Wins
Internal Microservices
gRPC's primary strength is service-to-service communication within a system you control. Both sides use generated code. Both sides benefit from type safety. The binary encoding reduces bandwidth between services that exchange millions of messages per day.
A typical microservice interaction:
// order-service calls inventory-service
service InventoryService {
rpc CheckAvailability (CheckAvailabilityRequest) returns (CheckAvailabilityResponse);
rpc ReserveItems (ReserveItemsRequest) returns (ReserveItemsResponse);
rpc ReleaseReservation (ReleaseReservationRequest) returns (ReleaseReservationResponse);
}
message CheckAvailabilityRequest {
repeated ItemQuantity items = 1;
}
message ItemQuantity {
string product_id = 1;
int32 requested_quantity = 2;
}
message CheckAvailabilityResponse {
repeated ItemAvailability items = 1;
}
message ItemAvailability {
string product_id = 1;
int32 available_quantity = 2;
bool in_stock = 3;
}
Both services are in the same infrastructure, maintained by teams in the same organization. They share .proto files through a common repository or package registry. Changes go through code review and compatibility checks.
High-Throughput Systems
When services exchange hundreds of thousands of messages per second, the difference between JSON and protobuf serialization matters. Protobuf's binary encoding is 3-10x smaller and 2-10x faster to serialize. Over HTTP/2 with multiplexed streams, gRPC can sustain significantly higher throughput on the same hardware.
Google, Netflix, and Uber use gRPC for their highest-throughput internal services. At Google's scale, the efficiency difference between JSON and protobuf translates to measurable infrastructure cost savings.
Streaming Use Cases
gRPC's streaming support is native, not bolted on. Server streaming for real-time updates, client streaming for batch uploads, and bidirectional streaming for interactive protocols are first-class features:
service MonitoringService {
// Server streams metrics as they're collected
rpc StreamMetrics (StreamMetricsRequest) returns (stream MetricPoint);
// Client streams log entries in batches
rpc IngestLogs (stream LogEntry) returns (IngestSummary);
// Bidirectional: debug session with interactive commands
rpc DebugSession (stream DebugCommand) returns (stream DebugResponse);
}
Achieving the same with REST requires WebSockets, Server-Sent Events, or polling — all of which are separate protocols with separate client libraries and separate operational concerns.
When REST Wins
Public APIs
External developers expect to call your API with curl, Postman, or a basic HTTP client in any language. gRPC requires protobuf definitions, code generation, and a gRPC client library. The barrier to entry is too high for a public API.
# REST: any developer can try this in 30 seconds
curl -X POST https://api.stripe.com/v1/charges \
-u sk_test_abc: \
-d amount=2000 \
-d currency=usd
# gRPC: requires protoc, generated stubs, a gRPC client setup
# No equivalent one-liner
Stripe, GitHub, Twilio, and every other developer-facing API uses REST because simplicity and accessibility trump performance for this audience.
Browser-First Applications
Browsers cannot make native gRPC calls. gRPC-Web exists but requires a proxy (Envoy), does not support all streaming modes, and adds infrastructure complexity. If your primary consumer is a web application, REST or GraphQL is the path of least resistance.
Simplicity
If your system has five services and handles modest traffic, gRPC's benefits do not outweigh its complexity. REST with JSON is simpler to debug (human-readable payloads), simpler to monitor (standard HTTP tooling), and simpler to operate (no protobuf compilation step, no special load balancing).
The gRPC-REST Bridge
Most organizations that adopt gRPC still need REST endpoints — for public APIs, for legacy clients, for admin tools that use browsers. The gRPC-Gateway project solves this by generating a REST reverse proxy from your .proto files.
import "google/api/annotations.proto";
service OrderService {
rpc GetOrder (GetOrderRequest) returns (Order) {
option (google.api.http) = {
get: "/v1/orders/{order_id}"
};
}
rpc CreateOrder (CreateOrderRequest) returns (Order) {
option (google.api.http) = {
post: "/v1/orders"
body: "*"
};
}
rpc ListOrders (ListOrdersRequest) returns (ListOrdersResponse) {
option (google.api.http) = {
get: "/v1/orders"
};
}
}
The google.api.http annotations map gRPC methods to REST endpoints. The gRPC-Gateway generator produces a Go reverse proxy that:
- Receives REST requests (JSON over HTTP)
- Translates them to gRPC calls (protobuf over HTTP/2)
- Forwards them to the gRPC server
- Translates the gRPC response back to REST
REST client --> gRPC-Gateway --> gRPC server
(HTTP/JSON) (HTTP/2/protobuf)
gRPC client ----------------------> gRPC server
(HTTP/2/protobuf)
One implementation, two protocols. Internal services use gRPC directly for performance. External clients use REST through the gateway. Google uses this pattern for their Cloud APIs — the same service handles both gRPC and REST requests.
Interceptors
Interceptors are gRPC's middleware. They wrap RPC calls to add cross-cutting behavior: logging, authentication, tracing, metrics, retries. They are the equivalent of HTTP middleware in REST frameworks.
Common Interceptor Patterns
Logging — log every RPC call with method name, duration, and status code:
[INFO] OrderService.GetOrder | duration=12ms | status=OK | request_id=abc123
[WARN] OrderService.CreateOrder | duration=250ms | status=INVALID_ARGUMENT | request_id=def456
[ERROR] OrderService.CancelOrder | duration=5002ms | status=DEADLINE_EXCEEDED | request_id=ghi789
Authentication — extract a token from metadata, validate it, and attach the user identity to the context:
1. Extract "authorization" from metadata
2. Validate the JWT/token
3. Attach user_id and permissions to the RPC context
4. If invalid, return UNAUTHENTICATED
Tracing — propagate trace context (OpenTelemetry) across service boundaries. The interceptor reads the trace parent from incoming metadata and creates a child span for the RPC:
Incoming metadata: traceparent=00-abc-def-01
Create span: OrderService.GetOrder (parent=def)
Outgoing call to InventoryService:
Attach metadata: traceparent=00-abc-ghi-01
Metrics — record RPC count, latency, and status code distribution. Export to Prometheus, Datadog, or your monitoring system:
grpc_server_handled_total{method="GetOrder", code="OK"} 15234
grpc_server_handled_total{method="GetOrder", code="NOT_FOUND"} 42
grpc_server_handling_seconds_bucket{method="GetOrder", le="0.01"} 14890
grpc_server_handling_seconds_bucket{method="GetOrder", le="0.1"} 15200
Retries — automatically retry failed RPCs with status UNAVAILABLE using exponential backoff. gRPC clients have built-in retry support via service configuration:
{
"methodConfig": [
{
"name": [{"service": "orders.v1.OrderService"}],
"retryPolicy": {
"maxAttempts": 3,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2,
"retryableStatusCodes": ["UNAVAILABLE"]
}
}
]
}
Interceptors chain. A typical server interceptor chain: logging -> metrics -> tracing -> authentication -> the actual handler.
Health Checks
gRPC defines a standard health checking protocol:
service Health {
rpc Check (HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch (HealthCheckRequest) returns (stream HealthCheckResponse);
}
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3;
}
ServingStatus status = 1;
}
Implement this on every gRPC server. Kubernetes liveness and readiness probes can use grpc_health_probe to check the health endpoint. Load balancers use it to route traffic away from unhealthy instances.
The Watch RPC streams health status changes, so load balancers can react immediately rather than polling.
Reflection
gRPC server reflection lets clients discover available services and methods at runtime, without having the .proto files. It is the gRPC equivalent of REST's OpenAPI documentation.
Enable reflection on development and staging servers:
$ grpcurl -plaintext localhost:50051 list
orders.v1.OrderService
grpc.health.v1.Health
grpc.reflection.v1alpha.ServerReflection
$ grpcurl -plaintext localhost:50051 describe orders.v1.OrderService
orders.v1.OrderService is a service:
rpc CreateOrder (CreateOrderRequest) returns (Order);
rpc GetOrder (GetOrderRequest) returns (Order);
rpc ListOrders (ListOrdersRequest) returns (ListOrdersResponse);
$ grpcurl -plaintext -d '{"order_id": "ord_123"}' \
localhost:50051 orders.v1.OrderService/GetOrder
{
"orderId": "ord_123",
"status": "CONFIRMED",
"totalCents": "5000"
}
grpcurl is the gRPC equivalent of curl. With reflection enabled, developers can explore and test services without compiling proto definitions. Disable reflection in production if your service is sensitive.
The Migration Path: REST Externally, gRPC Internally
The most common adoption pattern for established systems:
Phase 1: New Internal Services Use gRPC
New microservices communicate via gRPC. Existing services continue using REST. The .proto files live in a shared repository.
Phase 2: Add the gRPC-REST Gateway
For services that need both internal (gRPC) and external (REST) access, add gRPC-Gateway annotations. The same service implementation serves both protocols.
Phase 3: Migrate Internal REST to gRPC
Gradually convert internal REST calls to gRPC. Start with the highest-traffic routes where the performance benefit is largest. Leave low-traffic admin endpoints on REST if the migration cost is not justified.
Phase 4: Stabilize
The architecture settles into a pattern:
External clients --> REST (via gRPC-Gateway) --> gRPC services
Internal clients --------------------------------> gRPC services
Admin/debug tools --> REST or gRPC with reflection
This is not a one-month project. Teams that successfully adopt gRPC typically spend 6-12 months reaching Phase 4.
Common Pitfalls
- All-or-nothing adoption — trying to convert every service to gRPC at once. Start with new services or high-traffic internal routes. Gradual migration is safer.
- No proto repository — scattering
.protofiles across service repositories. A shared proto repository (or package registry) ensures all services use compatible definitions. - Skipping the gateway — building separate REST endpoints alongside gRPC endpoints, maintaining two implementations of the same logic. The gRPC-Gateway generates the REST layer from the same
.protofiles. - Ignoring observability — gRPC is harder to observe than REST out of the box. Without interceptors for logging, metrics, and tracing, debugging production issues is painful.
- No reflection in development — requiring developers to find and compile
.protofiles before they can inspect a running service. Reflection makes development and debugging dramatically easier. - Forgetting health checks — deploying gRPC services without implementing the standard health protocol. Kubernetes probes, load balancers, and service meshes all depend on it.
Key Takeaways
- gRPC wins for internal microservices, high-throughput systems, and streaming use cases. REST wins for public APIs, browser-first applications, and simplicity.
- Use the gRPC-Gateway to serve both REST and gRPC from the same service implementation. Annotate
.protofiles with HTTP mappings and generate the REST proxy. - Interceptors handle cross-cutting concerns: logging, authentication, tracing, metrics, and retries. Chain them consistently across all services.
- Implement the standard health check protocol on every gRPC server. Enable reflection on development and staging environments.
- Migrate gradually: new internal services on gRPC, add the gateway for external access, convert high-traffic internal REST routes over time.
- The target architecture for most organizations: REST externally (via gateway), gRPC internally, with shared
.protodefinitions in a central repository.