gRPC Basics — Protocol Buffers and Services Guide
In this tutorial, you'll learn about gRPC basics. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
gRPC is a high-performance remote procedure call framework developed by Google that uses Protocol Buffers for Serialization and HTTP/2 for transport, enabling efficient communication between Microservices with built-in streaming support.
What You'll Learn
You will learn Protocol Buffer syntax, define gRPC services, implement unary and streaming RPCs in Node.js and Python, handle errors and deadlines, and compare gRPC performance with REST.
Why gRPC Matters
gRPC outperforms REST by 5 to 10 times in benchmarks, with message sizes up to 80 percent smaller than JSON. It supports bi-directional streaming, flow control, and automatic Code Generation in 11 languages. gRPC is the default choice for high-performance microservice communication at companies like Netflix, Cisco, and Square.
Real-World Use
DodaTech uses gRPC for internal service communication. Durga Antivirus Pro uses gRPC between threat analysis Microservices for real-time threat data streaming, DodaZIP uses gRPC for high-throughput file metadata exchange, and the Doda Browser sync service uses gRPC for low-latency bookmark synchronization.
gRPC Learning Path
flowchart LR
A[HTTP/2 Basics] --> B[Protocol Buffers]
B --> C[Service Definition]
C --> D[Unary RPC]
C --> E[Server Streaming]
C --> F[Client Streaming]
C --> G[Bidirectional Streaming]
D --> H[Implementation]
E --> H
F --> H
G --> H
B:::current
classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
Prerequisites
Understand RESTful Api Design Best Practices and HTTP Protocol Basics. Familiarity with JavaScript Basics or Python Basics is required. Knowledge of API Development Concepts helps.
What is Protocol Buffers?
Protocol Buffers (protobuf) is a language-neutral, platform-neutral mechanism for serializing structured data. It uses a .proto file to define the structure of your data.
syntax = "proto3";
package dodatech;
message User {
string id = 1;
string name = 2;
string email = 3;
string role = 4;
int64 created_at = 5;
}
message Address {
string street = 1;
string city = 2;
string country = 3;
string zip_code = 4;
}
Each field has a type, name, and field number. Field numbers are used to identify fields in the binary format. Numbers 1-15 take one byte and should be used for frequently occurring fields.
Protobuf Data Types
| Protobuf Type | Description | Equivalent in JSON |
|---|---|---|
| double | 64-bit float | number |
| float | 32-bit float | number |
| int32 | 32-bit integer | number |
| int64 | 64-bit integer | string |
| uint32 | Unsigned 32-bit | number |
| bool | Boolean | boolean |
| string | UTF-8 text | string |
| bytes | Binary data | base64 string |
| repeated | Array (list) | array |
| map | Key-value pairs | object |
| enum | Enumeration | string |
Defining a gRPC Service
syntax = "proto3";
package dodatech;
// User service definition
service UserService {
// Unary RPC: single request, single response
rpc GetUser (GetUserRequest) returns (User);
// Server streaming: single request, stream of responses
rpc ListUsers (ListUsersRequest) returns (stream User);
// Client streaming: stream of requests, single response
rpc CreateUsers (stream CreateUserRequest) returns (CreateUsersResponse);
// Bidirectional streaming: stream of requests, stream of responses
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
message GetUserRequest {
string id = 1;
}
message ListUsersRequest {
int32 page = 1;
int32 limit = 2;
}
message CreateUserRequest {
string name = 1;
string email = 2;
string password = 3;
}
message CreateUsersResponse {
int32 created_count = 1;
repeated User users = 2;
}
message ChatMessage {
string user_id = 1;
string content = 2;
int64 timestamp = 3;
}
message User {
string id = 1;
string name = 2;
string email = 3;
string role = 4;
int64 created_at = 5;
}
Implementing gRPC in Node.js
Step 1: Install Dependencies
Step 2: Generate Code
Copy your .proto file and load it dynamically:
// protoLoader.js
const path = require("path");
const gRPC = require("@gRPC/gRPC-js");
const protoLoader = require("@gRPC/proto-loader");
const PROTO_PATH = path.join(__dirname, "user.proto");
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const protoDescriptor = gRPC.loadPackageDefinition(packageDefinition);
const userProto = protoDescriptor.dodatech;
module.exports = userProto;
Step 3: Implement the Server
// server.js
const gRPC = require("@gRPC/gRPC-js");
const userProto = require("./protoLoader");
// In-memory data store
const users = new Map();
const server = new gRPC.Server();
server.addService(userProto.UserService.service, {
// Unary RPC
GetUser: (call, callback) => {
const { id } = call.request;
const user = users.get(id);
if (!user) {
return callback({
code: gRPC.status.NOT_FOUND,
message: "User not found"
});
}
callback(null, user);
},
// Server streaming RPC
ListUsers: (call) => {
const { page = 1, limit = 10 } = call.request;
const userArray = Array.from(users.values());
const start = (page - 1) * limit;
const end = start + limit;
const pageUsers = userArray.slice(start, end);
pageUsers.forEach((user) => {
call.write(user);
});
call.end();
},
// Client streaming RPC
CreateUsers: (call, callback) => {
let createdCount = 0;
const createdUsers = [];
call.on("data", (request) => {
const id = `user_${Date.now()}_${Math.random().toString(36).slice(2)}`;
const user = {
id,
name: request.name,
email: request.email,
role: "member",
created_at: Date.now()
};
users.set(id, user);
createdCount++;
createdUsers.push(user);
});
call.on("end", () => {
callback(null, {
created_count: createdCount,
users: createdUsers
});
});
},
// Bidirectional streaming RPC
Chat: (call) => {
call.on("data", (message) => {
console.log(`[Chat] User ${message.user_id}: ${message.content}`);
// Echo back with server acknowledgment
call.write({
user_id: "server",
content: `Received: ${message.content}`,
timestamp: Date.now()
});
});
call.on("end", () => {
call.end();
});
}
});
server.bindAsync(
"0.0.0.0:50051",
gRPC.ServerCredentials.createInsecure(),
(error, port) => {
if (error) {
console.error("Server failed:", error);
return;
}
console.log(`gRPC server running on port ${port}`);
server.start();
}
);
Step 4: Implement the Client
// client.js
const gRPC = require("@gRPC/gRPC-js");
const userProto = require("./protoLoader");
const client = new userProto.UserService(
"localhost:50051",
gRPC.credentials.createInsecure()
);
// Unary call
client.GetUser({ id: "user_1" }, (error, response) => {
if (error) {
console.error("Error:", error.details);
} else {
console.log("GetUser response:", response);
}
});
// Server streaming
const listCall = client.ListUsers({ page: 1, limit: 10 });
listCall.on("data", (user) => {
console.log("Received user:", user.name);
});
listCall.on("end", () => {
console.log("ListUsers stream ended");
});
// Client streaming
const createCall = client.CreateUsers((error, response) => {
if (!error) {
console.log(`Created ${response.created_count} users`);
}
});
createCall.write({ name: "Alice", email: "alice@example.com", password: "pass123" });
createCall.write({ name: "Bob", email: "bob@example.com", password: "pass456" });
createCall.end();
// Bidirectional streaming
const chatCall = client.Chat();
chatCall.on("data", (message) => {
console.log(`[${message.user_id}]: ${message.content}`);
});
chatCall.write({ user_id: "client1", content: "Hello from gRPC!", timestamp: Date.now() });
chatCall.write({ user_id: "client1", content: "How are you?", timestamp: Date.now() });
chatCall.end();
Implementing gRPC in Python
Pip install grpcio grpcio-tools
# Generate Python code from proto
Python -m gRPC_tools.protoc -I. --Python_out=. --gRPC_Python_out=. user.proto
# server.py
import gRPC
from concurrent import futures
import user_pb2
import user_pb2_gRPC
class UserService(user_pb2_gRPC.UserServiceServicer):
def __init__(self):
self.users = {}
def GetUser(self, request, context):
user = self.users.get(request.id)
if not user:
context.set_code(gRPC.StatusCode.NOT_FOUND)
context.set_details("User not found")
return user_pb2.User()
return user
def ListUsers(self, request, context):
user_list = list(self.users.values())
start = (request.page - 1) * request.limit
end = start + request.limit
for user in user_list[start:end]:
yield user
def CreateUsers(self, request_Iterator, context):
created = []
for req in request_Iterator:
user = user_pb2.User(
id=f"user_{len(self.users) + 1}",
name=req.name,
email=req.email,
role="member",
created_at=int(time.time())
)
self.users[user.id] = user
created.append(user)
return user_pb2.CreateUsersResponse(
created_count=len(created),
users=created
)
def serve():
server = gRPC.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_gRPC.add_UserServiceServicer_to_server(UserService(), server)
server.add_insecure_port("[::]:50051")
print("gRPC server running on port 50051")
server.start()
server.wait_for_termination()
if __name__ == "__main__":
serve()
Performance Comparison: gRPC vs REST
| Aspect | gRPC | REST (JSON) |
|---|---|---|
| Protocol | HTTP/2 | HTTP/1.1 or HTTP/2 |
| Serialization | Protocol Buffers (binary) | JSON (text) |
| Message Size | ~200 bytes for user object | ~500 bytes for same data |
| Latency (p95) | 5ms | 15ms |
| Throughput | 20,000 req/s | 5,000 req/s |
| Streaming | Native bidirectional | Requires WebSocket |
| Browser Support | Needs gRPC-Web | Native |
Common Errors
Not setting deadlines — gRPC calls can hang indefinitely without deadlines. Always set a deadline for every RPC call to prevent resource leaks.
Large messages without streaming — Sending protobuf messages larger than 4MB without streaming. Use streaming for large payloads or increase the max message size in configuration.
Ignoring error codes — Treating all gRPC errors the same. Differentiate between UNAVAILABLE (retry), NOT_FOUND (handle gracefully), and INTERNAL (log and alert).
No Load Balancing — Connecting to a single gRPC server instance. Use client-side Load Balancing with a service discovery system like Consul or Kubernetes DNS.
Field number conflicts — Reusing field numbers when updating protobuf schemas. Never reuse a field number. Deprecate old fields with
reservedkeyword instead of deleting them.Not using HTTPS — Using insecure gRPC connections in production. Always use TLS (ServerCredentials.createSsl) for production gRPC deployments.
Blocking the event loop — Running synchronous operations in gRPC handlers designed for async. Use async handlers or Thread pools for blocking operations.
Practice Questions
- What are the advantages of Protocol Buffers over JSON?
- What are the four types of gRPC RPCs?
- How does gRPC handle streaming differently from REST?
- Why is HTTP/2 important for gRPC performance?
- How do you handle errors in gRPC?
Challenge
Build a gRPC-based microservice for a real-time notification system. Define protobuf messages for Notification, NotificationType (enum), and UserPreference. Implement four services: SendNotification (unary), StreamNotifications (server streaming for a user), BatchSubscribe (client streaming for bulk subscription), and NotificationChat (bidirectional for support tickets). Use TLS for production security and implement deadlines and retries on the client side.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro