Introduction

Real-time communication is essential for modern applications--from chat and live dashboards to multiplayer gaming and financial trading platforms. gRPC and WebSocket represent two fundamentally different approaches to bidirectional streaming. gRPC builds on HTTP/2 with Protocol Buffers for typed, efficient RPCs. WebSocket provides a message-oriented protocol over a single TCP connection. This article compares them across the dimensions that matter for real-time application design.

gRPC vs WebSocket: Real-Time Communication

Protocol Fundamentals

gRPC: HTTP/2 + Protocol Buffers

gRPC leverages HTTP/2 multiplexing and Protocol Buffers for typed, efficient communication:

// order.proto

syntax = "proto3";

package order;

service OrderService {

// Unary RPC (request-response)

rpc CreateOrder(CreateOrderRequest) returns (Order);

// Server streaming (push events to client)

rpc SubscribeOrders(OrderFilter) returns (stream Order);

// Client streaming (upload batch)

rpc BulkCreateOrders(stream CreateOrderRequest) returns (BulkResponse);

// Bidirectional streaming (full duplex)

rpc ProcessOrders(stream OrderAction) returns (stream OrderResult);

}

message Order {

string id = 1;

string user_id = 2;

repeated LineItem items = 3;

double total = 4;

OrderStatus status = 5;

google.protobuf.Timestamp created_at = 6;

}

message CreateOrderRequest {

string user_id = 1;

repeated LineItem items = 2;

}

message OrderFilter {

repeated string statuses = 1;

}

message LineItem {

string product_id = 1;

int32 quantity = 2;

double price = 3;

}

enum OrderStatus {

PENDING = 0;

CONFIRMED = 1;

PROCESSING = 2;

SHIPPED = 3;

DELIVERED = 4;

CANCELLED = 5;

}

Server implementation in Go:

package main

import (

"context"

"log"

"net"

"google.golang.org/grpc"

pb "path/to/proto/order"

)

type orderServer struct {

pb.UnimplementedOrderServiceServer

}

// Bidirectional streaming

func (s *orderServer) ProcessOrders(

stream pb.OrderService_ProcessOrdersServer,

) error {

for {

action, err := stream.Recv()

if err != nil {

return err

}

// Process order action

result := &pb.OrderResult;{

OrderId: action.OrderId,

Status: pb.OrderStatus_PROCESSING,

Message: "Order received and processing",

}

if err := stream.Send(result); err != nil {

return err

}

}

}

func main() {

lis, _ := net.Listen("tcp", ":50051")

s := grpc.NewServer(

grpc.MaxRecvMsgSize(4 * 1024 * 1024), // 4MB

grpc.MaxSendMsgSize(4 * 1024 * 1024), // 4MB

grpc.InitialWindowSize(1<<31 - 1), // Flow control

grpc.InitialConnWindowSize(1<<31 - 1),

)

pb.RegisterOrderServiceServer(s, &orderServer;{})

log.Fatal(s.Serve(lis))

}

Client in Python:

import grpc

import order_pb2

import order_pb2_grpc

async def process_orders():

async with grpc.aio.insecure_channel('localhost:50051') as channel:

stub = order_pb2_grpc.OrderServiceStub(channel)

async def generate_actions():

for i in range(100):

yield order_pb2.OrderAction(

order_id=f"ord-{i}",

action="process",

payload=b"{}",

)

async for result in stub.ProcessOrders(generate_actions()):

print(f"Order {result.order_id}: {result.status}")

WebSocket: Message-Based Protocol

WebSocket provides a simpler, message-oriented protocol over TCP:

// WebSocket client (browser)

const ws = new WebSocket('wss://api.example.com/orders');

// Connection lifecycle

ws.onopen = () => {

console.log('Connected to order service');

ws.send(JSON.stringify({

type: 'subscribe',

channels: ['orders.created', 'orders.status'],

}));

};

ws.onmessage = (event) => {

const message = JSON.parse(event.data);

switch (message.type) {

case 'order.created':

displayNewOrder(message.data);

break;

case 'order.status':

updateOrderStatus(message.data);

break;

case 'error':

handleError(message.error);

break;

}

};

ws.onclose = (event) => {

if (event.code !== 1000) {

// Unexpected close, reconnect with exponential backoff

scheduleReconnect(event.code);

}

};

ws.onerror = (error) => {

console.error('WebSocket error:', error);

};

// Send action

function processOrder(orderId) {

if (ws.readyState === WebSocket.OPEN) {

ws.send(JSON.stringify({

type: 'process_order',

order_id: orderId,

timestamp: Date.now(),

}));

}

}

Server in Node.js:

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({

port: 8080,

maxPayload: 1024 * 1024, // 1MB

perMessageDeflate: true, // Compression

});

wss.on('connection', (ws, req) => {

const clientId = getClientId(req);

ws.on('message', async (data) => {

try {

const message = JSON.parse(data.toString());

switch (message.type) {

case 'subscribe':

// Add client to topic subscriptions

channelManager.subscribe(clientId, message.channels);

ws.send(JSON.stringify({

type: 'subscribed',

channels: message.channels,

}));

break;

case 'process_order':

// Process order and push status updates

await processAndStreamUpdates(ws, message.order_id);

break;

}

} catch (error) {

ws.send(JSON.stringify({

type: 'error',

error: error.message,

}));

}

});

ws.on('close', () => {

channelManager.unsubscribe(clientId);

});

});

Streaming Patterns

| Pattern | gRPC | WebSocket |

|---|---|---|

| Unary (request-response) | Native RPC | Wrapper over messages |

| Server streaming | Native (server pushes multiple responses) | Manual (loop sending) |

| Client streaming | Native (client sends multiple requests) | Manual (loop sending) |

| Bidirectional | Native (full duplex) | Native (full duplex) |

| Pub/Sub | Requires external broker | Manual topics |

| Request-reply patterns | Natural | Natural |

Protocol Buffers vs Raw Messages

Size comparison: gRPC (Protobuf) vs WebSocket (JSON)

example_message: |

Order with 3 items, totals 200 bytes

gRPC (binary Protobuf):

encoded_size: ~85 bytes

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\+ HTTP/2 framing: ~9 bytes

Total: ~94 bytes

WebSocket (JSON):

encoded_size: ~320 bytes

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\+ WebSocket framing: ~6 bytes

Total: ~326 bytes

Savings: gRPC is ~3.5x more efficient per message

at 1000 messages/second:

gRPC: ~94 KB/s + ~30 KB/s header overhead

WebSocket (JSON): ~326 KB/s + ~30 KB/s header overhead

Daily savings: ~20 GB

Type safety comparison:

gRPC: Compile-time type checking

Client gets typed stubs from proto definition

request = order_pb2.CreateOrderRequest(

user_id="user-123",

items=[

order_pb2.LineItem(product_id="prod-1", quantity=2, price=29.99)

],

)

response = stub.CreateOrder(request)

WebSocket: Runtime parsing

No compile-time type checking

message = json.loads(data)

Must validate fields manually

if not isinstance(message.get('user_id'), str):

raise ValueError("Invalid user_id")

Browser Support

| Factor | gRPC | WebSocket |

|---|---|---|

| Native browser support | gRPC-Web (with Envoy proxy) | Native |

| HTTP/2 browser APIs | Limited | Not needed |

| Streaming in browser | gRPC-Web (limited) | Full support |

| Headers and cookies | Via Envoy proxy | Standard HTTP upgrade |

| Fallback transport | WebSocket transport available | HTTP long-polling |

Envoy configuration for gRPC-Web

static_resources:

listeners:

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\- address:

socket_address:

address: 0.0.0.0

port_value: 443

filter_chains:

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\- filters:

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\- name: envoy.filters.network.http_connection_manager

typed_config:

"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager

codec_type: AUTO

stat_prefix: ingress_http

route_config:

virtual_hosts:

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\- name: backend

domains: ["*"]

routes:

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\- match:

prefix: "/order.OrderService/"

route:

cluster: grpc-backend

http_filters:

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\- name: envoy.filters.http.grpc_web

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\- name: envoy.filters.http.router

clusters:

\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\- name: grpc-backend

type: LOGICAL_DNS

typed_extension_protocol_options:

envoy.extensions.upstreams.http.v3.HttpProtocolOptions:

"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions

explicit_http_config:

http2_protocol_options: {}

Use Cases

| Use Case | Preferred | Reason |

|---|---|---|

| Microservice-to-microservice | gRPC | Typed contracts, efficient binary, built-in streaming |

| Browser real-time UI | WebSocket | Native browser support, simpler API |

| Mobile app real-time | WebSocket | Better battery, simpler to implement |

| Internal API gateway | gRPC | Strong typing, load balancing, authentication |

| Chat application | WebSocket | Message-oriented, pub/sub patterns |

| Financial ticker | gRPC streaming | Lower latency, smaller payloads |

| Live dashboards | WebSocket | Browser-side simplicity |

| IoT device communication | gRPC | Small binary, resource efficient, bi-directional |

Performance Comparison

| Metric | gRPC | WebSocket |

|---|---|---|

| Latency (p50) | ~0.5ms | ~1ms |

| Latency (p99) | ~5ms | ~10ms |

| Throughput (single connection) | 10,000 msg/s | 8,000 msg/s |

| Message size overhead | ~9 bytes | ~6 bytes |

| Compression | Automatic (HTTP/2 HPACK) | Optional (permessage-deflate) |

| Connection setup | 1 RTT (HTTP/2) | 1 RTT (HTTP upgrade) |

Both protocols are fast enough for most use cases. gRPC's advantages compound at high throughput due to binary encoding and HTTP/2 multiplexing.

Decision Framework

  • Choose gRPC for server-to-server communication, microservice APIs, IoT backends, or any system where strong typing, efficiency, and streaming matter more than browser compatibility.

  • Choose WebSocket for browser-based real-time applications, simple message passing, pub/sub patterns, or when you need universal protocol support without proxies.

  • Use both when your architecture needs gRPC for internal service communication and WebSocket for browser clients, with a gateway translating between them.

For modern applications, gRPC is the better choice for service-to-service communication, while WebSocket remains the practical standard for browser-based real-time features.