gRPC vs WebSocket: Real-Time Communication


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




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