NexusCS

gRPC

APIs
High-performance RPC framework using Protocol Buffers over HTTP/2 with cross-language support
rpc
protobuf
http2
microservices

Getting started

Introduction

gRPC is a modern open source high-performance Remote Procedure Call (RPC) framework that uses Protocol Buffers as the interface definition language and runs over HTTP/2.

Installation

# Node.js
npm install @grpc/grpc-js @grpc/proto-loader

# Python
pip install grpcio grpcio-tools

# Go
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Quick Example

syntax = "proto3";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

Protocol Buffers Syntax (Proto3)

Basic Message

syntax = "proto3";

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;
}

Field numbers 1-15 use 1 byte encoding. Use them for frequent fields.

Scalar Types

Proto Type Description Example
int32 Signed 32-bit integer -2147483648 to 2147483647
int64 Signed 64-bit integer Large numbers
uint32 Unsigned 32-bit 0 to 4294967295
uint64 Unsigned 64-bit Large positive numbers
string UTF-8 text "hello"
bool Boolean true or false
bytes Binary data b"binary"
float 32-bit float 3.14
double 64-bit float 3.14159

Enums

enum Status {
  UNKNOWN = 0;  // First value must be 0
  ACTIVE = 1;
  INACTIVE = 2;
  DELETED = 3;
}

message User {
  string name = 1;
  Status status = 2;
}

Repeated Fields

message User {
  string name = 1;
  repeated string emails = 2;  // Array
  repeated int32 scores = 3;
}

Nested Messages

message Person {
  string name = 1;

  message PhoneNumber {
    string number = 1;
    string type = 2;
  }

  repeated PhoneNumber phones = 2;
}

Reserved Fields

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";

  string name = 1;
  // int32 deleted = 2;  // Reserved!
}

Prevent field number/name reuse to avoid compatibility issues.

Maps

message Project {
  string name = 1;
  map<string, string> metadata = 2;
  map<int32, User> users = 3;
}

Oneof

message SearchRequest {
  string query = 1;

  oneof filter {
    string category = 2;
    int32 user_id = 3;
    bool favorites = 4;
  }
}

Only one field can be set at a time.

Service Definition (4 RPC Types)

Unary RPC

service UserService {
  rpc GetUser (GetUserRequest) returns (User) {}
}

message GetUserRequest {
  int32 id = 1;
}

Single request, single response.

Server Streaming

service DataService {
  rpc StreamData (DataRequest)
    returns (stream DataChunk) {}
}

message DataRequest {
  string query = 1;
}

message DataChunk {
  bytes data = 1;
}

Single request, multiple responses.

Client Streaming

service UploadService {
  rpc UploadFile (stream FileChunk)
    returns (UploadResponse) {}
}

message FileChunk {
  bytes data = 1;
}

Multiple requests, single response.

Bidirectional Streaming

service ChatService {
  rpc Chat (stream ChatMessage)
    returns (stream ChatMessage) {}
}

message ChatMessage {
  string user = 1;
  string text = 2;
}

Multiple requests and responses.

Code Generation

Node.js

# Install tools
npm install -g grpc-tools

# Generate code
grpc_tools_node_protoc \
  --js_out=import_style=commonjs,binary:./generated \
  --grpc_out=grpc_js:./generated \
  --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` \
  proto/service.proto

Python

# Generate code
python -m grpc_tools.protoc \
  -I./proto \
  --python_out=./generated \
  --grpc_python_out=./generated \
  proto/service.proto

Go

# Generate code
protoc \
  --go_out=./generated \
  --go_opt=paths=source_relative \
  --go-grpc_out=./generated \
  --go-grpc_opt=paths=source_relative \
  proto/service.proto

Server Examples

Node.js Server

const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");

const packageDefinition = protoLoader.loadSync("service.proto", {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});

const proto = grpc.loadPackageDefinition(packageDefinition);

function sayHello(call, callback) {
  callback(null, { message: "Hello " + call.request.name });
}

const server = new grpc.Server();
server.addService(proto.Greeter.service, { sayHello: sayHello });
server.bindAsync(
  "0.0.0.0:50051",
  grpc.ServerCredentials.createInsecure(),
  () => {
    server.start();
    console.log("Server running at http://0.0.0.0:50051");
  },
);

Python Server

import grpc
from concurrent import futures
import service_pb2
import service_pb2_grpc

class GreeterServicer(service_pb2_grpc.GreeterServicer):
    def SayHello(self, request, context):
        return service_pb2.HelloReply(message=f'Hello {request.name}')

server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
service_pb2_grpc.add_GreeterServicer_to_server(GreeterServicer(), server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()

Go Server

package main

import (
    "context"
    "log"
    "net"
    "google.golang.org/grpc"
    pb "path/to/generated"
)

type server struct {
    pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello " + req.Name}, nil
}

func main() {
    lis, _ := net.Listen("tcp", ":50051")
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    log.Fatal(s.Serve(lis))
}

Client Examples

Node.js Client

const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");

const packageDefinition = protoLoader.loadSync("service.proto");
const proto = grpc.loadPackageDefinition(packageDefinition);

const client = new proto.Greeter(
  "localhost:50051",
  grpc.credentials.createInsecure(),
);

client.sayHello({ name: "World" }, (error, response) => {
  if (error) {
    console.error(error);
  } else {
    console.log("Greeting:", response.message);
  }
});

Python Client

import grpc
import service_pb2
import service_pb2_grpc

channel = grpc.insecure_channel('localhost:50051')
stub = service_pb2_grpc.GreeterStub(channel)

response = stub.SayHello(service_pb2.HelloRequest(name='World'))
print(f'Greeting: {response.message}')

Go Client

package main

import (
    "context"
    "log"
    "google.golang.org/grpc"
    pb "path/to/generated"
)

func main() {
    conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
    defer conn.Close()

    client := pb.NewGreeterClient(conn)

    response, err := client.SayHello(context.Background(),
        &pb.HelloRequest{Name: "World"})
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Greeting: %s", response.Message)
}

Streaming Examples

Server Streaming (Go)

func (s *server) StreamData(req *pb.DataRequest, stream pb.DataService_StreamDataServer) error {
    for i := 0; i < 10; i++ {
        chunk := &pb.DataChunk{Data: []byte(fmt.Sprintf("Chunk %d", i))}
        if err := stream.Send(chunk); err != nil {
            return err
        }
        time.Sleep(time.Second)
    }
    return nil
}

// Client
stream, _ := client.StreamData(context.Background(), &pb.DataRequest{Query: "test"})
for {
    chunk, err := stream.Recv()
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatal(err)
    }
    log.Printf("Received: %s", chunk.Data)
}

Bidirectional Streaming (Python)

# Server
def Chat(self, request_iterator, context):
    for message in request_iterator:
        response = service_pb2.ChatMessage(
            user='Bot',
            text=f'Echo: {message.text}'
        )
        yield response

# Client
def generate_messages():
    messages = ['Hello', 'How are you?', 'Goodbye']
    for msg in messages:
        yield service_pb2.ChatMessage(user='User', text=msg)

responses = stub.Chat(generate_messages())
for response in responses:
    print(f'{response.user}: {response.text}')

Status Codes

Common Codes

Code Number Description
OK 0 Success
CANCELLED 1 Operation cancelled
UNKNOWN 2 Unknown error
INVALID_ARGUMENT 3 Invalid argument
DEADLINE_EXCEEDED 4 Timeout
NOT_FOUND 5 Resource not found
ALREADY_EXISTS 6 Resource exists
PERMISSION_DENIED 7 No permission

Error Codes

Code Number Description
RESOURCE_EXHAUSTED 8 Out of resources
FAILED_PRECONDITION 9 Precondition failed
ABORTED 10 Operation aborted
OUT_OF_RANGE 11 Out of range
UNIMPLEMENTED 12 Not implemented
INTERNAL 13 Internal error
UNAVAILABLE 14 Service unavailable
DATA_LOSS 15 Data loss
UNAUTHENTICATED 16 Not authenticated

Error Handling

# Server
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details('User not found')
return service_pb2.User()

# Client
try:
    response = stub.GetUser(request)
except grpc.RpcError as e:
    print(f'Error: {e.code()}: {e.details()}')

Authentication

SSL/TLS

# Server
server_credentials = grpc.ssl_server_credentials(
    [(private_key, certificate_chain)]
)
server.add_secure_port('[::]:50051', server_credentials)

# Client
credentials = grpc.ssl_channel_credentials(
    root_certificates=root_cert
)
channel = grpc.secure_channel('localhost:50051', credentials)

Token-based (Metadata)

# Client
def auth_interceptor(context, callback):
    metadata = (('authorization', 'Bearer YOUR_TOKEN'),)
    callback(metadata, None)

channel = grpc.insecure_channel('localhost:50051')
channel = grpc.intercept_channel(channel, auth_interceptor)

# Server
def validate_token(context):
    metadata = dict(context.invocation_metadata())
    token = metadata.get('authorization', '')
    if not token.startswith('Bearer '):
        context.abort(grpc.StatusCode.UNAUTHENTICATED, 'Invalid token')

Google Default Credentials

import "google.golang.org/grpc/credentials/oauth"

perRPC, _ := oauth.NewApplicationDefault(context.Background(), scope)
conn, _ := grpc.Dial(
    serverAddr,
    grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
    grpc.WithPerRPCCredentials(perRPC),
)

Deadlines & Timeouts

Setting Deadlines

// Client - set 5 second deadline
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

response, err := client.SayHello(ctx, &pb.HelloRequest{Name: "World"})
if err != nil {
    if status.Code(err) == codes.DeadlineExceeded {
        log.Println("Request timed out")
    }
}
# Python - set 5 second timeout
try:
    response = stub.SayHello(
        request,
        timeout=5.0
    )
except grpc.RpcError as e:
    if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
        print("Request timed out")

Propagating Deadlines

// Server - propagate deadline to downstream service
func (s *server) ProcessRequest(ctx context.Context, req *pb.Request) (*pb.Response, error) {
    // Deadline from incoming context is automatically propagated
    downstreamResponse, err := downstreamClient.DoWork(ctx, &pb.Work{})
    if err != nil {
        return nil, err
    }
    return &pb.Response{}, nil
}

Metadata

Sending Metadata

// Client - send metadata
md := metadata.Pairs(
    "timestamp", time.Now().Format(time.StampNano),
    "client-id", "web-client",
)
ctx := metadata.NewOutgoingContext(context.Background(), md)
response, err := client.SayHello(ctx, &pb.HelloRequest{Name: "World"})
# Python - send metadata
metadata = (
    ('timestamp', str(time.time())),
    ('client-id', 'web-client'),
)
response = stub.SayHello(request, metadata=metadata)

Reading Metadata

// Server - read metadata
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if ok {
        clientID := md.Get("client-id")
        log.Printf("Client ID: %v", clientID)
    }
    return &pb.HelloReply{Message: "Hello"}, nil
}

Binary Metadata

// Keys ending in "-bin" are treated as binary
md := metadata.Pairs(
    "my-key", "text value",
    "my-binary-key-bin", string([]byte{1, 2, 3}),
)

Binary keys must end with -bin suffix.

Interceptors

Unary Interceptor (Go)

func unaryInterceptor(
    ctx context.Context,
    req interface{},
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (interface{}, error) {
    // Pre-processing
    log.Printf("Method: %s", info.FullMethod)
    start := time.Now()

    // Call handler
    resp, err := handler(ctx, req)

    // Post-processing
    log.Printf("Duration: %v", time.Since(start))
    return resp, err
}

// Register
server := grpc.NewServer(
    grpc.UnaryInterceptor(unaryInterceptor),
)

Stream Interceptor (Go)

func streamInterceptor(
    srv interface{},
    ss grpc.ServerStream,
    info *grpc.StreamServerInfo,
    handler grpc.StreamHandler,
) error {
    log.Printf("Stream Method: %s", info.FullMethod)
    return handler(srv, ss)
}

// Register
server := grpc.NewServer(
    grpc.StreamInterceptor(streamInterceptor),
)

Load Balancing

Client-side Load Balancing

import "google.golang.org/grpc/balancer/roundrobin"

// Round-robin
conn, err := grpc.Dial(
    "dns:///my-service:50051",
    grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
    grpc.WithInsecure(),
)

Name Resolver

// Custom resolver for service discovery
conn, err := grpc.Dial(
    "my-custom-scheme://authority/service",
    grpc.WithInsecure(),
)

Health Checking

Server Health Check

import "google.golang.org/grpc/health"
import "google.golang.org/grpc/health/grpc_health_v1"

healthServer := health.NewServer()
grpc_health_v1.RegisterHealthServer(s, healthServer)
healthServer.SetServingStatus("myservice", grpc_health_v1.HealthCheckResponse_SERVING)

Client Health Check

import "google.golang.org/grpc/health/grpc_health_v1"

healthClient := grpc_health_v1.NewHealthClient(conn)
resp, err := healthClient.Check(
    context.Background(),
    &grpc_health_v1.HealthCheckRequest{Service: "myservice"},
)
if resp.Status != grpc_health_v1.HealthCheckResponse_SERVING {
    log.Println("Service unhealthy")
}

Best Practices

Field Numbers

message User {
  // Use 1-15 for frequent fields (1 byte encoding)
  string id = 1;
  string name = 2;
  string email = 3;

  // Use 16+ for less frequent fields (2 bytes)
  string bio = 16;
  string avatar_url = 17;
}

Always Set Deadlines

// ✅ Good - explicit timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
response, err := client.GetUser(ctx, request)

// ❌ Bad - no timeout (may hang forever)
response, err := client.GetUser(context.Background(), request)

Reserve Deleted Fields

message User {
  reserved 2, 15;  // Never reuse these numbers
  reserved "old_field", "deprecated_field";

  string name = 1;
  // string deleted_field = 2;  // RESERVED!
}

Check EOF on Streams

// ✅ Correct stream reading
for {
    msg, err := stream.Recv()
    if err == io.EOF {
        break  // Stream ended normally
    }
    if err != nil {
        return err  // Actual error
    }
    // Process msg
}

Use SSL/TLS

// ✅ Production - use TLS
creds, _ := credentials.NewClientTLSFromFile("server.crt", "")
conn, _ := grpc.Dial("server:443", grpc.WithTransportCredentials(creds))

// ❌ Development only - insecure
conn, _ := grpc.Dial("server:50051", grpc.WithInsecure())

Metadata Reserved Keys

// ✅ Good - custom metadata
md := metadata.Pairs("x-request-id", "12345")

// ❌ Bad - grpc- prefix is reserved
md := metadata.Pairs("grpc-custom", "value")

Gotchas

Deadlines Have No Default

gRPC does not set a default deadline. Calls without a deadline can hang indefinitely. Always set explicit timeouts on client calls.

Field Numbers are Permanent

Once a field number is used in production, it cannot be reused. Use reserved to prevent accidental reuse when removing fields.

Enum First Value Must Be 0

// ✅ Correct
enum Status {
  UNKNOWN = 0;
  ACTIVE = 1;
}

// ❌ Error: first enum value must be zero
enum Status {
  ACTIVE = 1;
  INACTIVE = 2;
}

Metadata Keys Are Lowercase

gRPC automatically lowercases metadata keys. Case-sensitive matching will fail.

Streaming Context Cancellation

When client cancels context, server stream handler should check ctx.Done() to avoid processing cancelled requests.

Supported Languages

  • C++
  • C#
  • Dart
  • Go
  • Java
  • Kotlin
  • Node.js
  • Objective-C
  • PHP
  • Python
  • Ruby
  • Swift

Also see