6 min read
On this page

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:

  1. Receives REST requests (JSON over HTTP)
  2. Translates them to gRPC calls (protobuf over HTTP/2)
  3. Forwards them to the gRPC server
  4. 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 .proto files 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 .proto files.
  • 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 .proto files 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 .proto files 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 .proto definitions in a central repository.