Setup and context — Why gRPC Matters in 2026
In 2026, microservice architectures are the standard for building scalable systems. As the number of services grows, the efficiency of inter-service communication becomes critical to overall system performance. While REST APIs are intuitive and widely adopted, they introduce challenges at scale: JSON serialization overhead, schema ambiguity, and the difficulty of implementing bidirectional communication.
gRPC is an open-source RPC framework developed by Google that addresses these pain points head-on. It leverages Protocol Buffers (protobuf) for binary serialization, HTTP/2 for multiplexed communication, and strict type definitions for contract-driven development. In this guide, we'll walk through how to design and implement production-ready gRPC microservices using Antigravity's AI agents as your development partner.
This guide is intended for mid-to-senior engineers with REST API experience who want to build more efficient inter-service communication layers.
Schema-Driven Development with Protocol Buffers
The Philosophy Behind .proto Files
The greatest strength of gRPC is that .proto files serve as the Single Source of Truth, enabling automatic code generation for both server and client. With Antigravity's agents, you can generate schemas from requirements and get type-safe code instantly.
// proto/product_service.proto
// Product microservice schema definition
syntax = "proto3";
package ecommerce.product.v1;
option go_package = "github.com/example/ecommerce/gen/product/v1";
import "google/protobuf/timestamp.proto";
import "google/protobuf/field_mask.proto";
// Product service RPC definitions
service ProductService {
// Get a single product (Unary RPC)
rpc GetProduct(GetProductRequest) returns (Product);
// Search products (Server Streaming RPC)
rpc SearchProducts(SearchProductsRequest) returns (stream Product);
// Batch create products (Client Streaming RPC)
rpc BatchCreateProducts(stream CreateProductRequest) returns (BatchCreateResponse);
// Monitor inventory in real-time (Bidirectional Streaming RPC)
rpc WatchInventory(stream InventoryQuery) returns (stream InventoryUpdate);
}
message Product {
string id = 1;
string name = 2;
string description = 3;
int64 price_cents = 4; // Store prices in cents
Currency currency = 5;
repeated string tags = 6;
ProductStatus status = 7;
google.protobuf.Timestamp created_at = 8;
google.protobuf.Timestamp updated_at = 9;
}
enum Currency {
CURRENCY_UNSPECIFIED = 0;
CURRENCY_JPY = 1;
CURRENCY_USD = 2;
CURRENCY_EUR = 3;
}
enum ProductStatus {
PRODUCT_STATUS_UNSPECIFIED = 0;
PRODUCT_STATUS_DRAFT = 1;
PRODUCT_STATUS_ACTIVE = 2;
PRODUCT_STATUS_ARCHIVED = 3;
}
message GetProductRequest {
string id = 1;
}
message SearchProductsRequest {
string query = 1;
repeated string tags = 2;
int32 page_size = 3; // Max 100
string page_token = 4; // Cursor-based pagination
}
message CreateProductRequest {
string name = 1;
string description = 2;
int64 price_cents = 3;
Currency currency = 4;
repeated string tags = 5;
}
message BatchCreateResponse {
int32 created_count = 1;
repeated string product_ids = 2;
}
message InventoryQuery {
repeated string product_ids = 1;
}
message InventoryUpdate {
string product_id = 1;
int32 quantity = 2;
google.protobuf.Timestamp timestamp = 3;
}Auto-Generating Schemas with Antigravity
Antigravity's agents can generate well-structured schemas from plain language requirements. The key is to provide clear context in your prompt:
- Domain models: What entities exist in your system
- RPC patterns: Which communication styles each endpoint requires
- Versioning strategy: Whether to include
v1in package names - Naming conventions: Adherence to Google's API Design Guide
# Run from Antigravity's terminal
# AI agent generates schema from domain requirements
antigravity agent "Generate a gRPC schema with these requirements:
- E-commerce product management service
- CRUD + search + real-time inventory monitoring
- Follow Google API Design Guide conventions
- Include go_package option"Mastering the Four RPC Patterns
gRPC provides four distinct communication patterns. Understanding when to use each one is the key to efficient API design.
Unary RPC — The Request/Response Baseline
The most basic pattern, equivalent to REST's GET/POST. One request yields one response.
// server/product-service.ts
// Unary RPC: Get a single product
import * as grpc from "@grpc/grpc-js";
import { ProductServiceServer } from "../gen/product_service_grpc_pb";
import { Product, GetProductRequest } from "../gen/product_service_pb";
const getProduct: grpc.handleUnaryCall<GetProductRequest, Product> =
async (call, callback) => {
const productId = call.request.getId();
try {
// Fetch product from database
const product = await db.products.findUnique({
where: { id: productId }
});
if (!product) {
// Use standard gRPC error codes
callback({
code: grpc.status.NOT_FOUND,
message: `Product ${productId} not found`
});
return;
}
const response = new Product();
response.setId(product.id);
response.setName(product.name);
response.setPriceCents(product.priceCents);
callback(null, response);
} catch (error) {
callback({
code: grpc.status.INTERNAL,
message: "Internal server error"
});
}
};Server Streaming RPC — Efficient Large Dataset Delivery
The server sends a stream of responses to the client. Ideal for search results, log tailing, and data exports.
// server/product-service.ts
// Server Streaming RPC: Stream search results to client
const searchProducts: grpc.handleServerStreamingCall<
SearchProductsRequest, Product
> = async (call) => {
const query = call.request.getQuery();
const tags = call.request.getTagsList();
const pageSize = call.request.getPageSize() || 20;
try {
// Cursor-based incremental fetch and delivery
let cursor: string | undefined;
let sent = 0;
while (sent < pageSize) {
const batch = await db.products.findMany({
where: {
OR: [
{ name: { contains: query } },
{ tags: { hasSome: tags } }
]
},
take: Math.min(10, pageSize - sent),
cursor: cursor ? { id: cursor } : undefined,
skip: cursor ? 1 : 0,
orderBy: { createdAt: "desc" }
});
if (batch.length === 0) break;
for (const item of batch) {
const product = new Product();
product.setId(item.id);
product.setName(item.name);
product.setPriceCents(item.priceCents);
// Write one item at a time to the stream
call.write(product);
sent++;
}
cursor = batch[batch.length - 1].id;
}
// End the stream
call.end();
} catch (error) {
call.destroy(new Error("Search failed"));
}
};Bidirectional Streaming RPC — Real-Time Two-Way Communication
Both client and server send streams simultaneously. This is the type-safe alternative to WebSockets for real-time communication.
// server/product-service.ts
// Bidirectional Streaming: Real-time inventory monitoring
const watchInventory: grpc.handleBidiStreamingCall<
InventoryQuery, InventoryUpdate
> = (call) => {
const watchedProducts = new Set<string>();
// Receive watch requests from the client
call.on("data", (request: InventoryQuery) => {
const productIds = request.getProductIdsList();
productIds.forEach(id => watchedProducts.add(id));
console.log(`Watching ${watchedProducts.size} products`);
});
// Poll for inventory changes and push updates
const interval = setInterval(async () => {
for (const productId of watchedProducts) {
const inventory = await db.inventory.findUnique({
where: { productId }
});
if (inventory?.lastChanged) {
const update = new InventoryUpdate();
update.setProductId(productId);
update.setQuantity(inventory.quantity);
call.write(update);
}
}
}, 1000);
call.on("end", () => {
clearInterval(interval);
call.end();
});
call.on("error", () => {
clearInterval(interval);
});
};Authentication and Security
Production gRPC services require robust authentication and authorization. gRPC interceptors (middleware) provide a clean way to implement cross-cutting security concerns.
JWT Authentication Interceptor
// server/interceptors/auth.ts
// gRPC authentication interceptor
import * as grpc from "@grpc/grpc-js";
import * as jwt from "jsonwebtoken";
interface AuthenticatedCall extends grpc.ServerUnaryCall<any, any> {
user?: { id: string; role: string };
}
// Methods that don't require authentication
const PUBLIC_METHODS = [
"/ecommerce.product.v1.ProductService/SearchProducts",
];
export function authInterceptor(
methodDescriptor: grpc.MethodDefinition<any, any>,
call: grpc.ServerUnaryCall<any, any>,
callback: grpc.sendUnaryData<any>,
next: Function
) {
const method = call.getPath();
// Skip authentication for public methods
if (PUBLIC_METHODS.includes(method)) {
return next(call, callback);
}
// Extract token from metadata
const metadata = call.metadata.get("authorization");
const token = metadata[0]?.toString().replace("Bearer ", "");
if (!token) {
callback({
code: grpc.status.UNAUTHENTICATED,
message: "Missing authentication token"
});
return;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as {
sub: string;
role: string;
};
(call as AuthenticatedCall).user = {
id: decoded.sub,
role: decoded.role
};
next(call, callback);
} catch (error) {
callback({
code: grpc.status.UNAUTHENTICATED,
message: "Invalid or expired token"
});
}
}Mutual TLS (mTLS) Configuration
For service-to-service communication, mTLS encrypts and authenticates the transport layer itself, in addition to JWT-based application-layer auth.
// server/tls-config.ts
// mTLS configuration for mutual service authentication
import * as grpc from "@grpc/grpc-js";
import * as fs from "fs";
export function createSecureCredentials(): grpc.ServerCredentials {
// Load certificate files
const rootCert = fs.readFileSync("certs/ca.pem");
const serverCert = fs.readFileSync("certs/server.pem");
const serverKey = fs.readFileSync("certs/server-key.pem");
return grpc.ServerCredentials.createSsl(
rootCert,
[{
cert_chain: serverCert,
private_key: serverKey,
}],
true // Require client certificates (mTLS)
);
}
// Apply when starting the server
// const creds = createSecureCredentials();
// server.bindAsync("0.0.0.0:50051", creds, callback);Error Handling Best Practices
gRPC has its own status code system that differs from HTTP status codes. Proper mapping is essential for clear error communication.
gRPC Status Code Reference
- OK (0): Successful completion
- INVALID_ARGUMENT (3): Validation error (REST 400)
- NOT_FOUND (5): Resource doesn't exist (REST 404)
- ALREADY_EXISTS (6): Duplicate creation (REST 409)
- PERMISSION_DENIED (7): Insufficient permissions (REST 403)
- UNAUTHENTICATED (16): Authentication failure (REST 401)
- RESOURCE_EXHAUSTED (8): Rate limit exceeded (REST 429)
- INTERNAL (13): Server error (REST 500)
- UNAVAILABLE (14): Temporary outage (REST 503)
Structured Error Responses
// server/errors/grpc-errors.ts
// Utility for returning structured error responses
import * as grpc from "@grpc/grpc-js";
import { Status } from "@grpc/grpc-js/build/src/constants";
interface FieldViolation {
field: string;
description: string;
}
export function validationError(
violations: FieldViolation[]
): Partial<grpc.StatusObject> {
// Attach error details to metadata
const metadata = new grpc.Metadata();
metadata.set(
"errors-bin",
Buffer.from(JSON.stringify(violations))
);
return {
code: Status.INVALID_ARGUMENT,
message: `Validation failed: ${violations.map(v => v.field).join(", ")}`,
metadata
};
}
// Usage example:
// callback(validationError([
// { field: "name", description: "Name is required" },
// { field: "price_cents", description: "Price must be positive" }
// ]));Health Checks and Graceful Shutdown
To integrate with orchestrators like Kubernetes, implement gRPC's standard health checking protocol.
// server/health.ts
// gRPC Health Checking Protocol implementation
import * as grpc from "@grpc/grpc-js";
import {
HealthCheckRequest,
HealthCheckResponse,
ServingStatus,
} from "grpc-health-check";
export class HealthService {
private statusMap = new Map<string, ServingStatus>();
constructor() {
// Set initial status to SERVING for all services
this.statusMap.set("", ServingStatus.SERVING);
this.statusMap.set(
"ecommerce.product.v1.ProductService",
ServingStatus.SERVING
);
}
// Unary health check (for Kubernetes liveness/readiness probes)
check(
call: grpc.ServerUnaryCall<HealthCheckRequest, HealthCheckResponse>,
callback: grpc.sendUnaryData<HealthCheckResponse>
) {
const service = call.request.getService();
const status = this.statusMap.get(service);
if (status === undefined) {
callback({
code: grpc.status.NOT_FOUND,
message: `Unknown service: ${service}`
});
return;
}
const response = new HealthCheckResponse();
response.setStatus(status);
callback(null, response);
}
// Update service status
setStatus(service: string, status: ServingStatus) {
this.statusMap.set(service, status);
}
}
// Graceful shutdown handler
export function setupGracefulShutdown(server: grpc.Server) {
const shutdown = () => {
console.log("Received shutdown signal, draining connections...");
// Stop accepting new connections
server.tryShutdown((error) => {
if (error) {
console.error("Graceful shutdown failed:", error);
server.forceShutdown();
}
console.log("Server shut down gracefully");
process.exit(0);
});
// Force shutdown after 30 seconds
setTimeout(() => {
console.error("Forced shutdown after timeout");
server.forceShutdown();
process.exit(1);
}, 30_000);
};
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
}Performance Optimization and Benchmarking
Here are practical tuning techniques to maximize gRPC performance.
Connection Pooling and Channel Configuration
// client/channel-config.ts
// Optimized gRPC client configuration
import * as grpc from "@grpc/grpc-js";
export function createOptimizedChannel(
address: string
): grpc.Channel {
const options: grpc.ChannelOptions = {
// Keep-alive settings (for load balancer compatibility)
"grpc.keepalive_time_ms": 10_000,
"grpc.keepalive_timeout_ms": 5_000,
"grpc.keepalive_permit_without_calls": 1,
// Message size limits (default 4MB -> 16MB)
"grpc.max_receive_message_length": 16 * 1024 * 1024,
"grpc.max_send_message_length": 16 * 1024 * 1024,
// Initial window size (improves throughput)
"grpc.http2.min_time_between_pings_ms": 10_000,
// DNS round-robin (multi-backend support)
"grpc.service_config": JSON.stringify({
loadBalancingConfig: [{ round_robin: {} }],
methodConfig: [{
name: [{}],
retryPolicy: {
maxAttempts: 3,
initialBackoff: "0.1s",
maxBackoff: "1s",
backoffMultiplier: 2,
retryableStatusCodes: [
"UNAVAILABLE",
"DEADLINE_EXCEEDED"
]
}
}]
})
};
return new grpc.Channel(
address,
grpc.credentials.createInsecure(),
options
);
}Benchmarking with ghz
# ghz — dedicated gRPC benchmarking tool
# Install: brew install ghz
# Benchmark Unary RPC (1000 requests, 50 concurrency)
ghz --insecure \
--proto proto/product_service.proto \
--call ecommerce.product.v1.ProductService/GetProduct \
--data '{"id": "prod_001"}' \
--total 1000 \
--concurrency 50 \
localhost:50051
# Expected output:
# Summary:
# Count: 1000
# Total: 1.23 s
# Slowest: 45.12 ms
# Fastest: 1.05 ms
# Average: 5.67 ms
# Requests/sec: 813.01The Antigravity × gRPC Development Workflow
Here's an efficient workflow for gRPC development powered by Antigravity's AI agents.
Step 1: Schema Generation
Describe your requirements in Antigravity's chat, and the AI analyzes your domain model to generate .proto files following Google's API Design Guide. Defining gRPC coding conventions in your AGENTS.md ensures consistent schema output.
Step 2: Automated Code Generation
# Code generation with buf (recommended protoc alternative)
# Let the AI agent generate your buf.gen.yaml configuration
# buf.gen.yaml
# version: v2
# plugins:
# - remote: buf.build/grpc/node
# out: gen
# opt: grpc_js
# - remote: buf.build/protocolbuffers/js
# out: gen
# opt: import_style=commonjs,binary
buf generate proto/Step 3: Auto-Generated Tests
Ask Antigravity's agent to generate test code, and it creates tests for each RPC pattern. Streaming RPC tests are particularly complex to write by hand, making AI assistance especially valuable.
// test/product-service.test.ts
// gRPC service integration test example
import * as grpc from "@grpc/grpc-js";
import { expect } from "chai";
describe("ProductService", () => {
let client: any;
before(() => {
// Initialize test client
client = new ProductServiceClient(
"localhost:50051",
grpc.credentials.createInsecure()
);
});
it("should return a product by ID", (done) => {
const request = new GetProductRequest();
request.setId("prod_001");
client.getProduct(request, (error: any, response: any) => {
expect(error).to.be.null;
expect(response.getId()).to.equal("prod_001");
expect(response.getName()).to.be.a("string");
done();
});
});
it("should return NOT_FOUND for unknown product", (done) => {
const request = new GetProductRequest();
request.setId("nonexistent");
client.getProduct(request, (error: any) => {
expect(error.code).to.equal(grpc.status.NOT_FOUND);
done();
});
});
});Summary
gRPC with Protocol Buffers delivers type safety, performance, and developer efficiency at a level that REST simply can't match for inter-service communication. By leveraging Antigravity's AI agents, you can dramatically accelerate the development cycle from schema design through code generation, testing, and deployment.
Start with a small proof-of-concept by migrating one existing REST service to gRPC, and build your team's expertise incrementally. For related infrastructure setup, check out our Kubernetes Container Orchestration Guide. For API security patterns, see Building Secure API Backends. And for schema-driven API design approaches, our OpenAPI Spec API Design Automation guide is a great companion read.