Back to Blog

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

Viprasol Tech Team
June 27, 2026
12 min read

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

ScenarioRESTgRPC
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):

ProtocolSerializationThroughputLatency (p99)
REST + JSONJSON encode/decode~20K req/s12ms
gRPC (Connect) + ProtobufProtobuf binary~45K req/s6ms
gRPC (raw) + ProtobufProtobuf binary~60K req/s4ms

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

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

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

Viprasol · Web Development

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.