gRPC With. Net Call Types Supported by gRPC Created: 26 Mar 2026 Updated: 26 Mar 2026

Server Streaming in gRPC with .NET

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 TypeClient SendsServer Responds
Unary1 message1 message
Client StreamingMany messages1 message
Server Streaming1 messageMany messages (stream)
Bidirectional StreamingMany messagesMany messages

Common use cases:

  1. Downloading a large dataset in chunks
  2. Real-time notifications or event feeds
  3. Progress updates on a long-running job
  4. 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:

  1. The method is async and returns a plain Task (not Task<T>).
  2. You use responseStream.WriteAsync() to send each individual message.
  3. Always check context.CancellationToken.IsCancellationRequested so you can stop early if the client cancels or the deadline passes.
  4. 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:

MethodReturnsDescription
WriteAsync(message)TaskSerializes and sends one message to the client. Always await this.
WriteOptionsWriteOptionsControls 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);
}
ApproachWhen to use
ReadAllAsync() + await foreachMost cases — clean and readable
MoveNext() + CurrentWhen you need explicit cancellation token or older C# compatibility

8. The AsyncServerStreamingCall Object

client.GetMultipleReplies() returns an AsyncServerStreamingCall<HelloReply>.

Property / MethodTypeWhat it does
.ResponseStreamIAsyncStreamReader<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.CurrentHelloReplyThe current message (after MoveNext returned true)
.ResponseHeadersAsyncTask<Metadata>Headers sent by the server before the first message
.GetStatus()StatusgRPC 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

  1. Server streaming uses the stream keyword before the response type in the proto.
  2. The server method returns Task (not Task<T>) and receives an IServerStreamWriter<T>.
  3. Use responseStream.WriteAsync() to send each message — always await it before writing again.
  4. Always check context.CancellationToken.IsCancellationRequested so the server can stop gracefully on deadline or cancellation.
  5. The client calls the method with an optional deadline named parameter to set a time limit.
  6. The client receives AsyncServerStreamingCall<T> and reads messages via ResponseStream.ReadAllAsync() or MoveNext().
  7. Catch RpcException with StatusCode.DeadlineExceeded to handle timeouts gracefully.
Share this lesson: