Hot vs Cold Streams, Ordering, and Async Composition in Streamix
Most streaming abstractions become hard to trust at the exact point where they become operationally interesting.
Not when they map or filter values. That part is easy. The trust gap appears when a pipeline is shared, when asynchronous work runs concurrently, and when correctness depends on whether results preserve source order.
That is where Streamix needs to be explicit.
This post covers three related ideas:
- when a stream is cold versus hot
- when asynchronous composition preserves ordering
- when concurrency intentionally allows reordering for throughput
These are not advanced edge cases. In real systems, they are often the difference between a correct pipeline and a misleading one.
Cold means per-subscriber execution
The default Streamix model is cold and pull-based.
Operationally, that means each subscriber re-executes the source. If a pipeline wraps an IAsyncEnumerable<T>, that should already feel familiar. A second consumer does not automatically mean shared work. It usually means a second enumeration.
That default is conservative on purpose.
It avoids accidental sharing, keeps side effects easier to reason about, and matches normal .NET expectations. If a stream reads from a remote API, a database, or a timer-driven source, cold behavior means every subscriber gets its own logical run of that source unless you say otherwise.
This matters more than many APIs admit.
If an upstream source is expensive, assuming it is shared when it is actually cold can quietly double or triple work. If a source has side effects, cold semantics mean those side effects can happen again for every subscriber.
That is not a bug. It is the contract.
Hot means shared execution is now intentional
A hot stream is different because subscribers are no longer independently driving the full source lifecycle in the same way. Some amount of upstream work is shared.
In Streamix, hot behavior is not the default guess. It becomes explicit through primitives such as:
Publish()Replay(...)RefCount()
That explicitness matters because shared execution changes the operational model in non-trivial ways:
- late subscribers may miss values
- upstream work may outlive individual subscribers
- buffering and replay become part of correctness, not just performance
Consider a source that polls external state or receives live updates. If you want all subscribers to observe the same underlying feed rather than independently re-run the source, you are in hot-stream territory.
That is when the code should say so.
Why “hot vs cold” should not feel mystical
This topic often gets explained in library-specific jargon. It does not need to be.
A simpler framing is:
- cold: the subscriber gets its own execution of the source
- hot: subscribers attach to shared execution of the source
That framing is usually enough to reason about the common cases.
The key engineering questions are practical:
- do I want independent or shared upstream work?
- can a late subscriber miss items?
- do I need replay?
- who owns the lifecycle of the shared connection?
If the answers are not obvious from the pipeline, the abstraction is not doing enough work.
Ordering is not a performance detail
Once asynchronous work enters the picture, ordering becomes a first-class behavioral choice.
This is easy to underestimate because many toy examples still behave “as if” order were preserved. Real systems are less polite. One slow network call, one overloaded dependency, or one larger payload is enough to expose the true semantics of the operator.
Suppose you start with a sequence of IDs and enrich them asynchronously:
await stream
.MapAwait(id => LoadAsync(id))
.ForEachAsync(WriteAsync);
This is the sequential mental model. Each item completes before the next advances. The output stays ordered because the work itself is not concurrent.
Now compare that to:
await stream
.Map(id => LoadAsync(id), maxConcurrency: 8)
.ForEachAsync(WriteAsync);
This means something different. Work is concurrent, and results may be emitted out of source order.
That is not an implementation quirk. It is the contract.
If item 8 finishes before item 3, item 8 may be observed first. For workloads where order is irrelevant, that can be exactly what you want. It avoids waiting for a slow earlier item and typically improves throughput and tail latency.
But if downstream correctness depends on source order, then this operator is the wrong one.
Ordered concurrency is a separate choice
There is a middle ground between fully sequential processing and fully unordered concurrency: concurrent work with ordered output.
That is where ordered operators matter.
An operator such as MapOrdered says:
- process multiple items concurrently
- do not emit later results until earlier items are ready to be observed
This is useful when source order is semantically meaningful but purely sequential execution is too expensive.
The tradeoff is important. Ordered concurrency usually means buffering and waiting. If item 2 is slow and items 3 through 10 finish early, those later results may need to sit until item 2 can be emitted first.
This is a real cost, but it is also the price of preserving correctness without giving up concurrency entirely.
You should choose it consciously.
Flattening makes the semantics even more visible
The same choices apply to 1:N composition.
In everyday terms:
FlatMapfavors concurrent expansion and does not promise ordered outputConcatMapis sequential and orderedFlatMapOrderedkeeps ordered output while still allowing concurrent inner work
These distinctions matter because flattening often appears in pipelines that fan out into remote calls, nested collections, or multi-step lookups.
Imagine processing a stream of users where each user expands into many orders. The operator choice answers several practical questions:
- can later users’ orders appear before earlier users’ orders?
- can multiple users be expanded at the same time?
- will the pipeline buffer later inner results while waiting for earlier ones?
Those are business and systems questions, not just operator trivia.
Choosing the right semantics
A useful rule of thumb:
Use sequential composition when:
- correctness depends on strict step-by-step execution
- the pipeline is simple enough that concurrency is not worth the extra cost
- you want the most straightforward failure and cancellation behavior
Use unordered concurrent composition when:
- throughput matters more than source-order preservation
- results are logically independent
- downstream consumers do not care about reordering
Use ordered concurrent composition when:
- source order carries meaning
- concurrency still provides meaningful latency or throughput gains
- you are willing to pay the buffering and coordination cost to preserve order
None of these is the universally correct default. The important thing is that the pipeline says which one you picked.
The connection between hot/cold and ordering
These topics are related because both deal with where the abstraction exposes behavior rather than hiding it.
Hot versus cold tells you whether execution is shared. Ordering semantics tell you how results emerge from concurrency.
Together, they answer the operational questions an experienced engineer actually asks:
- will this source run once or many times?
- are subscribers observing the same live execution?
- can later work overtake earlier work?
- is buffering happening to preserve order?
If the library makes those choices explicit, it earns trust. If it leaves them implicit, engineers end up debugging semantics through production behavior.
What comes next
At this point, the picture is mostly complete from a behavioral point of view:
- streams are cold unless explicitly made hot
- shared execution should be deliberate
- concurrency and ordering are distinct choices
- flattening operators carry real throughput and correctness tradeoffs
The remaining step is to connect those semantics to system boundaries:
- what happens when producers are faster than consumers
- where channel-backed boundaries belong
- how Streamix interoperates with existing .NET code
- what practical server-side streaming looks like in ASP.NET Core
That is where backpressure and integration stop being theory and start becoming deployment concerns.
Important Links
For a closer look at the code and examples, check out the Streamix repo: https://github.com/khurram-uworx/streamix
Nugets:
- https://www.nuget.org/packages/Streamix
- https://www.nuget.org/packages/Streamix.Extensions
- https://www.nuget.org/packages/Streamix.AspNetCore
More from Streamix
- Streamix: A Stream Library for Modern .NET
- Streamix: The Core Mental Model
- Hot vs Cold Streams, Ordering, and Async Composition in Streamix (Current)
- Backpressure, Interop, and Streaming ASP.NET Core Responses With Streamix