gRPC: High-Performance RPC Framework
gRPC provides high-performance RPC for microservices. After building production gRPC services, here’s how to use it effectively.
What is gRPC?
gRPC is:
- Language-agnostic RPC framework
- Uses Protocol Buffers for serialization
- HTTP/2 based
- Streaming support
- Type-safe contracts
Protocol Buffers
Define Service
// user.proto
syntax = "proto3";
package user;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc StreamUsers(StreamUsersRequest) returns (stream User);
}
message GetUserRequest {
string user_id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message ListUsersRequest {
int32 page = 1;
int32 page_size = 2;
}
message ListUsersResponse {
repeated User users = 1;
int32 total = 2;
}
message StreamUsersRequest {
string filter = 1;
}
message User {
string id = 1;
string name = 2;
string email = 3;
int64 created_at = 4;
}
Node.js Server
Installation
npm install @grpc/grpc-js @grpc/proto-loader
npm install -D grpc-tools
Server Implementation
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync('user.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const userProto = grpc.loadPackageDefinition(packageDefinition).user;
const users = [
{ id: '1', name: 'John Doe', email: 'john@example.com', created_at: Date.now() },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', created_at: Date.now() }
];
function getUser(call, callback) {
const user = users.find(u => u.id === call.request.user_id);
if (!user) {
return callback({
code: grpc.status.NOT_FOUND,
message: 'User not found'
});
}
callback(null, user);
}
function createUser(call, callback) {
const user = {
id: String(users.length + 1),
name: call.request.name,
email: call.request.email,
created_at: Date.now()
};
users.push(user);
callback(null, user);
}
function listUsers(call, callback) {
const page = call.request.page || 1;
const pageSize = call.request.page_size || 10;
const start = (page - 1) * pageSize;
const end = start + pageSize;
callback(null, {
users: users.slice(start, end),
total: users.length
});
}
function streamUsers(call) {
users.forEach(user => {
call.write(user);
});
call.end();
}
const server = new grpc.Server();
server.addService(userProto.UserService.service, {
getUser,
createUser,
listUsers,
streamUsers
});
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
server.start();
console.log('gRPC server running on port 50051');
});
Node.js Client
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync('user.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const userProto = grpc.loadPackageDefinition(packageDefinition).user;
const client = new userProto.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
// Unary call
client.getUser({ user_id: '1' }, (error, user) => {
if (error) {
console.error('Error:', error);
return;
}
console.log('User:', user);
});
// Streaming call
const stream = client.streamUsers({ filter: '' });
stream.on('data', (user) => {
console.log('User:', user);
});
stream.on('end', () => {
console.log('Stream ended');
});
Python Server
import grpc
from concurrent import futures
import user_pb2
import user_pb2_grpc
class UserService(user_pb2_grpc.UserServiceServicer):
def __init__(self):
self.users = [
user_pb2.User(
id='1',
name='John Doe',
email='john@example.com',
created_at=1234567890
)
]
def GetUser(self, request, context):
user = next((u for u in self.users if u.id == request.user_id), None)
if not user:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details('User not found')
return user_pb2.User()
return user
def CreateUser(self, request, context):
user = user_pb2.User(
id=str(len(self.users) + 1),
name=request.name,
email=request.email,
created_at=int(time.time())
)
self.users.append(user)
return user
def ListUsers(self, request, context):
page = request.page or 1
page_size = request.page_size or 10
start = (page - 1) * page_size
end = start + page_size
return user_pb2.ListUsersResponse(
users=self.users[start:end],
total=len(self.users)
)
def StreamUsers(self, request, context):
for user in self.users:
yield user
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_grpc.add_UserServiceServicer_to_server(UserService(), server)
server.add_insecure_port('[::]:50051')
server.start()
print('gRPC server running on port 50051')
server.wait_for_termination()
if __name__ == '__main__':
serve()
Streaming
Server-Side Streaming
rpc StreamUsers(StreamUsersRequest) returns (stream User);
function streamUsers(call) {
const interval = setInterval(() => {
const user = {
id: String(Math.random()),
name: 'User',
email: 'user@example.com',
created_at: Date.now()
};
call.write(user);
}, 1000);
call.on('cancelled', () => {
clearInterval(interval);
});
}
Client-Side Streaming
rpc CreateUsers(stream CreateUserRequest) returns (CreateUsersResponse);
function createUsers(call, callback) {
const users = [];
call.on('data', (request) => {
const user = {
id: String(users.length + 1),
name: request.name,
email: request.email,
created_at: Date.now()
};
users.push(user);
});
call.on('end', () => {
callback(null, { users, count: users.length });
});
}
Bidirectional Streaming
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
function chat(call) {
call.on('data', (message) => {
// Echo message back
call.write({
id: message.id,
text: `Echo: ${message.text}`,
timestamp: Date.now()
});
});
call.on('end', () => {
call.end();
});
}
Error Handling
function getUser(call, callback) {
const user = users.find(u => u.id === call.request.user_id);
if (!user) {
return callback({
code: grpc.status.NOT_FOUND,
message: 'User not found',
details: `User ID: ${call.request.user_id}`
});
}
callback(null, user);
}
// Client-side error handling
client.getUser({ user_id: '999' }, (error, user) => {
if (error) {
if (error.code === grpc.status.NOT_FOUND) {
console.log('User not found');
} else {
console.error('Error:', error.message);
}
return;
}
console.log('User:', user);
});
Interceptors
Server Interceptor
const interceptor = (options, nextCall) => {
return new grpc.InterceptingCall(nextCall(options), {
start: function(metadata, listener, next) {
// Add metadata
metadata.add('request-id', generateRequestId());
// Log request
console.log('Request:', metadata.getMap());
next(metadata, listener);
},
sendMessage: function(message, next) {
console.log('Sending:', message);
next(message);
},
recvMessage: function(message, next) {
console.log('Receiving:', message);
next(message);
}
});
};
const server = new grpc.Server();
server.use(interceptor);
Client Interceptor
const interceptor = (options, nextCall) => {
return new grpc.InterceptingCall(nextCall(options), {
start: function(metadata, listener, next) {
metadata.add('client-version', '1.0.0');
next(metadata, listener);
}
});
};
const client = new userProto.UserService(
'localhost:50051',
grpc.credentials.createInsecure(),
{ interceptors: [interceptor] }
);
Best Practices
- Use Protocol Buffers - Efficient serialization
- Handle errors properly - Use gRPC status codes
- Use streaming - For large datasets
- Implement timeouts - Prevent hanging calls
- Use interceptors - For logging/auth
- Version services - Support multiple versions
- Monitor performance - Track latency/errors
- Use connection pooling - Reuse connections
Conclusion
gRPC provides:
- High performance
- Type safety
- Streaming support
- Language interoperability
Use gRPC for inter-service communication in microservices. The patterns shown here handle production workloads.
gRPC high-performance RPC from April 2019, covering gRPC 1.20+ features.