What Is a Bidirectional Streaming RPC?
Bidirectional streaming opens a single HTTP/2 stream over which both the client and the server can send messages independently and in any order relative to each other, with each side reading and writing on its own schedule until both close their respective send directions. Unlike client or server streaming, neither side waits for the other to finish before sending its own messages, which makes this pattern the closest gRPC gets to a full-duplex, persistent connection — well suited to chat systems, collaborative editing, and real-time multiplayer state synchronization.
Cricket analogy: It is like a stump microphone feed during a live broadcast: the umpire's calls and the players' chatter are captured continuously in both directions at once, not waiting for one party to finish talking before the other starts.
Message Ordering and Stream Lifecycle
Within each direction, gRPC guarantees ordered delivery — messages the client sends arrive at the server in the order sent, and the same holds for server-to-client — but there is no guaranteed ordering between the two directions relative to each other. The stream ends only once both sides have finished: the client half-closes by ending its send loop, the server independently finishes sending and returns a final status, and the RPC completes when both conditions are satisfied, which means either side can also end the call early by returning an error status or cancelling.
Cricket analogy: It is like a scorer's book and a commentator's transcript both being kept in strict chronological order individually, even though you can't precisely align which commentary line corresponds to which scorebook entry down to the millisecond.
Defining and Implementing a Bidirectional Streaming RPC
syntax = "proto3";
service ChatService {
// Bidirectional streaming: independent streams both ways.
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message ChatMessage {
string room_id = 1;
string sender = 2;
string text = 3;
int64 sent_at_ms = 4;
}func (s *chatServer) Chat(stream pb.ChatService_ChatServer) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil // client finished sending
}
if err != nil {
return err
}
// Broadcast to every other subscriber in the room concurrently.
s.rooms.Broadcast(msg.RoomId, msg)
// Send is called independently, not in lockstep with Recv.
if err := stream.Send(msg); err != nil {
return err
}
}
}Reading (Recv) and writing (Send) on the same stream must each be single-threaded per direction — most gRPC implementations are not safe to call Send concurrently from multiple goroutines or threads on the same stream. Use one dedicated writer loop, often fed by a channel, if multiple producers need to send on the same bidirectional stream.
Designing Bidirectional Protocols Well
Because both sides can send at any time, well-designed bidirectional protocols usually include an explicit message-type or oneof field so the receiver can distinguish, say, a heartbeat from a data update from a control command, rather than assuming every message on the stream means the same thing. It's also common to build a lightweight application-level acknowledgment or sequence number scheme on top of the raw stream when the business logic needs stronger guarantees than gRPC's per-direction ordering, such as detecting a gap after a brief reconnect.
Cricket analogy: It is like a stadium's radio-comms protocol distinguishing umpire calls, security alerts, and groundstaff chatter on separate labeled channels, rather than mixing every voice into one undifferentiated feed.
Bidirectional streams can be synchronous (client and server strictly alternate, effectively a ping-pong pattern) or fully asynchronous (either side sends freely at any time). Decide which model your protocol needs up front — it changes how you structure the read and write loops on both ends.
- Bidirectional streaming lets client and server each send an independent, ordered message stream on one connection.
- Ordering is guaranteed within each direction but not across the two directions relative to each other.
- The RPC completes only once both sides have finished sending and a final status is returned.
- The proto definition marks both the request and response types with 'stream'.
- Send calls on a single stream must not be issued concurrently from multiple goroutines or threads.
- Well-designed protocols tag messages with an explicit type so receivers can dispatch correctly.
- This pattern fits chat, collaborative editing, and real-time multiplayer state sync best.
Practice what you learned
1. What distinguishes bidirectional streaming from server streaming and client streaming?
2. What ordering guarantee does gRPC provide in a bidirectional stream?
3. Why is it unsafe to call Send concurrently from multiple goroutines on the same bidirectional stream?
4. Which application pattern is bidirectional streaming best suited for?
Was this page helpful?
You May Also Like
Client Streaming RPC
A gRPC pattern where the client sends a sequence of messages over one stream and the server replies with a single response once the client finishes.
Server Streaming RPC
A gRPC pattern where the client sends one request and the server replies with a sequence of messages over time on the same stream.
Unary RPC Explained
The simplest gRPC call pattern: one request in, one response out, modeled as a single HTTP/2 stream that opens and closes in a single round trip.
Choosing the Right RPC Type
A practical decision guide for picking between unary, server-streaming, client-streaming, and bidirectional-streaming gRPC methods.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics