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