gRPC for Microservices Communication

Reverend Philip Jan 12, 2026 7 min read

Build efficient service-to-service communication with gRPC. Learn Protocol Buffers, streaming, and error handling.

gRPC has become the standard for high-performance microservices communication. Built on HTTP/2 and Protocol Buffers, it offers significant advantages over REST for service-to-service calls: strongly typed contracts, efficient binary serialization, bidirectional streaming, and excellent tooling. Understanding when and how to use gRPC helps build more efficient distributed systems.

The choice between gRPC and REST isn't absolute. REST excels for public APIs, browser clients, and simple integrations. gRPC excels for internal service communication, performance-critical paths, and scenarios requiring streaming. Many systems use both: REST at the edge for external clients, gRPC internally for service-to-service calls.

Protocol Buffers and Contracts

gRPC uses Protocol Buffers (protobuf) for defining service contracts and serializing messages. A .proto file defines your API schema, and the protobuf compiler generates client and server code in your target language.

This contract-first approach has profound implications. The schema is the source of truth. Both client and server implementations derive from the same definition. Type safety is enforced at compile time, not discovered at runtime.

syntax = "proto3";

package clientportal;

option php_namespace = "App\\Grpc";
option php_metadata_namespace = "App\\Grpc\\Metadata";

service ClientService {
  // Unary RPC - simple request/response
  rpc GetClient(GetClientRequest) returns (Client);

  // Server streaming - client requests, server streams responses
  rpc ListClients(ListClientsRequest) returns (stream Client);

  // Client streaming - client streams requests, server responds once
  rpc UploadDocuments(stream Document) returns (UploadResponse);

  // Bidirectional streaming - both sides stream
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

message Client {
  int64 id = 1;
  string company_name = 2;
  string email = 3;
  ClientStatus status = 4;
  google.protobuf.Timestamp created_at = 5;

  repeated Project projects = 6;
}

message Project {
  int64 id = 1;
  string name = 2;
  ProjectStatus status = 3;
}

enum ClientStatus {
  CLIENT_STATUS_UNSPECIFIED = 0;
  CLIENT_STATUS_ACTIVE = 1;
  CLIENT_STATUS_INACTIVE = 2;
  CLIENT_STATUS_PENDING = 3;
}

message GetClientRequest {
  int64 id = 1;
}

message ListClientsRequest {
  int32 page_size = 1;
  string page_token = 2;
  ClientStatus status_filter = 3;
}

Field numbers in protobuf are crucial for wire format compatibility. Once assigned, they shouldn't change. Adding new fields is safe; removing fields should mark them as reserved to prevent reuse.

HTTP/2 Benefits

gRPC runs on HTTP/2, gaining its performance benefits automatically. Multiplexing allows multiple requests over a single connection without head-of-line blocking. Header compression reduces overhead for repeated calls. Binary framing is more efficient than HTTP/1.1's text-based protocol.

Connection management differs from REST. Instead of opening connections per request or using a simple connection pool, gRPC maintains long-lived connections with multiplexed streams. This reduces latency by eliminating connection setup overhead.

class GrpcClientFactory
{
    private array $channels = [];

    public function getClientServiceClient(string $host): ClientServiceClient
    {
        // Reuse channel for connection multiplexing
        if (!isset($this->channels[$host])) {
            $this->channels[$host] = new Channel($host, [
                'credentials' => ChannelCredentials::createSsl(),
                'grpc.keepalive_time_ms' => 30000,
                'grpc.keepalive_timeout_ms' => 10000,
                'grpc.http2.min_time_between_pings_ms' => 10000,
            ]);
        }

        return new ClientServiceClient($host, [
            'credentials' => ChannelCredentials::createSsl(),
        ], $this->channels[$host]);
    }
}

Streaming Patterns

gRPC's streaming capabilities enable patterns impossible with traditional REST. Server streaming sends multiple responses to a single request, ideal for large result sets or real-time updates. Client streaming sends multiple requests before receiving a response, useful for uploads or batch operations. Bidirectional streaming enables real-time communication in both directions.

Server streaming replaces polling for real-time updates:

// Server implementation
class ClientServiceImpl extends ClientServiceInterface
{
    public function ListClients(
        ListClientsRequest $request,
        ServerCallWriter $writer
    ): void {
        $query = Client::query();

        if ($request->getStatusFilter() !== ClientStatus::CLIENT_STATUS_UNSPECIFIED) {
            $query->where('status', $this->mapStatus($request->getStatusFilter()));
        }

        // Stream results instead of loading all into memory
        $query->cursor()->each(function ($client) use ($writer) {
            $protoClient = $this->mapToProto($client);
            $writer->write($protoClient);
        });

        $writer->finish();
    }
}

// Client usage
$stream = $client->ListClients($request);

foreach ($stream->responses() as $clientProto) {
    // Process each client as it arrives
    $this->processClient($clientProto);
}

Bidirectional streaming enables real-time applications:

// Chat implementation with bidirectional streaming
class ChatServiceImpl extends ChatServiceInterface
{
    public function Chat(ServerCallReaderWriter $stream): void
    {
        while ($message = $stream->read()) {
            // Process incoming message
            $response = $this->processMessage($message);

            // Send response (or broadcast to other streams)
            $stream->write($response);
        }

        $stream->finish();
    }
}

Error Handling

gRPC uses status codes distinct from HTTP status codes. These codes are more specific to RPC semantics: NOT_FOUND, ALREADY_EXISTS, PERMISSION_DENIED, RESOURCE_EXHAUSTED, etc. Rich error details can accompany the status code.

class ClientServiceImpl extends ClientServiceInterface
{
    public function GetClient(
        GetClientRequest $request,
        ServerContext $context
    ): Client {
        $client = Client::find($request->getId());

        if (!$client) {
            throw new StatusException(
                Status::notFound("Client {$request->getId()} not found"),
                [
                    'resource_type' => 'Client',
                    'resource_id' => (string) $request->getId(),
                ]
            );
        }

        if (!$this->canAccess($context->getUser(), $client)) {
            throw new StatusException(
                Status::permissionDenied("Access denied to client {$request->getId()}")
            );
        }

        return $this->mapToProto($client);
    }
}

// Client error handling
try {
    $client = $stub->GetClient($request);
} catch (StatusException $e) {
    match ($e->getStatus()->getCode()) {
        Code::NOT_FOUND => $this->handleNotFound($e),
        Code::PERMISSION_DENIED => $this->handlePermissionDenied($e),
        Code::UNAVAILABLE => $this->handleUnavailable($e),
        default => throw $e,
    };
}

Interceptors and Middleware

gRPC interceptors provide middleware functionality for cross-cutting concerns: authentication, logging, metrics, tracing. They wrap RPC calls on both client and server sides.

class LoggingInterceptor implements ServerInterceptor
{
    public function intercept(
        ServerCall $call,
        callable $handler
    ): mixed {
        $startTime = microtime(true);
        $method = $call->getMethod();

        Log::info("gRPC request started", [
            'method' => $method,
            'metadata' => $call->getMetadata(),
        ]);

        try {
            $result = $handler($call);

            $duration = microtime(true) - $startTime;
            Log::info("gRPC request completed", [
                'method' => $method,
                'duration_ms' => $duration * 1000,
                'status' => 'OK',
            ]);

            return $result;

        } catch (StatusException $e) {
            $duration = microtime(true) - $startTime;
            Log::error("gRPC request failed", [
                'method' => $method,
                'duration_ms' => $duration * 1000,
                'status' => $e->getStatus()->getCode(),
                'message' => $e->getMessage(),
            ]);

            throw $e;
        }
    }
}

class AuthInterceptor implements ServerInterceptor
{
    public function intercept(
        ServerCall $call,
        callable $handler
    ): mixed {
        $token = $call->getMetadata()['authorization'][0] ?? null;

        if (!$token) {
            throw new StatusException(
                Status::unauthenticated('Missing authorization token')
            );
        }

        $user = $this->validateToken($token);
        $call->getContext()->set('user', $user);

        return $handler($call);
    }
}

Schema Evolution

Protocol buffers support backward and forward compatible schema evolution when following certain rules. New fields can be added with new field numbers. Old fields can be removed but their numbers should be reserved. Field types can only change in compatible ways.

message Client {
  int64 id = 1;
  string company_name = 2;
  string email = 3;
  ClientStatus status = 4;

  // Added in v2 - old clients ignore it, new clients use it
  string phone = 5;

  // Removed in v3 - reserve the number to prevent reuse
  reserved 6;
  reserved "old_field_name";

  // Added in v3
  Address address = 7;
}

Services can evolve by adding new RPC methods. Removing methods breaks clients. Changing method signatures breaks clients. Version your services appropriately when breaking changes are necessary.

Performance Tuning

gRPC's binary protocol is inherently faster than JSON-based REST, but optimal performance requires tuning. Message size affects serialization time; keep messages focused and use streaming for large data. Connection pooling and keepalives reduce connection overhead.

// Optimize for large responses with streaming
service ReportService {
  // Bad: Single large response
  rpc GetFullReport(ReportRequest) returns (FullReport);

  // Better: Stream report sections
  rpc GetReport(ReportRequest) returns (stream ReportSection);
}

// Client-side deadline ensures requests don't hang forever
$options = [
    'deadline' => microtime(true) + 30.0, // 30 second timeout
];

$response = $stub->GetClient($request, $options);

When to Choose gRPC

gRPC fits best for internal service-to-service communication where both ends are under your control. The strong typing, efficient serialization, and streaming capabilities provide clear benefits for microservices architectures.

REST remains better for public APIs, browser clients (gRPC-Web adds complexity), and simple integrations where ease of use outweighs performance. Many APIs benefit from REST's simplicity and broad tooling support.

Consider gRPC when you have high-volume service-to-service calls, need streaming capabilities, want compile-time type safety across services, or are building performance-critical paths. Consider REST when simplicity matters more than performance, clients are browsers or varied external systems, or your team lacks gRPC experience.

Conclusion

gRPC provides high-performance, strongly-typed communication for microservices. Protocol Buffers enforce contracts at compile time, catching integration issues early. HTTP/2 multiplexing and binary serialization deliver performance improvements over REST. Streaming enables real-time patterns impossible with request-response APIs.

The investment in gRPC pays off in large-scale microservices architectures where service-to-service communication is frequent and performance matters. The learning curve and tooling requirements are real costs that must be weighed against benefits for your specific situation.

Share this article

Related Articles

Distributed Locking Patterns

Coordinate access to shared resources across services. Implement distributed locks with Redis, ZooKeeper, and databases.

Jan 16, 2026

Need help with your project?

Let's discuss how we can help you build reliable software.