gRPC vs REST: Protobuf, Bidirectional Streaming, Performance, and When to Use Each
Compare gRPC and REST in 2026 — Protocol Buffers schema definition, generated TypeScript clients, gRPC streaming types, performance benchmarks, gRPC-Web for bro
gRPC vs REST: Protobuf, Bidirectional Streaming, Performance, and When to Use Each
gRPC is not a REST replacement. It's a different tool for a different job: strongly-typed service-to-service communication with generated clients, efficient binary serialization, and native streaming. REST remains the right choice for public APIs and browser clients.
When to Choose Each
| Scenario | REST | gRPC |
|---|---|---|
| Public API | ✅ Universal client support | ❌ Requires generated clients |
| Browser clients | ✅ Native fetch | ⚠️ Needs gRPC-Web or Connect |
| Service-to-service | ✅ Simple, works everywhere | ✅ Better for high-throughput |
| Streaming data | ⚠️ SSE (server) or WebSockets (bi-di) | ✅ Native streaming types |
| Strong typing | ⚠️ OpenAPI + codegen | ✅ Protobuf schema |
| Legacy integration | ✅ Works with anything | ❌ Requires gRPC support |
| Mobile clients | ✅ Simple | ✅ Efficient on bandwidth |
| Human-readable debugging | ✅ JSON is readable | ❌ Binary (need grpcurl) |
The practical rule: Internal microservice communication → gRPC. Public API or browser → REST. Complex streaming needs → gRPC.
Protocol Buffers: Schema Definition
// proto/orders/v1/orders.proto
syntax = "proto3";
package orders.v1;
option go_package = "github.com/yourorg/proto/orders/v1";
import "google/protobuf/timestamp.proto";
// Enum for order status
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0; // Always include 0 as the default/unknown
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PROCESSING = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_DELIVERED = 4;
ORDER_STATUS_CANCELLED = 5;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
int64 price_cents = 3;
}
message Order {
string id = 1;
string user_id = 2;
OrderStatus status = 3;
repeated OrderItem items = 4;
int64 total_cents = 5;
google.protobuf.Timestamp created_at = 6;
google.protobuf.Timestamp updated_at = 7;
}
// Request/Response messages
message GetOrderRequest {
string order_id = 1;
}
message GetOrderResponse {
Order order = 1;
}
message ListOrdersRequest {
string user_id = 1;
OrderStatus status_filter = 2; // 0 = no filter
int32 page_size = 3;
string page_token = 4; // Cursor for pagination
}
message ListOrdersResponse {
repeated Order orders = 1;
string next_page_token = 2;
}
message WatchOrderRequest {
string order_id = 1;
}
// Service definition with all four RPC types
service OrderService {
// Unary: single request, single response
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
// Server streaming: single request, stream of responses
// Client subscribes to live order updates
rpc WatchOrder(WatchOrderRequest) returns (stream Order);
// Client streaming: stream of requests, single response
// Batch create orders
rpc BatchCreateOrders(stream Order) returns (ListOrdersResponse);
// Bidirectional streaming: both sides stream simultaneously
// Real-time order management session
rpc ManageOrders(stream GetOrderRequest) returns (stream Order);
}
🌐 Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Fixed-price contracts — no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
TypeScript Server Implementation (Connect-ES)
Connect is the modern way to use gRPC from TypeScript — it replaces the heavyweight @grpc/grpc-js with a simpler, web-compatible protocol:
# Install dependencies
pnpm add @connectrpc/connect @connectrpc/connect-node
pnpm add -D @bufbuild/buf @connectrpc/protoc-gen-connect-es @bufbuild/protoc-gen-es
# buf.gen.yaml — code generation config
version: v1
plugins:
- plugin: es
out: src/gen
opt: target=ts
- plugin: connect-es
out: src/gen
opt: target=ts
// src/services/order-service.ts
import type { ConnectRouter } from '@connectrpc/connect';
import { OrderService } from '../gen/orders/v1/orders_connect';
import type {
GetOrderRequest,
ListOrdersRequest,
WatchOrderRequest,
} from '../gen/orders/v1/orders_pb';
import { Order, GetOrderResponse, ListOrdersResponse } from '../gen/orders/v1/orders_pb';
import { Timestamp } from '@bufbuild/protobuf/wkt';
export function ordersRouter(router: ConnectRouter) {
router.service(OrderService, {
// Unary RPC
async getOrder(req: GetOrderRequest) {
const order = await db.orders.findUnique({ where: { id: req.orderId } });
if (!order) throw new ConnectError('Order not found', Code.NotFound);
return new GetOrderResponse({
order: new Order({
id: order.id,
userId: order.userId,
status: mapStatus(order.status),
totalCents: BigInt(order.totalCents),
createdAt: Timestamp.fromDate(order.createdAt),
items: order.items.map(i => new OrderItem({
productId: i.productId,
quantity: i.quantity,
priceCents: BigInt(i.priceCents),
})),
}),
});
},
// Server streaming RPC: yield updates as they happen
async *watchOrder(req: WatchOrderRequest) {
const orderId = req.orderId;
// Send current state immediately
const initial = await db.orders.findUnique({ where: { id: orderId } });
if (initial) yield mapToProto(initial);
// Stream subsequent updates
for await (const update of watchOrderUpdates(orderId)) {
yield mapToProto(update);
}
},
async listOrders(req: ListOrdersRequest) {
const orders = await db.orders.findMany({
where: {
userId: req.userId,
...(req.statusFilter ? { status: mapStatusBack(req.statusFilter) } : {}),
},
take: req.pageSize || 20,
// cursor-based pagination using pageToken
});
return new ListOrdersResponse({
orders: orders.map(mapToProto),
nextPageToken: orders.length === (req.pageSize || 20) ? orders[orders.length - 1].id : '',
});
},
});
}
// src/server.ts
import { fastify } from 'fastify';
import { fastifyConnectPlugin } from '@connectrpc/connect-fastify';
import { ordersRouter } from './services/order-service';
const app = fastify({ http2: true }); // gRPC requires HTTP/2
await app.register(fastifyConnectPlugin, {
routes: ordersRouter,
});
await app.listen({ port: 50051 });
TypeScript Client
// src/clients/orders.ts
import { createClient } from '@connectrpc/connect';
import { createConnectTransport } from '@connectrpc/connect-node';
import { OrderService } from '../gen/orders/v1/orders_connect';
const transport = createConnectTransport({
baseUrl: 'http://orders-service:50051',
httpVersion: '2',
});
export const ordersClient = createClient(OrderService, transport);
// Unary call
const response = await ordersClient.getOrder({ orderId: 'order-123' });
console.log(response.order?.status);
// Server streaming
const stream = ordersClient.watchOrder({ orderId: 'order-123' });
for await (const update of stream) {
console.log('Order updated:', update.status);
}
// Error handling (gRPC status codes)
try {
await ordersClient.getOrder({ orderId: 'nonexistent' });
} catch (err) {
if (err instanceof ConnectError) {
console.log(err.code); // Code.NotFound
console.log(err.message); // "Order not found"
}
}
🚀 Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4–8 weeks, full platforms in 3–5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
gRPC-Web for Browser Clients
Browsers can't make native HTTP/2 gRPC calls. gRPC-Web or Connect bridges this:
// Browser client — same API, different transport
import { createConnectTransport } from '@connectrpc/connect-web'; // Web transport
import { createClient } from '@connectrpc/connect';
// Connect protocol works in browsers without a proxy
const webTransport = createConnectTransport({
baseUrl: 'https://api.yourproduct.com',
// Connect uses HTTP/1.1 framing — works everywhere
});
export const ordersClient = createClient(OrderService, webTransport);
// Usage is identical to Node.js client
const response = await ordersClient.getOrder({ orderId: 'order-123' });
Performance Comparison
Benchmarks (100K requests, 1KB payload, single connection):
| Protocol | Serialization | Throughput | Latency (p99) |
|---|---|---|---|
| REST + JSON | JSON encode/decode | ~20K req/s | 12ms |
| gRPC (Connect) + Protobuf | Protobuf binary | ~45K req/s | 6ms |
| gRPC (raw) + Protobuf | Protobuf binary | ~60K req/s | 4ms |
Protobuf vs JSON payload size:
JSON: {"id":"order-123","status":"PENDING","totalCents":4999} → 57 bytes
Protobuf: field tags + varint encoding → 14 bytes (75% smaller)
At scale, the CPU and bandwidth savings add up. For internal services doing millions of calls/day, gRPC's efficiency is meaningful.
Reflection and Debugging
# grpcurl — command-line gRPC client (like curl for REST)
brew install grpcurl
# List services
grpcurl -plaintext localhost:50051 list
# Describe a service
grpcurl -plaintext localhost:50051 describe orders.v1.OrderService
# Call a method
grpcurl -plaintext -d '{"order_id": "order-123"}' \
localhost:50051 orders.v1.OrderService/GetOrder
# Server streaming
grpcurl -plaintext -d '{"order_id": "order-123"}' \
localhost:50051 orders.v1.OrderService/WatchOrder
Working With Viprasol
We design and implement gRPC service architectures — Protobuf schema design, Connect-ES TypeScript implementation, browser-compatible gRPC-Web setup, and service mesh configuration for gRPC in Kubernetes.
→ Talk to our team about API architecture and microservices.
See Also
- API Gateway Comparison — routing gRPC through Kong and Envoy
- GraphQL Federation — another typed API alternative
- OpenAPI Design — REST API design with strong typing
- Contract Testing — testing gRPC service contracts
- Web Development Services — API architecture and development
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms — we build it all with Next.js and React.
Free consultation • No commitment • Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.