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:

Protobuf
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:

Protobuf
rpc GetTask (GetTaskRequest) returns (Task);
Go
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:

Protobuf
rpc WatchPriceUpdates (WatchRequest) returns (stream PriceUpdate);
Go
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:

Protobuf
rpc UploadTelemetry (stream TelemetryPoint) returns (UploadSummary);
Go
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:

Protobuf
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
Go
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.