Skip to content

Bidirectional Streaming

Bidirectional streaming opens one stream on which both the client and the server send messages, independently and at the same time. Neither side has to wait for the other; a message can flow in either direction whenever there is something to say.

This is the shape for genuinely interactive sessions: chat, multiplayer game state, collaborative editing, or any protocol where a request and its responses are interleaved rather than lock-stepped.

sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: join(room: "general")
  S-->>C: "Sam joined"
  C->>S: "hello everyone"
  S-->>C: "Alex: hi Sam"
  C->>S: "how's the deploy?"
  S-->>C: "Alex: green"
  Note over C,S: each side sends whenever it wants; order is per-direction
Bidirectional streaming: independent, interleaved messages

Both the request and the response carry the stream keyword:

syntax = "proto3";
package chat.v1;
message ChatMessage {
string from = 1;
string text = 2;
int64 ts = 3;
}
service Chat {
// Both sides stream ChatMessages independently.
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

Both ends run a send loop and a receive loop

Section titled “Both ends run a send loop and a receive loop”

The key difference from the one-directional streams: each side typically reads and writes concurrently. Reading and writing on the same stream from separate goroutines/threads/callbacks is safe and expected.

stream, _ := client.Chat(ctx)
// receive loop (its own goroutine)
go func() {
for {
msg, err := stream.Recv()
if err == io.EOF { return }
if err != nil { log.Println(err); return }
fmt.Printf("%s: %s\n", msg.From, msg.Text)
}
}()
// send loop (main goroutine)
for _, text := range outgoing {
stream.Send(&chatv1.ChatMessage{From: "me", Text: text})
}
stream.CloseSend() // done sending; server may still send

The server implementation mirrors this: it loops on Recv and calls Send whenever it has something to deliver — often fanning a received message out to other connected clients.

  • Ordering is per-direction. Messages the client sends arrive at the server in order; messages the server sends arrive at the client in order. But the two directions are independent — there is no global ordering between a client message and a server message.
  • Half-close is one-sided. CloseSend (client) or returning (server) ends that side’s sending while the other side keeps streaming. The call is fully done only when both directions close.
  • Read and write concurrently. Don’t block your receive loop behind your send loop, or a full flow-control window can deadlock you (next lesson).
  • This is not a message broker. A bidi stream is point-to-point between one client and one server; fanning out to many clients (a chat room) is application logic on top, usually backed by a pub/sub system.
What defines a bidirectional streaming RPC in the .proto?
What ordering guarantee does a bidi stream give?
Why do both ends usually read and write concurrently?
How do you fan one client’s message out to many other clients (a chat room)?