REST APIs are everywhere, but they have inherent limitations for microservice communication:
gRPC solves all of these problems.
Unlike REST which is strictly request-response, gRPC supports four communication modes. Here is how they look:
sequenceDiagram
participant C as Client
participant S as Server
rect rgb(240, 248, 255)
Note over C,S: 1. Unary RPC (Simple Request-Response)
C->>S: Request (Protobuf)
S-->>C: Response (Protobuf)
end
rect rgb(255, 248, 240)
Note over C,S: 2. Server Streaming
C->>S: Request
loop Stream
S-->>C: Data Chunk 1
S-->>C: Data Chunk 2
end
end
Everything starts with a .proto file:
syntax = "proto3";
package userservice;
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
rpc CreateUser (CreateUserRequest) returns (User);
}
message User {
string id = 1;
string name = 2;
string email = 3;
int64 created_at = 4;
}
message GetUserRequest {
string id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}With the proto file defined, code generation gives you type-safe server stubs:
type server struct {
pb.UnimplementedUserServiceServer
}
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user, err := s.db.FindByID(ctx, req.GetId())
if err != nil {
return nil, status.Errorf(codes.NotFound, "user not found: %v", err)
}
return &pb.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
}, nil
}
}, nil
}One major "gotcha" with gRPC is load balancing. Because gRPC uses persistent HTTP/2 connections, standard L4 load balancers (sticky TCP) will send all requests from one client to a single server instance. You need an L7 load balancer (like Envoy or Nginx) to distribute requests (streams) individually.
graph TD
subgraph L4LB [L4 LB (Bottleneck)]
C1[Client] -->|Single TCP Conn| L4[AWS NLB / HAProxy TCP]
L4 -->|Stickies to one node| S1[Server 1]
S2["Server 2 (Idle)"]
end
subgraph L7LB [L7 LB (Solution)]
C2[Client] -->|HTTP/2 Streams| L7[Envoy / Nginx]
L7 -->|RPC 1| S3[Server 1]
L7 -->|RPC 2| S4[Server 2]
end
style L4 fill:#ffebee,stroke:#c62828
style L7 fill:#e8f5e9,stroke:#2e7d32
Stop using HTTP 500 for everything. gRPC has a rich set of canonical status codes:
| gRPC Code | HTTP Mapping | Description |
|---|---|---|
OK (0) |
200 | Success |
INVALID_ARGUMENT (3) |
400 | Validation failed |
NOT_FOUND (5) |
404 | Resource missing |
PERMISSION_DENIED (7) |
403 | Authz failed |
UNAUTHENTICATED (16) |
401 | Authn failed |
RESOURCE_EXHAUSTED (8) |
429 | Rate limit hit |
DEADLINE_EXCEEDED (4) |
504 | Timeout |
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
// Load the .proto file
const packageDefinition = protoLoader.loadSync('user.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const userProto = grpc.loadPackageDefinition(packageDefinition).userservice;
// Create the client
const client = new userProto.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
// Make an RPC call
client.GetUser({ id: '123' }, (err, response) => {
if (err) {
if (err.code === grpc.status.NOT_FOUND) {
console.error("User not found!");
} else {
console.error("RPC Error:", err);
}
} else {
console.log("User:", response);
}
});| Metric | REST/JSON | gRPC/Protobuf |
|---|---|---|
| Serialization | ~3ms | ~0.2ms |
| Payload Size | 1.0x | 0.3x |
| Latency (P50) | 5ms | 1.2ms |
| Throughput | 15k rps | 45k rps |
gRPC is ideal for:
Stick with REST for:
gRPC with Protocol Buffers provides a significant performance improvement over REST/JSON for internal microservice communication. The strong typing, code generation, and streaming support make it a compelling choice for modern backend architectures.
Core Engineering Group
Building robust, scalable applications with modern best practices.
Understanding the fundamental differences between HTTP/1.1 and HTTP/2 with visual diagrams and practical examples
A deep dive into how PostgreSQL's query planner works, from parsing SQL to generating optimal execution plans. Understand EXPLAIN output like never before.
How to run schema migrations on billion-row tables without any downtime. Covers expand-contract pattern, online DDL tools, and rollback strategies.