Skip to content

Client Streaming

Client streaming flips server streaming around: the client sends a sequence of messages over one open stream, and the server replies with a single response — but only after the client signals it is finished.

It is the natural fit for feeding data into a service: uploading a file in chunks, ingesting a batch of metrics, or sending many records that the server folds into one summary.

sequenceDiagram
  participant C as Client
  participant S as Server
  C->>S: Sample 1
  C->>S: Sample 2
  C->>S: Sample 3
  C->>S: (half-close: done sending)
  S-->>C: UploadSummary(count: 3, accepted: 3)
  Note over C,S: server responds once, after the client finishes
Client streaming: a stream of requests, one response

Here the stream keyword goes on the request. The response stays singular:

syntax = "proto3";
package metrics.v1;
message Sample {
string name = 1;
double value = 2;
}
message UploadSummary {
int32 count = 1;
int32 accepted = 2;
}
service Ingest {
// A stream of Samples, one UploadSummary response.
rpc UploadSamples(stream Sample) returns (UploadSummary);
}

The client sends each message, then performs a half-close — it tells the server “I’m done sending” — and waits for the single response.

stream, _ := client.UploadSamples(ctx)
for _, s := range samples {
if err := stream.Send(s); err != nil {
log.Fatal(err)
}
}
summary, err := stream.CloseAndRecv() // half-close, then get the reply
if err != nil {
log.Fatal(err)
}
fmt.Println(summary.Accepted)

The server reads every message, accumulates state, and returns a single response once the client half-closes.

func (s *server) UploadSamples(stream metricsv1.Ingest_UploadSamplesServer) error {
var count, accepted int32
for {
sample, err := stream.Recv()
if err == io.EOF {
// client is done — send the single response
return stream.SendAndClose(&metricsv1.UploadSummary{Count: count, Accepted: accepted})
}
if err != nil {
return err
}
count++
if sample.Value >= 0 {
accepted++
}
}
}
  • The response comes only after the half-close. The client cannot expect a reply mid-stream; the single response is the signal that the whole upload was processed.
  • Bound the total. A client can stream forever; the server should cap how much it will accept (message count or size) to avoid unbounded memory or work.
  • Order is preserved, so the server can rely on receiving samples in send order.
  • Either side can end early with an error. If the server rejects the batch partway (e.g. quota exceeded), it can return an error status instead of the summary — the client’s CloseAndRecv surfaces it.
In client streaming, when does the server send its single response?
Where does the `stream` keyword go for client streaming?
Why should the server cap how much a client-streaming call accepts?
What is the "half-close" in a client-streaming call?