Skip to main content

Bidirectional Streaming RPC

rpc Method(stream Request) returns (stream Response)

Bidirectional streaming opens a full-duplex HTTP/2 stream where both client and server can send messages independently at any time. Each side sends a stream and receives a stream. The order and interleaving of messages is application-defined. Used for real-time chat, live collaboration, gaming state sync, and any scenario requiring simultaneous two-way message flow.

Details

Bidirectional streaming (bidi streaming) is the most powerful and complex gRPC pattern: client sends messages at any time → server sends messages at any time → either side closes their stream

HTTP/2 mechanics: a single HTTP/2 stream carries both directions. The client half-closes with END_STREAM; the server half-closes with its own END_STREAM on the trailers frame. Either side can read and write concurrently.

Message ordering: Messages from client to server arrive in order Messages from server to client arrive in order There is no defined ordering between client messages and server messages – the server does not have to respond to each client message

Three interaction patterns: Ping-pong: client sends, server responds, client sends again (conversational) Client-driven: client sends many, server occasionally sends (command stream with acks) Server-driven: server sends many, client occasionally sends (event feed with control messages)

Goroutine model (Go): Typically: one goroutine reads from the stream, another writes to the stream Must handle stream closure from either side (io.EOF from Recv)

Deadline and cancellation: bidi streams can run indefinitely. Always set a max duration or implement heartbeat/keepalive to detect dead streams. Without it, hung connections accumulate.

For browser clients: gRPC-Web supports bidi streaming via HTTP/2 streams but requires grpc-web proxy (Envoy). Consider WebSocket or WebTransport for browser-facing bidi communication.

Proto definition

Protocol Buffer service definition
syntax = "proto3";

service ChatService {
  // Full duplex chat
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

service GameService {
  // Client sends player actions; server sends game state
  rpc Play(stream PlayerAction) returns (stream GameState);
}

message ChatMessage {
  string user_id = 1;
  string text = 2;
  int64 timestamp = 3;
}
message PlayerAction { string action_type = 1; }
message GameState { bytes state = 1; int64 tick = 2; }

Go bidirectional streaming

Go bidirectional streaming
// Server (Go) – concurrent read/write
func (s *ChatService) Chat(stream pb.ChatService_ChatServer) error {
    // Read goroutine
    go func() {
        for {
            msg, err := stream.Recv()
            if err == io.EOF || err != nil { return }
            broadcast(msg)  // send to all connected clients
        }
    }()
    // Write: subscribe to broadcast channel
    for msg := range s.subscribe(stream.Context()) {
        if err := stream.Send(msg); err != nil { return err }
    }
    return nil
}

// Client (Go)
stream, _ := client.Chat(ctx)
go func() {
    for { msg, _ := stream.Recv(); display(msg) }  // receive
}()
for input := range userInput {
    stream.Send(&pb.ChatMessage{Text: input})         // send
}

When to use

Real-time chat

Live collaboration (collaborative editing)

Game state synchronization

Interactive data analysis

Bidirectional device communication

Backpressure

HTTP/2 flow control in both directions independently. A slow consumer on either side blocks the other's writes if the receive window fills.

See Also