gRPC — When & Why to Use It
What is gRPC?
gRPC (Google Remote Procedure Call) is a high-performance, open-source framework that lets one service call a function on another service — even if they run on different machines — as if it were a local function call.
Think of it as a super-charged alternative to REST APIs. Instead of sending JSON over HTTP, gRPC:
- Uses Protocol Buffers (Protobuf) — a compact binary format (much smaller than JSON)
- Runs over HTTP/2 — enabling multiplexing, streaming, and lower latency
- Auto-generates client & server code in many languages from a schema file (
.proto)
REST vs gRPC — The Core Difference
| Feature | REST | gRPC |
|---|---|---|
| Protocol | HTTP/1.1 | HTTP/2 |
| Data Format | JSON (text) | Protobuf (binary) |
| Contract | None (implicit) | .proto schema (strict) |
| Code Gen | Manual | Automatic |
| Streaming | ❌ (workaround) | ✅ Native |
| Browser Support | ✅ Native | ⚠️ Needs proxy (grpc-web) |
| Best for | Public APIs, CRUD | Internal services, real-time |
When Should You Use gRPC?
✅ USE gRPC when:
- Microservices talking to each other — low latency, high throughput internal communication
- Real-time streaming — live chat, stock tickers, video feeds, IoT sensor data
- Polyglot environments — your services use different languages (Node.js + Go + Java) — gRPC generates clients for all of them
- Strong contracts matter — you want compile-time errors if API shape changes, not runtime surprises
- Performance is critical — Protobuf payloads are 3–10× smaller than JSON
❌ AVOID gRPC when:
- Building public browser-facing APIs — browsers don't natively support gRPC (use REST or GraphQL instead)
- Your team is small and the overhead of
.protofiles isn't worth it - You need human-readable request/response (debugging gRPC is harder without tooling)
Why is Debugging gRPC Harder? (Human-Readable vs. Binary)
One of the main trade-offs when choosing gRPC over REST is observability during debugging.
1. REST (Human-Readable)
When you make a REST API call, the data is sent as JSON (JavaScript Object Notation), which is just plain text. If a request fails, you can open your browser's Network tab or use a tool like cURL, and immediately read exactly what was sent and received.
Example REST Payload (JSON):
{
"product_id": "SKU-789",
"quantity": 3,
"customer_id": "user-42"
}Anyone can read this and instantly spot if quantity was accidentally sent as a string instead of a number.
2. gRPC (Binary / Protobuf)
gRPC uses Protocol Buffers (Protobuf) to serialize data. Instead of sending keys and values as text, Protobuf strips out the keys and converts the values into a highly compressed stream of bytes based on the .proto schema.
Example gRPC Payload (Binary):
\x0A\x07SKU-789\x10\x03\x1A\x07user-42If you intercept this network traffic, it looks like gibberish. You cannot read it without a tool that knows the .proto schema to decode it.
Visualizing the Difference
The Solution: gRPC Tooling
To debug gRPC effectively, you cannot just use the standard browser network tab. You must use specialized tooling that can load your .proto files and translate the binary back into human-readable JSON for you:
- Postman / BloomRPC: GUI clients that support importing
.protofiles to make and debug gRPC requests visually. - grpcurl: A command-line tool (like
curlbut for gRPC) that can read protobuf schemas and decode responses in the terminal. - gRPC UI: A browser-based GUI specifically for inspecting gRPC APIs.
The 4 Types of gRPC Communication
How gRPC Works — Architecture
Hands-On Example in JavaScript (Node.js)
Let's build a simple Order Service that a client calls to place an order.
Step 1 — Install dependencies
npm install @grpc/grpc-js @grpc/proto-loaderStep 2 — Define the Contract (order.proto)
This is the schema file — the single source of truth for both client and server.
syntax = "proto3";
package order;
// The service definition
service OrderService {
// Unary RPC: client sends one request, gets one response
rpc PlaceOrder (OrderRequest) returns (OrderResponse);
// Server Streaming RPC: client sends one request, server streams responses
rpc TrackOrder (TrackRequest) returns (stream TrackUpdate);
}
// Request message shape
message OrderRequest {
string product_id = 1;
int32 quantity = 2;
string customer_id = 3;
}
// Response message shape
message OrderResponse {
string order_id = 1;
string status = 2;
string message = 3;
}
message TrackRequest {
string order_id = 1;
}
message TrackUpdate {
string order_id = 1;
string status = 2;
string location = 3;
}Step 3 — Build the Server (server.js)
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const path = require("path");
const { v4: uuidv4 } = require("uuid");
// Load the .proto definition
const packageDef = protoLoader.loadSync(path.join(__dirname, "order.proto"), {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const orderProto = grpc.loadPackageDefinition(packageDef).order;
// --- Implement the service methods ---
/**
* Unary RPC: PlaceOrder
* Client sends one request → Server sends one response
*/
function placeOrder(call, callback) {
const { product_id, quantity, customer_id } = call.request;
console.log(
`📦 New order from customer ${customer_id}: ${quantity}x ${product_id}`
);
// Simulate order processing
const orderId = uuidv4();
// callback(error, response)
callback(null, {
order_id: orderId,
status: "CONFIRMED",
message: `Order placed successfully! ID: ${orderId}`,
});
}
/**
* Server Streaming RPC: TrackOrder
* Client sends one request → Server streams multiple updates
*/
function trackOrder(call) {
const { order_id } = call.request;
console.log(`🔍 Tracking order: ${order_id}`);
const updates = [
{ status: "PROCESSING", location: "Warehouse" },
{ status: "SHIPPED", location: "Distribution Center" },
{ status: "OUT_FOR_DELIVERY", location: "Local Hub" },
{ status: "DELIVERED", location: "Customer Door" },
];
let i = 0;
const interval = setInterval(() => {
if (i >= updates.length) {
clearInterval(interval);
call.end(); // Signal end of stream
return;
}
// call.write() sends a chunk to the client
call.write({ order_id, ...updates[i] });
i++;
}, 1000); // Send update every second
}
// --- Start the gRPC server ---
function main() {
const server = new grpc.Server();
server.addService(orderProto.OrderService.service, {
PlaceOrder: placeOrder,
TrackOrder: trackOrder,
});
const address = "0.0.0.0:50051";
server.bindAsync(
address,
grpc.ServerCredentials.createInsecure(),
(err, port) => {
if (err) throw err;
console.log(`🚀 gRPC Server running on port ${port}`);
}
);
}
main();Step 4 — Build the Client (client.js)
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const path = require("path");
// Load the same .proto file
const packageDef = protoLoader.loadSync(path.join(__dirname, "order.proto"), {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const orderProto = grpc.loadPackageDefinition(packageDef).order;
// Create a client stub pointing at the server
const client = new orderProto.OrderService(
"localhost:50051",
grpc.credentials.createInsecure()
);
// --- 1. Unary call: PlaceOrder ---
function placeOrder() {
const request = {
product_id: "SKU-789",
quantity: 3,
customer_id: "user-42",
};
console.log("📤 Placing order...");
// client.PlaceOrder(request, callback)
client.PlaceOrder(request, (error, response) => {
if (error) {
console.error("❌ Error:", error.message);
return;
}
console.log("✅ Order placed!", response);
// Now track the placed order using streaming
trackOrder(response.order_id);
});
}
// --- 2. Server Streaming call: TrackOrder ---
function trackOrder(orderId) {
console.log(`\n📡 Tracking order ${orderId} (live stream)...`);
// client.TrackOrder returns a readable stream
const stream = client.TrackOrder({ order_id: orderId });
// Listen for streamed messages
stream.on("data", (update) => {
console.log(`📍 [${update.status}] — ${update.location}`);
});
stream.on("end", () => {
console.log("✅ Order tracking complete!");
});
stream.on("error", (err) => {
console.error("❌ Stream error:", err.message);
});
}
placeOrder();Step 5 — Run It
# Terminal 1 — Start the server
node server.js
# Terminal 2 — Run the client
node client.jsExpected output on client:
📤 Placing order...
✅ Order placed! { order_id: 'abc-123', status: 'CONFIRMED', message: 'Order placed successfully! ID: abc-123' }
📡 Tracking order abc-123 (live stream)...
📍 [PROCESSING] — Warehouse
📍 [SHIPPED] — Distribution Center
📍 [OUT_FOR_DELIVERY] — Local Hub
📍 [DELIVERED] — Customer Door
✅ Order tracking complete!Full Architecture in a Microservices Setup
Key insight: The public-facing layer (browser/mobile) uses REST. Once inside your infrastructure, all service-to-service calls use gRPC for speed and type safety.
Error Handling in gRPC
gRPC has its own status codes (similar to HTTP status codes):
const grpc = require("@grpc/grpc-js");
// On the SERVER — return a structured error
function getProduct(call, callback) {
const { product_id } = call.request;
if (!product_id) {
// Return a gRPC error with status code
return callback({
code: grpc.status.INVALID_ARGUMENT, // Like HTTP 400
message: "product_id is required",
});
}
const product = db.find(product_id);
if (!product) {
return callback({
code: grpc.status.NOT_FOUND, // Like HTTP 404
message: `Product ${product_id} not found`,
});
}
callback(null, product);
}
// On the CLIENT — handle errors
client.GetProduct({ product_id: "XYZ" }, (error, response) => {
if (error) {
if (error.code === grpc.status.NOT_FOUND) {
console.log("Product does not exist");
} else if (error.code === grpc.status.UNAVAILABLE) {
console.log("Service is down, retry later");
}
return;
}
console.log("Product:", response);
});Common gRPC Status Codes
| gRPC Status | HTTP Equivalent | Meaning |
|---|---|---|
OK | 200 | Success |
INVALID_ARGUMENT | 400 | Bad input |
NOT_FOUND | 404 | Resource missing |
ALREADY_EXISTS | 409 | Duplicate |
PERMISSION_DENIED | 403 | No access |
UNAUTHENTICATED | 401 | Not logged in |
UNAVAILABLE | 503 | Service down |
INTERNAL | 500 | Server error |
DEADLINE_EXCEEDED | 504 | Timeout |
Decision Guide — REST or gRPC?
Summary
| Details | |
|---|---|
| What | A framework for calling remote functions as if they're local |
| Why | Faster (binary Protobuf), strongly typed, supports streaming |
| When | Microservice-to-service communication, real-time, polyglot systems |
| When NOT | Public browser APIs, simple CRUD, small single-service apps |
| Key files | .proto (schema), generated stubs (client & server code) |
| Protocol | HTTP/2 + Protocol Buffers |
