· via dev.to (home feed)
.NET 10 stops BufferedStream.WriteByte from implicitly flushing the wrapped stream
.NET 10 removes a long-standing quirk where BufferedStream.WriteByte flushed the wrapped stream once its buffer filled. Code relying on that side effect needs an explicit flush at a real boundary.

What changed
For years, BufferedStream.WriteByte has behaved differently from every other write method on the class: when a single-byte write filled the internal buffer to its configured size, the call went on to invoke Flush() on the wrapped stream. According to dev.to, Microsoft documents the removal of that implicit flush as an official .NET 10 behavioral change, and the new behavior ships in the .NET 10 LTS release rather than behind a preview flag. The post's author verified it using SDK 10.0.303 and the 10.0.11 runtime, compared against runtime 9.0.18 for the old contract.
The distinction is subtler than "bytes now stay in the buffer." BufferedStream has always been able to push buffered data down to the destination when it needs room to accept more. What disappears in .NET 10 is the call to the destination's Flush() method that used to accompany those capacity-driven write-outs. The wrapped stream still receives the bytes; it just no longer receives an accidental flush at the same moment.
A reproducible difference
The dev.to post demonstrates the gap with a TrackingStream — a MemoryStream subclass that counts Flush and Write calls — wrapped in a BufferedStream with a four-byte buffer. Writing four bytes one at a time yields:
- .NET 9: one write, one flush, with the byte sequence 010203 already in the destination
- .NET 10: one write, zero flushes, with the same three bytes delivered
In both runtimes the fourth byte stays inside BufferedStream until an explicit flush or disposal, after which both contain the full 01020304 sequence. Only the flush side effect changed. The author recommends keeping exactly this kind of executable contract as a regression test rather than inferring behavior from a stream length check.
How to respond
The suggested migration is not to tune buffer sizes until the old accidental timing reappears. Instead, place the flush where the application genuinely has a boundary — after a record, frame, or bounded batch — using Flush or its asynchronous counterpart. Flushing after every byte would undo the batching that BufferedStream exists to provide.
Not every codebase needs to change. If the bytes only have to arrive by the time the stream is disposed, and the wrapped stream attaches no observable meaning to Flush(), disposal still supplies the final boundary. The post also cautions that an explicit flush is not a durability guarantee: memory streams, network streams, compressors, and file streams each interpret Flush() differently, and physical-media durability requires the storage-specific operation rather than a generic one.
For teams that do depend on flush timing, a counting fake stream — or a protocol fixture verifying that a complete record only becomes visible after the chosen flush — catches the problem offline, without needing a network service.
Why it matters
The change is easy to miss because applications keep producing the correct bytes; what they lose is a side effect that used to fire at a predictable point. That matters wherever the wrapped stream makes Flush observable: custom streams, protocol adapters, compressors, and test doubles. Bytes may now sit in the destination's own buffering, or a compressor's internal state, for longer than before, shifting when a peer or downstream component perceives a complete message. The broader migration rule from the post is to stop treating internal buffer capacity as application control flow, and instead flush at the boundaries the protocol or storage layer actually defines.
- #dotnet
- #net-10
- #breaking-changes
- #streams
- #io