Streaming RPCs
The four gRPC RPC types — unary, server streaming, client streaming and bidirectional streaming — with real use cases.
The four RPC types
gRPC supports four request/response shapes, all defined declaratively in the .proto file itself using the stream keyword:
service ExampleService {
rpc Unary (Request) returns (Response);
rpc ServerStream (Request) returns (stream Response);
rpc ClientStream (stream Request) returns (Response);
rpc BidiStream (stream Request) returns (stream Response);
}
1. Unary — one request, one response
The familiar shape, equivalent to a normal REST call:
rpc GetTask (GetTaskRequest) returns (Task);
resp, err := client.GetTask(ctx, &pb.GetTaskRequest{Id: 1})
Use case: anything request/response shaped — fetching a record by ID, submitting a single form, most everyday CRUD operations.
2. Server streaming — one request, many responses
The client sends a single request; the server responds with a stream of messages over time, over the same connection, until it closes the stream:
rpc WatchPriceUpdates (WatchRequest) returns (stream PriceUpdate);
stream, err := client.WatchPriceUpdates(ctx, &pb.WatchRequest{Symbol: "GOOG"})
for {
update, err := stream.Recv()
if err == io.EOF {
break // server closed the stream
}
fmt.Println(update.Price)
}
Use case: a live feed from a single subscription — stock price ticks, a live sports score, log tailing, progress updates for a long-running server-side job. The client asks once; the server keeps pushing updates as they happen.
3. Client streaming — many requests, one response
The reverse: the client sends a stream of messages, and the server responds once, after the client finishes sending:
rpc UploadTelemetry (stream TelemetryPoint) returns (UploadSummary);
stream, err := client.UploadTelemetry(ctx)
for _, point := range readings {
stream.Send(point)
}
summary, err := stream.CloseAndRecv() // signals "done sending", waits for the one response
Use case: a client accumulating many small pieces of data before a single summarizing response makes sense — batching sensor readings before an acknowledgment, uploading a large file in chunks and getting one final confirmation, aggregating client-side metrics before a summary comes back.
4. Bidirectional streaming — both sides stream independently
Client and server each send a stream of messages over the same long-lived connection, and — critically — independently of each other's timing: either side can send at any point, not necessarily in strict request/response lockstep:
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
stream, err := client.Chat(ctx)
go func() {
for _, msg := range outgoing {
stream.Send(msg)
}
stream.CloseSend()
}()
for {
msg, err := stream.Recv()
if err == io.EOF {
break
}
fmt.Println("received:", msg.Text)
}
Use case: real-time chat, collaborative editing, a game server exchanging state with a client — anything where both sides need to push data to each other on their own schedule over one open connection, not a strict alternating turn-based exchange.
Why HTTP/2 makes this possible
All four shapes run over the same underlying HTTP/2 connection. HTTP/2's native support for multiple concurrent, independent streams over a single TCP connection is exactly what lets gRPC keep a request "open" for an extended, ongoing exchange of messages in either direction — something HTTP/1.1 has no clean way to express, which is why streaming RPC patterns like these were awkward to build directly on top of plain REST.
Choosing the right shape
| RPC type | Client sends | Server sends | Typical use |
|---|---|---|---|
| Unary | 1 | 1 | Standard request/response, CRUD |
| Server streaming | 1 | many | Live feeds, subscriptions, progress updates |
| Client streaming | many | 1 | Batched uploads, aggregation before a summary |
| Bidirectional streaming | many | many | Chat, collaborative editing, real-time state sync |
Common mistakes
- Reaching for bidirectional streaming when server streaming (or even plain unary with polling) would be simpler and sufficient — bidirectional streams are the most powerful shape but also the most complex to reason about and to handle errors/reconnection for.
- Forgetting to handle
io.EOF(or the equivalent "stream closed" signal in your language) as the normal, expected end of a stream, rather than treating it as an error. - Not planning for reconnection — a long-lived stream can be interrupted by a network blip, a load balancer timeout, or a server restart; production streaming clients need retry/reconnect logic, not an assumption the stream stays open forever.
Interview questions
Q: What are the four types of gRPC calls? Unary (one request, one response — like a normal REST call), server streaming (one request, a stream of responses), client streaming (a stream of requests, one response), and bidirectional streaming (both sides stream independently over the same connection).
Q: Give a concrete example of when you'd use server streaming instead of unary.
A live stock price feed: the client sends one subscription request ("watch GOOG"), and rather than polling repeatedly with separate unary calls, the server pushes a new PriceUpdate message down the same open stream every time the price changes, until the client disconnects or the server closes the stream.
Q: What makes bidirectional streaming different from just running client streaming and server streaming at the same time? Both sides share one single stream/connection and can send messages independently, in any interleaving, without waiting for the other side to finish — a real-time back-and-forth (like chat) rather than two separate, unrelated one-directional flows. It requires HTTP/2's support for concurrent, independent data frames over one connection.