gRPC
A high-performance, open-source universal RPC framework that uses Protocol Buffers for serialisation and HTTP/2 for transport.
gRPC Cheatsheet
A high-performance, open-source universal RPC framework that uses Protocol Buffers for serialisation and HTTP/2 for transport.
Overview
gRPC (gRPC Remote Procedure Calls) is a modern RPC framework developed by Google that enables efficient communication between distributed services. It supports multiple programming languages, bidirectional streaming, and provides built-in features for authentication, load balancing, and health checking.
graph TB
subgraph "gRPC Architecture"
Client[gRPC Client]
Server[gRPC Server]
Proto[Protocol Buffers<br/>Schema Definition]
Proto -->|Generate| ClientStub[Client Stub]
Proto -->|Generate| ServerStub[Server Stub]
Client --> ClientStub
ClientStub -->|HTTP/2| ServerStub
ServerStub --> Server
subgraph "Transport Layer"
HTTP2[HTTP/2 Protocol]
TLS[TLS Encryption]
end
end
style Proto fill:#e1f5fe
style HTTP2 fill:#fff3e0
style TLS fill:#f3e5f5
Protocol Buffers Schema Definition
Key Concepts
- Protocol Buffers (Protobuf): Language-neutral, platform-neutral serialisation mechanism
- Message Types: Define data structures with typed fields
- Field Numbers: Unique identifiers for each field (1-15 use 1 byte, 16-2047 use 2 bytes)
- Scalar Types: Basic types like int32, string, bool, bytes
- Composite Types: Nested messages, enums, maps, repeated fields
Common Patterns
// Basic syntax declaration
syntax = "proto3";
// Package declaration
package myservice.v1;
// Import other proto files
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
// Options for code generation
option go_package = "github.com/myorg/myservice/v1";
option java_package = "com.myorg.myservice.v1";
option java_multiple_files = true;
// Enum definition
enum Status {
STATUS_UNSPECIFIED = 0; // Always have 0 as default
STATUS_ACTIVE = 1;
STATUS_INACTIVE = 2;
}
// Message definition
message User {
// Scalar types
string id = 1;
string name = 2;
int32 age = 3;
bool is_verified = 4;
// Enum field
Status status = 5;
// Repeated field (list/array)
repeated string tags = 6;
// Nested message
Address address = 7;
// Map field
map<string, string> metadata = 8;
// Timestamp (well-known type)
google.protobuf.Timestamp created_at = 9;
// Optional field (explicit presence)
optional string nickname = 10;
// Oneof (mutually exclusive fields)
oneof contact {
string email = 11;
string phone = 12;
}
}
// Nested message definition
message Address {
string street = 1;
string city = 2;
string country = 3;
string postcode = 4;
}
Examples
Compiling Protocol Buffers:
# Install protoc compiler
# Ubuntu/Debian
apt-get install -y protobuf-compiler
# macOS
brew install protobuf
# Generate Go code
protoc --go_out=. --go-grpc_out=. proto/*.proto
# Generate Python code
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. proto/*.proto
# Generate multiple languages
protoc --go_out=. --go-grpc_out=. \
--java_out=. --grpc-java_out=. \
--python_out=. --grpc_python_out=. \
proto/*.proto
Service and Method Definitions
Key Concepts
- Service: Collection of RPC methods
- RPC Methods: Define request/response message types
- Metadata: Key-value pairs sent with requests (like HTTP headers)
- Deadlines: Time limits for RPC completion
- Cancellation: Ability to terminate RPCs early
Common Patterns
syntax = "proto3";
package userservice.v1;
import "google/protobuf/empty.proto";
// Service definition
service UserService {
// Unary RPC
rpc GetUser(GetUserRequest) returns (GetUserResponse);
// Server streaming RPC
rpc ListUsers(ListUsersRequest) returns (stream User);
// Client streaming RPC
rpc CreateUsers(stream CreateUserRequest) returns (CreateUsersResponse);
// Bidirectional streaming RPC
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
// Empty request/response
rpc Ping(google.protobuf.Empty) returns (google.protobuf.Empty);
}
// Request messages
message GetUserRequest {
string user_id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
string filter = 3;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
// Response messages
message GetUserResponse {
User user = 1;
}
message CreateUsersResponse {
int32 created_count = 1;
repeated string user_ids = 2;
}
message ChatMessage {
string user_id = 1;
string content = 2;
int64 timestamp = 3;
}
message User {
string id = 1;
string name = 2;
string email = 3;
}
Unary vs Streaming RPCs
Key Concepts
graph LR
subgraph "Unary RPC"
C1[Client] -->|Single Request| S1[Server]
S1 -->|Single Response| C1
end
subgraph "Server Streaming"
C2[Client] -->|Single Request| S2[Server]
S2 -->|Stream of Responses| C2
end
subgraph "Client Streaming"
C3[Client] -->|Stream of Requests| S3[Server]
S3 -->|Single Response| C3
end
subgraph "Bidirectional Streaming"
C4[Client] <-->|Stream Both Ways| S4[Server]
end
| Type | Use Case | Example |
|---|---|---|
| Unary | Simple request/response | Get user by ID |
| Server Streaming | Large data sets, real-time updates | List all users, stock prices |
| Client Streaming | Uploading data, aggregation | File upload, metrics collection |
| Bidirectional | Real-time communication | Chat, collaborative editing |
Examples
Go Server Implementation:
package main
import (
"context"
"io"
"log"
pb "github.com/myorg/myservice/proto"
)
type server struct {
pb.UnimplementedUserServiceServer
}
// Unary RPC
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
// Check for deadline
if deadline, ok := ctx.Deadline(); ok {
log.Printf("Request deadline: %v", deadline)
}
user := &pb.User{
Id: req.UserId,
Name: "John Doe",
Email: "john@example.com",
}
return &pb.GetUserResponse{User: user}, nil
}
// Server streaming RPC
func (s *server) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
users := []pb.User{
{Id: "1", Name: "Alice"},
{Id: "2", Name: "Bob"},
{Id: "3", Name: "Charlie"},
}
for _, user := range users {
if err := stream.Send(&user); err != nil {
return err
}
}
return nil
}
// Client streaming RPC
func (s *server) CreateUsers(stream pb.UserService_CreateUsersServer) error {
var userIds []string
for {
req, err := stream.Recv()
if err == io.EOF {
// Client finished sending
return stream.SendAndClose(&pb.CreateUsersResponse{
CreatedCount: int32(len(userIds)),
UserIds: userIds,
})
}
if err != nil {
return err
}
// Process each user
userId := createUser(req)
userIds = append(userIds, userId)
}
}
// Bidirectional streaming RPC
func (s *server) Chat(stream pb.UserService_ChatServer) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
// Echo back with modification
response := &pb.ChatMessage{
UserId: "server",
Content: "Received: " + msg.Content,
Timestamp: time.Now().Unix(),
}
if err := stream.Send(response); err != nil {
return err
}
}
}
Python Client Implementation:
import grpc
from proto import user_service_pb2 as pb
from proto import user_service_pb2_grpc as pb_grpc
def main():
# Create channel with options
channel = grpc.insecure_channel(
'localhost:50051',
options=[
('grpc.max_receive_message_length', 10 * 1024 * 1024),
]
)
stub = pb_grpc.UserServiceStub(channel)
# Unary RPC with timeout
try:
response = stub.GetUser(
pb.GetUserRequest(user_id="123"),
timeout=5.0
)
print(f"User: {response.user.name}")
except grpc.RpcError as e:
print(f"RPC failed: {e.code()}: {e.details()}")
# Server streaming RPC
for user in stub.ListUsers(pb.ListUsersRequest(page_size=10)):
print(f"User: {user.name}")
# Client streaming RPC
def user_generator():
users = [
pb.CreateUserRequest(name="Alice", email="alice@example.com"),
pb.CreateUserRequest(name="Bob", email="bob@example.com"),
]
for user in users:
yield user
response = stub.CreateUsers(user_generator())
print(f"Created {response.created_count} users")
# Bidirectional streaming RPC
def message_generator():
messages = ["Hello", "How are you?", "Goodbye"]
for content in messages:
yield pb.ChatMessage(user_id="client", content=content)
for response in stub.Chat(message_generator()):
print(f"Server: {response.content}")
if __name__ == '__main__':
main()
Authentication and Security
Key Concepts
- TLS/SSL: Transport layer encryption
- Channel Credentials: Secure the connection (TLS certificates)
- Call Credentials: Authenticate individual calls (tokens, JWT)
- Interceptors: Middleware for adding auth to all calls
- mTLS: Mutual TLS for client certificate authentication
sequenceDiagram
participant Client
participant Server
participant AuthService
Note over Client,Server: TLS Handshake
Client->>Server: ClientHello
Server->>Client: ServerHello + Certificate
Client->>Server: Verify Certificate
Note over Client,AuthService: Token Authentication
Client->>AuthService: Get JWT Token
AuthService->>Client: JWT Token
Note over Client,Server: Authenticated RPC
Client->>Server: RPC + JWT in Metadata
Server->>Server: Validate Token
Server->>Client: Response
Examples
Server with TLS (Go):
package main
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func main() {
// Load server certificate and key
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
if err != nil {
log.Fatalf("Failed to load key pair: %v", err)
}
// Create TLS credentials
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.NoClientCert, // or tls.RequireAndVerifyClientCert for mTLS
})
// Create server with TLS
server := grpc.NewServer(grpc.Creds(creds))
// Register services
pb.RegisterUserServiceServer(server, &userServer{})
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
log.Println("Server listening on :50051 with TLS")
if err := server.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
// Server with mTLS (mutual TLS)
func createMTLSServer() *grpc.Server {
// Load CA certificate
caCert, _ := ioutil.ReadFile("ca.crt")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
// Load server certificate
cert, _ := tls.LoadX509KeyPair("server.crt", "server.key")
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCertPool,
}
return grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))
}
Client with TLS and JWT (Go):
package main
import (
"context"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
)
// JWT credentials
type jwtCredentials struct {
token string
}
func (j jwtCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"authorization": "Bearer " + j.token,
}, nil
}
func (j jwtCredentials) RequireTransportSecurity() bool {
return true // Require TLS
}
func main() {
// Load TLS credentials
creds, err := credentials.NewClientTLSFromFile("ca.crt", "")
if err != nil {
log.Fatalf("Failed to load credentials: %v", err)
}
// Create JWT credentials
jwtCreds := jwtCredentials{token: "your-jwt-token"}
// Connect with both TLS and JWT
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithTransportCredentials(creds),
grpc.WithPerRPCCredentials(jwtCreds),
)
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
// Alternatively, add metadata per-call
ctx := metadata.AppendToOutgoingContext(
context.Background(),
"authorization", "Bearer your-token",
"x-custom-header", "custom-value",
)
stub := pb.NewUserServiceClient(conn)
response, err := stub.GetUser(ctx, &pb.GetUserRequest{UserId: "123"})
}
Server-side JWT Validation Interceptor (Go):
package main
import (
"context"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
func authInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// Skip auth for certain methods
if info.FullMethod == "/health.v1.Health/Check" {
return handler(ctx, req)
}
// Extract metadata
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
// Get authorization header
authHeader := md.Get("authorization")
if len(authHeader) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing authorization header")
}
// Extract and validate token
token := strings.TrimPrefix(authHeader[0], "Bearer ")
claims, err := validateJWT(token)
if err != nil {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
// Add claims to context
newCtx := context.WithValue(ctx, "claims", claims)
return handler(newCtx, req)
}
func main() {
server := grpc.NewServer(
grpc.UnaryInterceptor(authInterceptor),
grpc.StreamInterceptor(streamAuthInterceptor),
)
}
Load Balancing and Service Discovery
Key Concepts
- Client-side Load Balancing: Client chooses which server to call
- Proxy Load Balancing: External proxy (Envoy, nginx) distributes traffic
- Look-aside Load Balancing: Separate load balancer service
- Service Discovery: Dynamic server endpoint resolution
- Health Checking: Monitor server availability
graph TB
subgraph "Client-side LB"
C1[Client] --> R1[Resolver]
R1 --> LB1[Load Balancer]
LB1 --> S1A[Server A]
LB1 --> S1B[Server B]
LB1 --> S1C[Server C]
end
subgraph "Proxy LB"
C2[Client] --> P[Proxy<br/>Envoy/nginx]
P --> S2A[Server A]
P --> S2B[Server B]
P --> S2C[Server C]
end
subgraph "Service Mesh"
C3[Client] --> SC[Sidecar]
SC --> SD[Service<br/>Discovery]
SC --> S3A[Server A]
SC --> S3B[Server B]
end
Common Patterns
Client-side Load Balancing (Go):
API note (grpc-go):
grpc.Dial/grpc.DialContextandgrpc.WithInsecure()are deprecated. Current code usesgrpc.NewClient(target, opts...)withgrpc.WithTransportCredentials(insecure.NewCredentials())(fromgoogle.golang.org/grpc/credentials/insecure).NewClientdefaults to thednsresolver and connects lazily. The older calls still compile throughout 1.x; treat theDial/WithInsecureforms elsewhere in this sheet as legacy shorthand.
package main
import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// DNS resolver with round-robin (one A record per backend)
conn, err := grpc.NewClient(
"dns:///myservice.example.com:50051",
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
// Multiple explicit addresses: grpc-go has no built-in "static" scheme
// (only dns, passthrough, unix). Use a manual resolver and feed it the
// address list, then dial its scheme:
// r := manual.NewBuilderWithScheme("static")
// r.InitialState(resolver.State{Addresses: []resolver.Address{
// {Addr: "localhost:50051"}, {Addr: "localhost:50052"},
// }})
conn, err = grpc.NewClient(
"static:///lb",
grpc.WithResolvers(r),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
}
Custom Resolver for Service Discovery:
package main
import (
"google.golang.org/grpc/resolver"
)
const scheme = "consul"
type consulResolver struct {
target resolver.Target
cc resolver.ClientConn
}
func (r *consulResolver) ResolveNow(resolver.ResolveNowOptions) {
// Query Consul for service endpoints
endpoints := queryConsul(r.target.Endpoint())
var addrs []resolver.Address
for _, ep := range endpoints {
addrs = append(addrs, resolver.Address{Addr: ep})
}
r.cc.UpdateState(resolver.State{Addresses: addrs})
}
func (r *consulResolver) Close() {}
type consulResolverBuilder struct{}
func (b *consulResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
r := &consulResolver{target: target, cc: cc}
r.ResolveNow(resolver.ResolveNowOptions{})
return r, nil
}
func (b *consulResolverBuilder) Scheme() string {
return scheme
}
func init() {
resolver.Register(&consulResolverBuilder{})
}
// Usage
func main() {
conn, _ := grpc.Dial(
"consul:///myservice",
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
)
}
Health Checking:
// Standard health checking protocol
syntax = "proto3";
package grpc.health.v1;
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;
}
Implementing Health Check (Go):
package main
import (
"google.golang.org/grpc"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
func main() {
server := grpc.NewServer()
// Create health server
healthServer := health.NewServer()
healthpb.RegisterHealthServer(server, healthServer)
// Set service status
healthServer.SetServingStatus("myservice", healthpb.HealthCheckResponse_SERVING)
// Update status based on conditions
go func() {
if databaseConnected() {
healthServer.SetServingStatus("myservice", healthpb.HealthCheckResponse_SERVING)
} else {
healthServer.SetServingStatus("myservice", healthpb.HealthCheckResponse_NOT_SERVING)
}
}()
}
Common Client and Server Libraries
Key Concepts
| Language | Server Library | Client Library | Package Manager |
|---|---|---|---|
| Go | google.golang.org/grpc |
Same | go mod |
| Python | grpcio |
grpcio |
pip |
| Java | io.grpc:grpc-netty |
io.grpc:grpc-stub |
Maven/Gradle |
| Node.js | @grpc/grpc-js |
@grpc/grpc-js |
npm |
| C++ | grpc++ |
grpc++ |
vcpkg/cmake |
| Rust | tonic |
tonic |
cargo |
| C# | Grpc.AspNetCore |
Grpc.Net.Client |
NuGet |
Examples
Go Server Setup:
package main
import (
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
pb "github.com/myorg/myservice/proto"
)
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
// Server options
opts := []grpc.ServerOption{
grpc.MaxRecvMsgSize(10 * 1024 * 1024), // 10MB
grpc.MaxSendMsgSize(10 * 1024 * 1024),
}
server := grpc.NewServer(opts...)
// Register services
pb.RegisterUserServiceServer(server, &userServer{})
// Enable reflection for debugging tools like grpcurl
reflection.Register(server)
log.Println("Server listening on :50051")
if err := server.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
Python Server Setup:
import grpc
from concurrent import futures
from proto import user_service_pb2 as pb
from proto import user_service_pb2_grpc as pb_grpc
class UserServicer(pb_grpc.UserServiceServicer):
def GetUser(self, request, context):
# Implementation
pass
def serve():
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10),
options=[
('grpc.max_receive_message_length', 10 * 1024 * 1024),
]
)
pb_grpc.add_UserServiceServicer_to_server(UserServicer(), server)
# Enable reflection
from grpc_reflection.v1alpha import reflection
SERVICE_NAMES = (
pb.DESCRIPTOR.services_by_name['UserService'].full_name,
reflection.SERVICE_NAME,
)
reflection.enable_server_reflection(SERVICE_NAMES, server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()
if __name__ == '__main__':
serve()
Node.js Client:
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
// Load proto file
const packageDefinition = protoLoader.loadSync('proto/user_service.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
const userService = protoDescriptor.userservice.v1;
// Create client
const client = new userService.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
// Unary call
client.getUser({ user_id: '123' }, (err, response) => {
if (err) {
console.error('Error:', err);
return;
}
console.log('User:', response.user);
});
// With async/await (using promisify)
const { promisify } = require('util');
const getUser = promisify(client.getUser).bind(client);
async function main() {
try {
const response = await getUser({ user_id: '123' });
console.log('User:', response.user);
} catch (err) {
console.error('Error:', err);
}
}
Java Client (with Spring Boot):
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import com.myorg.userservice.v1.UserServiceGrpc;
import com.myorg.userservice.v1.GetUserRequest;
import com.myorg.userservice.v1.GetUserResponse;
public class UserClient {
private final ManagedChannel channel;
private final UserServiceGrpc.UserServiceBlockingStub blockingStub;
public UserClient(String host, int port) {
this.channel = ManagedChannelBuilder.forAddress(host, port)
.usePlaintext()
.build();
this.blockingStub = UserServiceGrpc.newBlockingStub(channel);
}
public GetUserResponse getUser(String userId) {
GetUserRequest request = GetUserRequest.newBuilder()
.setUserId(userId)
.build();
return blockingStub.getUser(request);
}
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
}
Performance Optimisation Techniques
Key Concepts
- Connection Pooling: Reuse HTTP/2 connections
- Message Compression: Reduce bandwidth usage
- Keepalive: Maintain connection health
- Flow Control: Manage streaming backpressure
- Deadline Propagation: Prevent cascading timeouts
Common Patterns
Connection and Channel Optimisation:
package main
import (
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/keepalive"
)
func createOptimisedClient() (*grpc.ClientConn, error) {
return grpc.Dial(
"localhost:50051",
grpc.WithInsecure(),
// Connection pooling (HTTP/2 multiplexing)
grpc.WithInitialWindowSize(1 << 20), // 1MB
grpc.WithInitialConnWindowSize(1 << 20), // 1MB
// Keepalive settings
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 10 * time.Second, // Ping server every 10s
Timeout: 3 * time.Second, // Wait 3s for ping ack
PermitWithoutStream: true, // Ping even without active streams
}),
// Message size limits
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(10*1024*1024), // 10MB
grpc.MaxCallSendMsgSize(10*1024*1024),
),
// Compression
grpc.WithDefaultCallOptions(
grpc.UseCompressor("gzip"),
),
)
}
func createOptimisedServer() *grpc.Server {
return grpc.NewServer(
// Keepalive enforcement
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 5 * time.Second, // Minimum ping interval
PermitWithoutStream: true,
}),
// Server keepalive
grpc.KeepaliveParams(keepalive.ServerParameters{
MaxConnectionIdle: 15 * time.Minute,
MaxConnectionAge: 30 * time.Minute,
MaxConnectionAgeGrace: 5 * time.Second,
Time: 5 * time.Second,
Timeout: 1 * time.Second,
}),
// Concurrent streams per connection
grpc.MaxConcurrentStreams(100),
// Message sizes
grpc.MaxRecvMsgSize(10 * 1024 * 1024),
grpc.MaxSendMsgSize(10 * 1024 * 1024),
)
}
Compression:
import (
"google.golang.org/grpc"
_ "google.golang.org/grpc/encoding/gzip" // Register gzip compressor
)
// Client-side compression
conn, _ := grpc.Dial(
"localhost:50051",
grpc.WithDefaultCallOptions(grpc.UseCompressor("gzip")),
)
// Per-call compression
response, err := client.GetUser(
ctx,
request,
grpc.UseCompressor("gzip"),
)
// Server automatically decompresses and can respond compressed
Deadline Propagation:
import (
"context"
"time"
)
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
// Check remaining time
deadline, ok := ctx.Deadline()
if ok {
remaining := time.Until(deadline)
if remaining < 100*time.Millisecond {
return nil, status.Error(codes.DeadlineExceeded, "insufficient time")
}
}
// Propagate deadline to downstream calls
user, err := s.userRepo.GetUser(ctx, req.UserId)
if err != nil {
return nil, err
}
return &pb.GetUserResponse{User: user}, nil
}
// Client setting deadline
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
response, err := client.GetUser(ctx, request)
Batching and Streaming for Performance:
// Instead of multiple unary calls
for _, id := range userIds {
user, _ := client.GetUser(ctx, &pb.GetUserRequest{UserId: id})
}
// Use batch endpoint or streaming
response, err := client.GetUsers(ctx, &pb.GetUsersRequest{UserIds: userIds})
// Or use client streaming for uploads
stream, _ := client.CreateUsers(ctx)
for _, user := range users {
stream.Send(&pb.CreateUserRequest{Name: user.Name})
}
response, _ := stream.CloseAndRecv()
Connection Reuse:
// BAD: Creating new connection per request
func getUser(userId string) (*pb.User, error) {
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
defer conn.Close()
client := pb.NewUserServiceClient(conn)
return client.GetUser(ctx, &pb.GetUserRequest{UserId: userId})
}
// GOOD: Reuse connection
type UserClient struct {
conn *grpc.ClientConn
client pb.UserServiceClient
}
func NewUserClient(addr string) (*UserClient, error) {
conn, err := grpc.Dial(addr, grpc.WithInsecure())
if err != nil {
return nil, err
}
return &UserClient{
conn: conn,
client: pb.NewUserServiceClient(conn),
}, nil
}
func (c *UserClient) GetUser(ctx context.Context, userId string) (*pb.User, error) {
resp, err := c.client.GetUser(ctx, &pb.GetUserRequest{UserId: userId})
if err != nil {
return nil, err
}
return resp.User, nil
}
Quick Reference
| Category | Command/Pattern | Description |
|---|---|---|
| Proto Compilation | protoc --go_out=. --go-grpc_out=. *.proto |
Generate Go gRPC code |
| Proto Compilation | python -m grpc_tools.protoc --python_out=. --grpc_python_out=. |
Generate Python gRPC code |
| Testing | grpcurl -plaintext localhost:50051 list |
List available services |
| Testing | grpcurl -plaintext -d '{"user_id":"123"}' localhost:50051 userservice.v1.UserService/GetUser |
Call unary RPC |
| Testing | grpc_health_probe -addr=localhost:50051 |
Check server health |
| Unary RPC | rpc Method(Request) returns (Response) |
Single request/response |
| Server Stream | rpc Method(Request) returns (stream Response) |
Server streams multiple responses |
| Client Stream | rpc Method(stream Request) returns (Response) |
Client streams multiple requests |
| Bidi Stream | rpc Method(stream Request) returns (stream Response) |
Bidirectional streaming |
| Metadata | metadata.AppendToOutgoingContext(ctx, "key", "value") |
Add call metadata (Go) |
| Deadline | context.WithTimeout(ctx, 5*time.Second) |
Set RPC deadline (Go) |
| Error Handling | status.Error(codes.NotFound, "user not found") |
Return gRPC error |
| Compression | grpc.UseCompressor("gzip") |
Enable gzip compression |
Common Issues and Solutions
Connection Issues
| Issue | Cause | Solution |
|---|---|---|
connection refused |
Server not running or wrong port | Verify server is running and port matches |
context deadline exceeded |
Request timeout | Increase deadline or optimise server |
transport is closing |
Connection dropped | Enable keepalive, check network |
too many pings |
Client pinging too frequently | Adjust keepalive parameters |
Serialisation Issues
| Issue | Cause | Solution |
|---|---|---|
proto: wrong wireType |
Proto schema mismatch | Regenerate code from latest proto |
field number conflict |
Duplicate field numbers | Ensure unique field numbers |
unknown field |
Missing field in proto | Add field or use proto3 defaults |
Performance Issues
| Issue | Cause | Solution |
|---|---|---|
| High latency | New connections per request | Reuse connections, implement pooling |
| Memory spikes | Large messages | Use streaming, increase limits carefully |
| Connection exhaustion | Too many concurrent streams | Limit concurrent streams, use backpressure |
Authentication Issues
// Issue: "transport: authentication handshake failed"
// Solution: Ensure TLS certificates are valid and properly configured
// Check certificate validity
openssl x509 -in server.crt -text -noout
// Verify certificate chain
openssl verify -CAfile ca.crt server.crt
// Issue: "metadata not found"
// Solution: Check interceptor order and metadata key names
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
// Handle missing metadata
}
authHeaders := md.Get("authorization") // Note: lowercase keys
Debugging Tips
# Enable gRPC debug logging
export GRPC_VERBOSITY=DEBUG
export GRPC_TRACE=all
# Use grpcurl for testing
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext localhost:50051 describe userservice.v1.UserService
# Check with grpc_cli
grpc_cli ls localhost:50051
grpc_cli call localhost:50051 userservice.v1.UserService.GetUser "user_id: '123'"
# Trace with OpenTelemetry
import (
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)
conn, _ := grpc.Dial(
addr,
grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
)
Related Topics
- Protocol Buffers: Deep dive into protobuf schema design, best practices, and evolution
- Service Mesh (Istio/Linkerd): Advanced traffic management, observability, and security for gRPC services
- HTTP/2 Protocol: Understanding the underlying transport protocol and its benefits
- API Gateway Patterns: Kong, Envoy, and other gateways for gRPC traffic management
- Distributed Tracing: OpenTelemetry, Jaeger, and Zipkin for tracing gRPC calls across services
- Message Queues (Kafka/RabbitMQ): Complementary asynchronous messaging patterns for event-driven architectures