1. What is Server Streaming?
In a server streaming call, the client sends one request and the server responds with a stream of multiple messages over time. The client reads messages as they arrive until the server closes the stream.
Think of server streaming like subscribing to a live news ticker. You make one request ("subscribe to news") and then you keep receiving updates one by one as they happen — you do not wait for all news to be collected before reading.
| Call Type | Client Sends | Server Responds |
|---|
| Unary | 1 message | 1 message |
| Client Streaming | Many messages | 1 message |
| Server Streaming | 1 message | Many messages (stream) |
| Bidirectional Streaming | Many messages | Many messages |
Common use cases:
- Downloading a large dataset in chunks
- Real-time notifications or event feeds
- Progress updates on a long-running job
- Paginating large query results
2. Adding stream to the Proto Response
To declare a server streaming RPC, add the stream keyword before the response type only. The request has no stream keyword.
syntax = "proto3";
option csharp_namespace = "BasicGrpcService";
package greet;
service Greetings {
// Unary
rpc SayHello (HelloRequest) returns (HelloReply);
// Client streaming
rpc SendMultipleHellos (stream HelloRequest) returns (HelloReply);
// Server streaming — "stream" is on the RESPONSE side only
rpc GetMultipleReplies (HelloRequest) returns (stream HelloReply);
// Bidirectional streaming
rpc Chat (stream HelloRequest) returns (stream HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
The stream keyword on the response side tells gRPC: "the server will send multiple HelloReply messages in response to one HelloRequest."
3. Server Implementation
On the server side (GrpcService1/Services/GreeterService.cs), the method signature for a server streaming call is different from a unary call. Instead of returning Task<HelloReply>, it returns just Task and receives an IServerStreamWriter<HelloReply> argument.
using BasicGrpcService;
using Grpc.Core;
namespace GrpcService1.Services
{
public class GreeterService(ILogger<GreeterService> logger) : Greetings.GreetingsBase
{
// Server streaming: client sends one request -> server sends many responses
public override async Task GetMultipleReplies(
HelloRequest request,
IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
for (int i = 1; i <= 5; i++)
{
// Check if the client cancelled or the deadline was exceeded
if (context.CancellationToken.IsCancellationRequested)
break;
await responseStream.WriteAsync(new HelloReply
{
Message = $"Hello {request.Name} - message {i} of 5"
});
await Task.Delay(500); // simulate work between messages
}
// When the method returns, the stream is automatically closed
}
}
}
Key points about the server method:
- The method is
async and returns a plain Task (not Task<T>). - You use
responseStream.WriteAsync() to send each individual message. - Always check
context.CancellationToken.IsCancellationRequested so you can stop early if the client cancels or the deadline passes. - When the method finishes, gRPC automatically closes the stream and tells the client there are no more messages.
4. Writing to the Stream on the Server
IServerStreamWriter<T> has one primary method for sending messages:
| Method | Returns | Description |
|---|
WriteAsync(message) | Task | Serializes and sends one message to the client. Always await this. |
WriteOptions | WriteOptions | Controls compression for the next write (optional) |
Important: You cannot call WriteAsync() in parallel. Always await each write before calling it again. gRPC requires writes to be sequential on a stream.
// CORRECT — each write is awaited before the next
await responseStream.WriteAsync(new HelloReply { Message = "First" });
await responseStream.WriteAsync(new HelloReply { Message = "Second" });
await responseStream.WriteAsync(new HelloReply { Message = "Third" });
// WRONG — do not fire-and-forget WriteAsync calls
_ = responseStream.WriteAsync(new HelloReply { Message = "First" }); // bug!
_ = responseStream.WriteAsync(new HelloReply { Message = "Second" }); // bug!
5. Using Deadlines
A deadline is a point in time after which the call should automatically stop. The client sets the deadline when making the call. If the deadline is reached before the server finishes, the client receives a DeadlineExceeded status code.
Think of a deadline like a pizza delivery time guarantee. The restaurant (server) promises to keep sending updates, but if the pizza is not delivered within 30 minutes (the deadline), the order is automatically cancelled.
The server should check the cancellation token and return partial results when the deadline is close:
public override async Task GetMultipleReplies(
HelloRequest request,
IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
for (int i = 1; i <= 100; i++)
{
// Stop sending if the client cancelled or deadline was exceeded
if (context.CancellationToken.IsCancellationRequested)
{
logger.LogInformation("Stream cancelled at message {i}", i);
break; // return partial results gracefully
}
await responseStream.WriteAsync(new HelloReply
{
Message = $"Hello {request.Name} - message {i}"
});
await Task.Delay(200);
}
}
6. Client Implementation
On the client side (BasicGrpcClient/Program.cs), calling a server streaming method returns an AsyncServerStreamingCall<HelloReply>. You then read the ResponseStream to receive messages one by one.
You can also pass a deadline as a named parameter to limit how long the call can run:
using BasicGrpcService;
using Grpc.Net.Client;
using var channel = GrpcChannel.ForAddress("http://localhost:5287");
var client = new Greetings.GreetingsClient(channel);
// Set a deadline 10 seconds from now
DateTime deadline = DateTime.UtcNow.AddSeconds(10);
// Open the server streaming call — pass deadline as named parameter
using var call = client.GetMultipleReplies(
new HelloRequest { Name = "Ahmet" },
deadline: deadline);
// Read messages as they arrive
await foreach (var reply in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine("Received: " + reply.Message);
}
Console.WriteLine("Stream ended — no more messages.");
7. Reading the Stream on the Client
Just like the server-side reader, you have two options for reading the response stream:
Option A: ReadAllAsync() — Recommended
await foreach (var reply in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine("Received: " + reply.Message);
}
Option B: MoveNext() + Current — Manual approach
while (await call.ResponseStream.MoveNext())
{
var reply = call.ResponseStream.Current;
Console.WriteLine("Received: " + reply.Message);
}
| Approach | When to use |
|---|
ReadAllAsync() + await foreach | Most cases — clean and readable |
MoveNext() + Current | When you need explicit cancellation token or older C# compatibility |
8. The AsyncServerStreamingCall Object
client.GetMultipleReplies() returns an AsyncServerStreamingCall<HelloReply>.
| Property / Method | Type | What it does |
|---|
.ResponseStream | IAsyncStreamReader<HelloReply> | Read response messages from the server |
.ResponseStream.ReadAllAsync() | IAsyncEnumerable<HelloReply> | Stream all messages via await foreach |
.ResponseStream.MoveNext() | Task<bool> | Advance to next message; returns false when done |
.ResponseStream.Current | HelloReply | The current message (after MoveNext returned true) |
.ResponseHeadersAsync | Task<Metadata> | Headers sent by the server before the first message |
.GetStatus() | Status | gRPC status code — available after stream ends |
9. Full Working Example
GrpcService1/Services/GreeterService.cs
using BasicGrpcService;
using Grpc.Core;
namespace GrpcService1.Services
{
public class GreeterService(ILogger<GreeterService> logger) : Greetings.GreetingsBase
{
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply { Message = "Hello " + request.Name });
}
public override async Task<HelloReply> SendMultipleHellos(
IAsyncStreamReader<HelloRequest> requestStream,
ServerCallContext context)
{
var names = new List<string>();
await foreach (var request in requestStream.ReadAllAsync())
names.Add(request.Name);
return new HelloReply { Message = "Hello " + string.Join(", ", names) };
}
// Server streaming: client sends one request -> server sends many responses
public override async Task GetMultipleReplies(
HelloRequest request,
IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
for (int i = 1; i <= 5; i++)
{
if (context.CancellationToken.IsCancellationRequested)
break;
await responseStream.WriteAsync(new HelloReply
{
Message = $"Hello {request.Name} - message {i} of 5"
});
await Task.Delay(500);
}
}
}
}
BasicGrpcClient/Program.cs
using BasicGrpcService;
using Grpc.Core;
using Grpc.Net.Client;
using var channel = GrpcChannel.ForAddress("http://localhost:5287");
var client = new Greetings.GreetingsClient(channel);
Console.WriteLine("=== Server Streaming Demo ===");
// Set a deadline of 15 seconds
DateTime deadline = DateTime.UtcNow.AddSeconds(15);
try
{
using var call = client.GetMultipleReplies(
new HelloRequest { Name = "Ahmet" },
deadline: deadline);
await foreach (var reply in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine("Received: " + reply.Message);
}
Console.WriteLine("All messages received.");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
Console.WriteLine("The call timed out before all messages arrived.");
}
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
Expected output (with 5 messages, 500ms apart, within 15-second deadline):
=== Server Streaming Demo ===
Received: Hello Ahmet - message 1 of 5
Received: Hello Ahmet - message 2 of 5
Received: Hello Ahmet - message 3 of 5
Received: Hello Ahmet - message 4 of 5
Received: Hello Ahmet - message 5 of 5
All messages received.
Summary
- Server streaming uses the
stream keyword before the response type in the proto. - The server method returns
Task (not Task<T>) and receives an IServerStreamWriter<T>. - Use
responseStream.WriteAsync() to send each message — always await it before writing again. - Always check
context.CancellationToken.IsCancellationRequested so the server can stop gracefully on deadline or cancellation. - The client calls the method with an optional
deadline named parameter to set a time limit. - The client receives
AsyncServerStreamingCall<T> and reads messages via ResponseStream.ReadAllAsync() or MoveNext(). - Catch
RpcException with StatusCode.DeadlineExceeded to handle timeouts gracefully.