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.
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
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.