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.