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.
The following proto file defines a complete client service with examples of all four RPC patterns: unary (simple request/response), server streaming, client streaming, and bidirectional streaming.
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. You'll also notice the enum starts with UNSPECIFIED at zero, which is a proto3 best practice for handling default values.
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.
When creating gRPC clients, you should reuse channels to take advantage of connection multiplexing. The following factory demonstrates proper channel configuration with keepalive settings that maintain healthy connections.
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. This implementation uses Laravel's cursor pagination to stream results efficiently without loading the entire dataset into memory.
// 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 like chat or collaborative editing. Both client and server can send messages independently, creating a full-duplex communication channel.
// 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.
Proper error handling is essential for building robust gRPC services. The following example shows how to throw appropriate status exceptions on the server and handle them gracefully on the client.
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,
};
}
The match expression provides clean handling for different error types, allowing you to take appropriate action for each case.
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.
These interceptors demonstrate common patterns you'll implement in production services: logging with timing metrics and token-based authentication.
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);
}
}
You can chain multiple interceptors to compose complex middleware pipelines, with each interceptor handling a specific concern.
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.
Here's how to evolve a message type safely over multiple versions. Notice the use of reserved field numbers and names to prevent accidental reuse.
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.
For large responses, prefer streaming over returning massive single messages. Always set deadlines on client calls to prevent requests from hanging indefinitely.
// 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.