0Pricing
C# Academy · Lesson

Client & Bidirectional Streaming

Build client streaming and full-duplex bidirectional streaming channels for high-throughput scenarios.

Client & Bidirectional Streaming is a free C# Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Client Streaming & Bidirectional Streaming

Beyond unary and server streaming, gRPC supports client streaming (client sends many, server responds once) and bidirectional streaming (both sides send multiple messages simultaneously over one connection).

Client Streaming: Proto Definition

Add the stream keyword before the request type. The client sends a sequence of messages, and when done, the server returns a single response.

service UploadService {
  // Client streams chunks, server returns a summary
  rpc UploadFile (stream FileChunk) returns (UploadSummary);
}

message FileChunk   { bytes data = 1; string filename = 2; }
message UploadSummary { int64 bytes_received = 1; string checksum = 2; }

Implementing Client Streaming on the Server

Use IAsyncStreamReader<T> to read client messages as they arrive. Call MoveNext() or iterate with ReadAllAsync().

public override async Task<UploadSummary> UploadFile(
    IAsyncStreamReader<FileChunk> requestStream,
    ServerCallContext context)
{
    long totalBytes = 0;
    using var ms = new MemoryStream();

    await foreach (var chunk in requestStream.ReadAllAsync(context.CancellationToken))
    {
        await ms.WriteAsync(chunk.Data.Memory, context.CancellationToken);
        totalBytes += chunk.Data.Length;
    }

    var checksum = ComputeMd5(ms.ToArray());
    return new UploadSummary { BytesReceived = totalBytes, Checksum = checksum };
}

Calling Client Streaming from the Client

Open the streaming call, write messages with RequestStream.WriteAsync(), then signal completion with CompleteAsync(). Await the response.

using var call = client.UploadFile();

var fileBytes = await File.ReadAllBytesAsync("large-file.bin");
const int chunkSize = 64 * 1024; // 64 KB

for (int offset = 0; offset < fileBytes.Length; offset += chunkSize)
{
    var chunk = fileBytes.Skip(offset).Take(chunkSize).ToArray();
    await call.RequestStream.WriteAsync(new FileChunk
    {
        Filename = "large-file.bin",
        Data = Google.Protobuf.ByteString.CopyFrom(chunk)
    });
}

await call.RequestStream.CompleteAsync(); // signal end
var summary = await call;                // await server response
Console.WriteLine($"Uploaded {summary.BytesReceived} bytes");

Bidirectional Streaming: Proto Definition

Add stream on both sides for full-duplex communication. Both client and server can send messages at any time, independently.

service ChatService {
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

message ChatMessage {
  string user    = 1;
  string content = 2;
  int64  sent_at = 3;
}

Implementing Bidirectional Streaming on the Server

Read from IAsyncStreamReader and write to IServerStreamWriter concurrently. Use Task.WhenAll to run both loops simultaneously.

public override async Task Chat(
    IAsyncStreamReader<ChatMessage> requestStream,
    IServerStreamWriter<ChatMessage> responseStream,
    ServerCallContext context)
{
    // Broadcast to all connected clients
    var readTask = Task.Run(async () =>
    {
        await foreach (var msg in requestStream.ReadAllAsync(context.CancellationToken))
        {
            _chatHub.Broadcast(msg);
        }
    });

    var writeTask = Task.Run(async () =>
    {
        await foreach (var msg in _chatHub.GetMessagesAsync(context.CancellationToken))
        {
            await responseStream.WriteAsync(msg);
        }
    });

    await Task.WhenAll(readTask, writeTask);
}

Bidirectional Streaming from the Client

Send and receive concurrently by launching two tasks — one for writing messages and one for reading replies.

using var call = client.Chat();

// Read task
var readTask = Task.Run(async () =>
{
    await foreach (var msg in call.ResponseStream.ReadAllAsync())
        Console.WriteLine($"{msg.User}: {msg.Content}");
});

// Write task
while (Console.ReadLine() is string text && text != "exit")
{
    await call.RequestStream.WriteAsync(
        new ChatMessage { User = "Alice", Content = text,
                          SentAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() });
}
await call.RequestStream.CompleteAsync();
await readTask;

Flow Control and Backpressure

HTTP/2 has built-in flow control. If the consumer is slow, gRPC pauses the sender automatically — preventing memory overflow. Don't buffer messages manually; let the stream handle it.

// DO: Let gRPC flow control handle backpressure
await foreach (var chunk in requestStream.ReadAllAsync(ct))
{
    await ProcessChunkAsync(chunk); // naturally paced
}

// DON'T: Buffering all chunks defeats flow control
var allChunks = new List<FileChunk>();
await foreach (var chunk in requestStream.ReadAllAsync(ct))
    allChunks.Add(chunk); // may OOM on large uploads

Half-Close and Stream Completion

In bidirectional streaming, either side can close its write-half while still reading from the other. This allows the client to signal "done sending" while still awaiting server responses.

// Client signals it's done sending
await call.RequestStream.CompleteAsync(); // half-close

// Continue reading server responses after half-close
await foreach (var reply in call.ResponseStream.ReadAllAsync())
{
    Console.WriteLine(reply.Content);
}

Using Channels for Thread-Safe Bidi Streaming

Combine System.Threading.Channels with bidirectional streaming for a clean producer-consumer pattern that safely fans out messages to many listeners.

private readonly Channel<ChatMessage> _broadcast =
    Channel.CreateUnbounded<ChatMessage>();

// Producer: receives from each client stream
public async Task ReceiveLoopAsync(
    IAsyncStreamReader<ChatMessage> reader, CancellationToken ct)
{
    await foreach (var msg in reader.ReadAllAsync(ct))
        await _broadcast.Writer.WriteAsync(msg, ct);
}

// Consumer: writes to each client's response stream
public async Task SendLoopAsync(
    IServerStreamWriter<ChatMessage> writer, CancellationToken ct)
{
    await foreach (var msg in _broadcast.Reader.ReadAllAsync(ct))
        await writer.WriteAsync(msg);
}

Real-World: Real-Time Analytics Pipeline

A data ingestion service accepts a stream of telemetry events from IoT devices and streams back aggregated statistics in real time — a perfect bidirectional streaming use case.

service TelemetryService {
  rpc StreamTelemetry(stream TelemetryEvent) returns (stream AggregatedStats);
}

// Server implementation sends a rolling aggregate every 100 events:
public override async Task StreamTelemetry(
    IAsyncStreamReader<TelemetryEvent> requests,
    IServerStreamWriter<AggregatedStats> responses,
    ServerCallContext context)
{
    int count = 0;
    double total = 0;
    await foreach (var e in requests.ReadAllAsync(context.CancellationToken))
    {
        total += e.Value;
        if (++count % 100 == 0)
            await responses.WriteAsync(
                new AggregatedStats { Count = count, Average = total / count });
    }
}

Quick Check

In client streaming, when does the server send its single response?

Recap: Client & Bidirectional Streaming

Key takeaways:

  • Client streaming: client sends many messages, server sends one reply — ideal for file upload
  • Bidirectional: both sides send independently — ideal for chat, live data feeds
  • Use ReadAllAsync() to consume streams with await foreach
  • Call RequestStream.CompleteAsync() to half-close the client write side
  • HTTP/2 flow control handles backpressure automatically — don't buffer entire streams
  • System.Threading.Channels pairs well with bidi streaming for fan-out scenarios

Frequently asked questions

Is the “Client & Bidirectional Streaming” lesson free?

Yes — the full text of “Client & Bidirectional Streaming” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.

What will I learn in “Client & Bidirectional Streaming”?

Build client streaming and full-duplex bidirectional streaming channels for high-throughput scenarios. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start C# Academy?

No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Client & Bidirectional Streaming” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this C# Academy lesson?

Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. gRPC & Protobuf Fundamentals
  2. Unary & Server Streaming RPCs
  3. Client & Bidirectional Streaming
  4. Deadlines, Cancellation & Interceptors
← Back to C# Academy