A simulation does not always need to wait for persistence as soon as a match ends. A producer can enqueue the result for a separate consumer. Channel is useful for that separation, provided that accepted into the queue and durably saved remain different outcomes.
Separate production from consumption
This independent .NET 10 console example creates a bounded channel with capacity four. A writer submits match IDs and one consumer reads them. The visible outputs are saved:1 and saved:2; those are console messages, not actual database writes.
using System.Threading.Channels;
var queue=Channel.CreateBounded<int>(new BoundedChannelOptions(4){
SingleReader=true,FullMode=BoundedChannelFullMode.Wait
});
var consumer=Task.Run(async()=>{
await foreach(var matchId in queue.Reader.ReadAllAsync())
Console.WriteLine($"saved:{matchId}");
});
await queue.Writer.WriteAsync(1);
await queue.Writer.WriteAsync(2);
queue.Writer.Complete();
await consumer;
With FullMode.Wait, WriteAsync can wait for room when the channel is full. That backpressure lets the producer feel the consumer's throughput instead of growing memory indefinitely. Four is a demonstration capacity, not a tuning recommendation. A real value depends on item size, production rate and acceptable waiting time.
Understand which order is preserved
The code starts one consumer and processes its items sequentially. SingleReader=true is a usage promise to the channel implementation, not an application-wide or distributed lock. Adding another reader requires reconsidering that promise.
Concurrent writers also mean queue acceptance order may differ from the business meaning of match completion order. Results may need match identity, round/world information and an explicit sequence. The last message received should not automatically be treated as the newest domain state.
A full queue affects product behavior
An unbounded channel can keep allocating memory if production outpaces consumption. A bounded channel makes the limit explicit, but Wait can slow the producer too. Drop policies are not harmless implementation shortcuts when losing a result is unacceptable.
The two-item sample never creates pressure. A controlled test with a slow consumer and more than four queued items can observe writer waiting. The goal is to understand that behavior, not simply generate more output.
Acceptance is not processing or commit
A completed WriteAsync means acceptance into the in-memory channel, not that the consumer has processed or committed the item. A process failure can lose queued results. If loss is unacceptable, use durable input, an outbox or another recoverable result store; Channel can remain a process-local coordination mechanism.
Retries can also replay a result after a database write has already succeeded. An idempotent persistence rule based on match identity and version is still useful. A single reader does not guarantee exactly-once side effects across crashes.
Coordinate shutdown and failure on both ends
Complete tells the reader that no more items will arrive. The consumer then drains the remaining input and completes. Draining and immediately cancelling are different shutdown policies that need an explicit timeout and cancellation budget.
If the consumer fails while writers continue, a full channel can leave producers waiting indefinitely. Propagate failure and choose a deliberate retry or failure-queue policy. Catching an exception and printing saved is not persistence.
Test order, backpressure, consumer failure, cancellation and queued items at shutdown. With a real store, verify the result by reading it back rather than treating a console message as the acceptance test.