🚀 W12D1: gRPC & Protocol Buffers

Modern API Communication for High-Performance Systems

Week 12 • Day 1 • 3 Hours
1
2
3
4
5

🎯 Learning Objectives

By the end of this session, you will be able to:

⚙️ Environment Setup

1

Navigate to Project Directory

Open your terminal and navigate to the project folder:

$ cd ~/Documents/W12D1/W12D1-gRPC-Services
2

Install Python Dependencies

Install the required gRPC packages:

$ pip install grpcio grpcio-tools protobuf
💡
What these packages do:
  • grpcio - Core gRPC library for Python
  • grpcio-tools - Protocol Buffer compiler for Python
  • protobuf - Protocol Buffers runtime library
3

Generate Protocol Buffer Code

Compile the .proto files to generate Python code:

$ python -m grpc_tools.protoc \
    --proto_path=protos \
    --python_out=generated \
    --grpc_python_out=generated \
    protos/*.proto
Expected Result: Six files created in the generated/ folder: user_service_pb2.py, user_service_pb2_grpc.py, etc.
4

Verify Setup

Run the performance demo to verify everything is working:

$ python performance_demo.py

You should see output comparing gRPC vs REST performance with ~40% latency improvement.

🔧 Setup Troubleshooting

ModuleNotFoundError: No module named 'grpc'

Cause: gRPC packages not installed or installed in wrong Python environment.

Solution:

# Check which Python you're using
which python

# Install packages explicitly
pip3 install grpcio grpcio-tools protobuf

# Or with python3
python3 -m pip install grpcio grpcio-tools protobuf
protoc: command not found

Cause: Using protoc directly instead of through Python.

Solution: Always use the Python module:

# Wrong:
protoc --python_out=generated protos/*.proto

# Correct:
python -m grpc_tools.protoc --proto_path=protos --python_out=generated --grpc_python_out=generated protos/*.proto
ImportError: cannot import name 'user_service_pb2'

Cause: Generated files not found or path issues.

Solutions:

  1. Verify files exist: ls generated/
  2. Run protoc command from the project root directory
  3. Check the generated/__init__.py file exists
# Create __init__.py if missing
touch generated/__init__.py

# Regenerate proto files
python -m grpc_tools.protoc --proto_path=protos --python_out=generated --grpc_python_out=generated protos/*.proto

📚 Core Concepts

What is gRPC?

gRPC (gRPC Remote Procedure Call) is a high-performance RPC framework developed by Google. It enables efficient communication between services using:

📦

Protocol Buffers

Binary serialization format that's 3x smaller and 10x faster than JSON

🚀

HTTP/2

Multiplexed connections, header compression, and server push

🔄

Streaming

Native support for bidirectional streaming

🌐

Multi-Language

Code generation for 10+ programming languages

The Four Streaming Patterns

Pattern Client → Server Use Cases Example
Unary (Simple) 1 request → 1 response Standard API calls, CRUD GetUser(id)
Server Streaming 1 request → N responses Data feeds, progress, A/B testing StreamPredictions()
Client Streaming N requests → 1 response File upload, batch processing BatchPredict()
Bidirectional N requests ↔ N responses Chat, real-time analytics Chat()

gRPC vs REST: When to Use Each

✅ Use gRPC when:

  • High performance required - ML serving, gaming, trading
  • Service-to-service communication - Microservices
  • Streaming data - Live dashboards, IoT
  • Polyglot environments - Python + Java + Go

✅ Use REST when:

  • Browser clients - Web apps, mobile apps
  • Public APIs - Third-party integrations
  • Simple CRUD - Content management
  • Caching needed - CDN, HTTP caching

🔄 Best of Both Worlds:

┌─────────────┐     REST      ┌─────────────┐     gRPC      ┌─────────────┐
│   Browser   │ ────────────> │ API Gateway │ ────────────> │   Service   │
└─────────────┘     JSON      └─────────────┘   Protobuf    └─────────────┘

Use REST for external clients, gRPC internally for performance.

📊 Exercise 1: Performance Demo (15 min)

🎯
Goal: Understand why gRPC is faster than REST APIs
1

Run the Performance Comparison

$ python performance_demo.py
2

Expected Output

📈 PERFORMANCE RESULTS ============================================================ REST API: ├─ Average latency: 104ms ├─ Average data size: 342 bytes └─ Total time for 50 requests: 0.52s gRPC: ├─ Average latency: 63ms ├─ Average data size: 114 bytes └─ Total time for 50 requests: 0.32s 🎯 gRPC IMPROVEMENTS ├─ 39.0% lower latency ├─ 66.7% smaller data transfer └─ 37.8% better throughput
3

Reflection Questions

  • Why is the data size so much smaller with gRPC?
  • What impact would these improvements have at 10,000 requests/second?
  • When might REST still be the better choice despite lower performance?

👤 Exercise 2: User Service (25 min)

🎯
Goal: Build your first gRPC service with simple RPC and server streaming

Part A: Understand the Protocol Buffer Schema

1

Review the Proto File

Open protos/user_service.proto and study the schema:

syntax = "proto3";

package user_service;

// User data structure
message User {
    string user_id = 1;       // Field number 1
    string name = 2;          // Field number 2
    string email = 3;         // Field number 3
    int32 age = 4;            // Integer field
    repeated string interests = 5; // List of strings
    UserPreferences preferences = 6; // Nested message
}

// Service definition
service UserService {
    rpc GetUser(GetUserRequest) returns (GetUserResponse);
    rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
    rpc GetAllUsers(Empty) returns (stream User);
}

Part B: Start the Server

2

Open Terminal 1 - Start the Server

$ python services/user_service.py
Expected: Server starts on port 50051
🚀 gRPC User Service Started
   Listening on: [::]:50051

Part C: Run the Client

3

Open Terminal 2 - Run the Client

$ python client.py
4

Observe the Tests

The client runs through several scenarios:

Test What It Does Expected Result
GetUser (exists) Fetches user_001 ✅ Returns Alice Johnson
GetUser (missing) Fetches user_999 ❌ NOT_FOUND error
CreateUser Creates David Lee ✅ User created
CreateUser (duplicate) Creates David again ❌ ALREADY_EXISTS error
GetAllUsers Streams all users ✅ Receives 4 users

🔧 Exercise 2 Troubleshooting

grpc._channel._InactiveRpcError: Connection refused

Cause: The server is not running.

Solution:

  1. Make sure you started the server in Terminal 1
  2. Check that port 50051 is available: lsof -i :50051
  3. Kill any existing process: pkill -f user_service.py
  4. Restart the server
Server starts but client hangs

Cause: Firewall or network issue.

Solution:

# Check if server is listening
netstat -an | grep 50051

# Try localhost explicitly
# Edit client.py to use '127.0.0.1' instead of 'localhost'
AttributeError: module has no attribute

Cause: Generated code is outdated or corrupted.

Solution: Regenerate the proto files:

# Delete old generated files
rm -f generated/*.py

# Regenerate
python -m grpc_tools.protoc --proto_path=protos --python_out=generated --grpc_python_out=generated protos/*.proto

# Restart the server

🧠 Exercise 3: ML Service - All Streaming Patterns (35 min)

🎯
Goal: Master all four gRPC streaming patterns with the ML Prediction Service

Part A: Start the ML Service

1

Stop the User Service (if running) and start ML Service

# Terminal 1 - Press Ctrl+C to stop User Service, then:
$ python services/ml_service.py
Expected: ML Service starts on port 50052 with 3 models available

Part B: Run the ML Client

2

Run the comprehensive ML client

$ python ml_client.py

Part C: Understand Each Streaming Pattern

1️⃣ Unary RPC

Method: Predict()

Pattern: 1 request → 1 response

Use Case: Single ML prediction

response = stub.Predict(request)

2️⃣ Server Streaming

Method: PredictStream()

Pattern: 1 request → N responses

Use Case: A/B testing model variants

for response in stub.PredictStream(request):
    print(response)

3️⃣ Client Streaming

Method: PredictBatch()

Pattern: N requests → 1 response

Use Case: Batch prediction

def generate():
    for data in batch:
        yield request
response = stub.PredictBatch(generate())

4️⃣ Bidirectional

Method: PredictRealtime()

Pattern: N requests ↔ N responses

Use Case: Real-time analytics

for response in stub.PredictRealtime(generate()):
    process(response)

Part D: Study the Server Implementation

3

Open services/ml_service.py and study:

  • How yield is used for streaming responses
  • How request_iterator handles streaming requests
  • Error handling with context.set_code()
  • The context.is_active() check for client disconnection

🔧 Exercise 3 Troubleshooting

Streaming stops unexpectedly

Cause: Client disconnected or timeout.

Solution: Check for context.is_active() in server code and handle gracefully:

def PredictStream(self, request, context):
    for i in range(100):
        if not context.is_active():
            print("Client disconnected")
            break
        yield response
Bidirectional streaming only receives first response

Cause: Generator exhausted or not properly yielding.

Solution: Ensure your request generator keeps yielding:

def generate_requests():
    for i in range(10):
        yield request
        time.sleep(0.1)  # Don't exhaust immediately

# Call with the generator
for response in stub.PredictRealtime(generate_requests()):
    print(response)

🌐 Exercise 4: API Gateway (25 min)

🎯
Goal: Create a REST API that translates to gRPC calls (hybrid architecture)

Part A: Start All Services

1

Open 3 Terminals

# Terminal 1 - User Service
$ python services/user_service.py

# Terminal 2 - ML Service
$ python services/ml_service.py

# Terminal 3 - API Gateway
$ python assignment/api_gateway/gateway.py

Part B: Test REST Endpoints

2

Test with curl (Terminal 4)

# Health check
$ curl http://localhost:8080/health

# Get all users (REST → gRPC streaming)
$ curl http://localhost:8080/api/users

# Get single user
$ curl http://localhost:8080/api/users/user_001

# List ML models
$ curl http://localhost:8080/api/models

# Make a prediction
$ curl -X POST http://localhost:8080/api/predict \
  -H "Content-Type: application/json" \
  -d '{"model_id":"fraud_detection","features":{"amount":500,"time":14}}'

# Create a new user
$ curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"user_id":"user_100","name":"Test User","email":"[email protected]","age":25}'

Part C: Understand the Gateway Pattern

3

Study assignment/api_gateway/gateway.py

Notice how the gateway:

  • Receives REST requests (JSON over HTTP/1.1)
  • Translates to gRPC calls (Protobuf over HTTP/2)
  • Converts gRPC responses back to JSON
  • Handles streaming by collecting all responses

🔧 Exercise 4 Troubleshooting

Gateway returns "Service unavailable"

Cause: Backend gRPC services not running.

Solution:

  1. Verify User Service is running on port 50051
  2. Verify ML Service is running on port 50052
  3. Check gateway logs for connection errors
# Check what's running
lsof -i :50051
lsof -i :50052
lsof -i :8080
CORS error in browser

Cause: Cross-origin request blocked.

Solution: The gateway includes CORS headers. If you still have issues:

# Test with curl instead of browser
curl -v http://localhost:8080/api/users

# Or add to browser fetch:
fetch('http://localhost:8080/api/users', {
    mode: 'cors'
})

🔧 General Troubleshooting

Common Issues & Solutions

Port already in use
# Find what's using the port
$ lsof -i :50051

# Kill the process
$ kill -9 <PID>

# Or kill all Python processes (careful!)
$ pkill -f "python.*service"
Python version issues

gRPC requires Python 3.7+. Check your version:

$ python --version
# Should be 3.7 or higher

# If using wrong version, try:
$ python3 services/user_service.py
SSL/TLS errors

This project uses insecure channels for learning. For production:

# Server with TLS
server_credentials = grpc.ssl_server_credentials([(key, cert)])
server.add_secure_port('[::]:50051', server_credentials)

# Client with TLS
credentials = grpc.ssl_channel_credentials(root_cert)
channel = grpc.secure_channel('localhost:50051', credentials)
Memory issues with large streams

When streaming large data, process items one at a time:

# DON'T: Collect all in memory
all_users = list(stub.GetAllUsers(request))  # Bad for large data

# DO: Process one at a time
for user in stub.GetAllUsers(request):
    process(user)  # Good - constant memory

Debug Tips

Enable gRPC Logging

import logging
logging.basicConfig(level=logging.DEBUG)

import os
os.environ['GRPC_VERBOSITY'] = 'DEBUG'
os.environ['GRPC_TRACE'] = 'all'

Test Connection

import grpc

channel = grpc.insecure_channel('localhost:50051')
try:
    grpc.channel_ready_future(channel).result(timeout=5)
    print("Connected!")
except grpc.FutureTimeoutError:
    print("Connection failed")

🚀 Next Steps & Resources

Assignment: Production Microservices

Build a complete microservices system with:

Further Reading

🎓 Career Applications

  • Senior Backend Engineer ($120K-180K)
  • Platform Engineer ($130K-200K)
  • ML Infrastructure Engineer ($140K-220K)