gRPC

A high-performance RPC framework using Protocol Buffers for serialisation. Strongly typed, auto-generated clients, 4 streaming modes. Preferred for internal service-to-service communication at scale.

A high-performance RPC framework using Protocol Buffers for serialisation. Strongly typed, auto-generated clients, 4 streaming modes. Preferred for internal service-to-service communication at scale.


gRPC vs REST vs GraphQL

RESTGraphQLgRPC
ProtocolHTTP/1.1HTTP/1.1HTTP/2
SerialisationJSONJSONProtobuf (binary)
SchemaOpenAPI (optional)SDL (required)Proto (required)
Type safetyWeakStrongStrong
StreamingNoSubscriptions4 modes
Browser supportNativeNativeVia grpc-web
Best forPublic APIsBFF/complex clientsInternal microservices

Proto Definition

// proto/product.proto
syntax = "proto3";

package product.v1;

option go_package = "github.com/myorg/myapp/gen/product/v1";

import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";

service ProductService {
  // Unary RPC
  rpc GetProduct (GetProductRequest) returns (Product);
  rpc CreateProduct (CreateProductRequest) returns (Product);
  rpc DeleteProduct (DeleteProductRequest) returns (google.protobuf.Empty);

  // Server streaming — send product list incrementally
  rpc ListProducts (ListProductsRequest) returns (stream Product);

  // Client streaming — batch create
  rpc BatchCreateProducts (stream CreateProductRequest) returns (BatchCreateResponse);

  // Bidirectional streaming — real-time price updates
  rpc WatchPrices (stream WatchRequest) returns (stream PriceUpdate);
}

message Product {
  string id = 1;
  string name = 2;
  double price = 3;
  string category = 4;
  bool in_stock = 5;
  google.protobuf.Timestamp created_at = 6;
  repeated string tags = 7;
}

message GetProductRequest {
  string id = 1;
}

message CreateProductRequest {
  string name = 1;
  double price = 2;
  string category_id = 3;
  repeated string tags = 4;
}

message ListProductsRequest {
  optional string category = 1;
  int32 page_size = 2;
  string page_token = 3;
}

message WatchRequest {
  repeated string product_ids = 1;
}

message PriceUpdate {
  string product_id = 1;
  double new_price = 2;
  google.protobuf.Timestamp updated_at = 3;
}

message BatchCreateResponse {
  repeated Product products = 1;
  int32 created_count = 2;
}

message DeleteProductRequest {
  string id = 1;
}

Python Server

# app/grpc/product_service.py
import grpc
from concurrent import futures
from generated import product_pb2, product_pb2_grpc
from app.db import get_db
from app.models import Product

class ProductServiceServicer(product_pb2_grpc.ProductServiceServicer):

    def GetProduct(self, request, context):
        db = get_db()
        product = db.query(Product).get(request.id)
        if not product:
            context.set_code(grpc.StatusCode.NOT_FOUND)
            context.set_details(f"Product {request.id} not found")
            return product_pb2.Product()
        return product_pb2.Product(
            id=str(product.id),
            name=product.name,
            price=float(product.price),
            category=product.category,
            in_stock=product.in_stock,
        )

    def ListProducts(self, request, context):
        """Server streaming — yield products one at a time."""
        db = get_db()
        query = db.query(Product)
        if request.category:
            query = query.filter(Product.category == request.category)
        for product in query.yield_per(100):  # stream in batches
            yield product_pb2.Product(
                id=str(product.id),
                name=product.name,
                price=float(product.price),
            )

    def WatchPrices(self, request_iterator, context):
        """Bidirectional streaming — receive watch requests, send price updates."""
        watched_ids = set()
        for req in request_iterator:
            watched_ids.update(req.product_ids)
            # Fetch and yield current prices
            for product_id in req.product_ids:
                price = get_current_price(product_id)
                yield product_pb2.PriceUpdate(product_id=product_id, new_price=price)

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    product_pb2_grpc.add_ProductServiceServicer_to_server(
        ProductServiceServicer(), server
    )
    server.add_insecure_port("[::]:50051")
    server.start()
    server.wait_for_termination()

Python Client

# app/clients/product_client.py
import grpc
from generated import product_pb2, product_pb2_grpc
from grpc import ssl_channel_credentials

def get_channel():
    creds = ssl_channel_credentials()
    return grpc.secure_channel("products.internal:443", creds)

class ProductClient:
    def __init__(self):
        self.stub = product_pb2_grpc.ProductServiceStub(get_channel())

    def get_product(self, product_id: str) -> product_pb2.Product:
        try:
            return self.stub.GetProduct(
                product_pb2.GetProductRequest(id=product_id),
                timeout=5.0,
                metadata=[("x-request-id", generate_request_id())],
            )
        except grpc.RpcError as e:
            if e.code() == grpc.StatusCode.NOT_FOUND:
                return None
            raise

    def list_products_streaming(self, category: str = None):
        """Server streaming — iterate over yielded products."""
        request = product_pb2.ListProductsRequest(category=category or "")
        for product in self.stub.ListProducts(request):
            yield product

Interceptors (Middleware)

# app/grpc/interceptors.py
import time
import grpc

class LoggingInterceptor(grpc.ServerInterceptor):
    def intercept_service(self, continuation, handler_call_details):
        start = time.time()
        response = continuation(handler_call_details)
        duration_ms = (time.time() - start) * 1000
        print(f"gRPC {handler_call_details.method} {duration_ms:.1f}ms")
        return response

class AuthInterceptor(grpc.ServerInterceptor):
    def intercept_service(self, continuation, handler_call_details):
        metadata = dict(handler_call_details.invocation_metadata)
        token = metadata.get("authorization", "").replace("Bearer ", "")
        if not verify_jwt(token):
            def abort(request, context):
                context.set_code(grpc.StatusCode.UNAUTHENTICATED)
                context.set_details("Invalid token")
            return grpc.unary_unary_rpc_method_handler(abort)
        return continuation(handler_call_details)

Health Checking and Reflection

from grpc_health.v1 import health, health_pb2_grpc
from grpc_reflection.v1alpha import reflection, reflection_pb2

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))

    # Service
    product_pb2_grpc.add_ProductServiceServicer_to_server(ProductServiceServicer(), server)

    # Health check (standard protocol — Kubernetes probes)
    health_servicer = health.HealthServicer()
    health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)
    health_servicer.set("", health_pb2.HealthCheckResponse.SERVING)

    # Reflection (allows grpcurl to discover services)
    SERVICE_NAMES = (
        product_pb2.DESCRIPTOR.services_by_name["ProductService"].full_name,
        reflection.SERVICE_NAME,
    )
    reflection.enable_server_reflection(SERVICE_NAMES, server)

    server.add_insecure_port("[::]:50051")
    server.start()
# grpcurl — CLI for gRPC (like curl for REST)
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext -d '{"id": "abc123"}' localhost:50051 product.v1.ProductService/GetProduct

Common Failure Cases

Missing timeout on client call causes cascading hang Why: without a deadline, a slow or unresponsive server keeps the client's goroutine/thread blocked indefinitely; under load, connection pool exhaustion spreads to other callers. Detect: p99 latency climbs to the connection pool timeout (often 30–60s) rather than a service-level SLA; grpc.StatusCode.DEADLINE_EXCEEDED never appears in logs. Fix: always pass timeout= on every stub call; set a default at the channel level and tighten it per RPC where the SLA is tighter.

Proto field number reuse after deletion Why: deleting a field and reusing its number in a new field causes clients and servers compiled at different proto versions to silently misinterpret the wire format. Detect: after a proto change, clients receive garbled or zero-valued fields that should have valid data. Fix: never reuse field numbers; mark deleted fields with reserved so the compiler rejects future reuse.

Unhandled grpc.RpcError propagating as a 500 Why: client code that calls stub.GetProduct(...) without catching grpc.RpcError lets gRPC transport errors surface as unhandled exceptions, losing the status code context. Detect: logs show grpc.RpcError tracebacks without status code or details; callers cannot distinguish NOT_FOUND from UNAVAILABLE. Fix: wrap every stub call in a try/except grpc.RpcError block and map gRPC status codes to application-level exceptions or HTTP status codes.

Insecure port (add_insecure_port) used in production Why: the server example uses add_insecure_port for simplicity; if carried to production, all traffic between services is unencrypted and unauthenticated. Detect: grpcurl -plaintext connects without TLS; traffic is visible in a network capture. Fix: use add_secure_port with ssl_channel_credentials() on the server and secure_channel on the client; enforce mTLS in a service mesh for zero-trust networking.

Connections

se-hub · cs-fundamentals/graphql-se · cs-fundamentals/microservices-patterns · cs-fundamentals/distributed-systems · cs-fundamentals/api-design · cloud/service-mesh · java/grpc

Open Questions

  • What are the most common misapplications of this concept in production codebases?
  • When should you explicitly choose not to use this pattern or technique?