Back to Blog

gRPC vs REST: Protobuf, Bidirectional Streaming, Performance

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
12 min read
Updated 2026

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 1000+ 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"
  }
}

grpc - gRPC vs REST: Protobuf, Bidirectional Streaming, Performance

🚀 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

Viprasol in Action

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.


More on This Topic

gRPC vs Fastify: Choosing the Right Backend Stack

The grpc vs fastify question usually comes up when a team has settled on Node.js and is weighing a high-performance REST framework against a contract-first RPC approach. Fastify is a fast HTTP server built around JSON and schema validation, ideal for public APIs, browser clients, and webhooks where human-readable payloads and easy debugging matter. gRPC, by contrast, uses Protobuf binary serialization and HTTP/2 multiplexing, making it stronger for internal service-to-service traffic, low-latency calls, and bidirectional streaming. They are not mutually exclusive: many systems expose a Fastify REST edge for external consumers while running gRPC between microservices behind it. At Viprasol, our senior engineers take full ownership of these decisions, matching transport, schema strategy, and streaming model to your real traffic patterns rather than to hype.

grpcrestapitypescriptmicroservices
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 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.

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.