CodeGym /Courses /C# SELF /Using BufferedStream

Using BufferedStream

C# SELF
Level 41, Lesson 2
Available

1. Introduction

Before you start using a new class, it’s worth understanding why it exists at all. Let’s figure out what happens when we work with a file "directly" using FileStream.

When you call Read or Write on a stream created via FileStream, you actually hit the disk subsystem. That process itself (especially on old HDDs, but also on modern SSDs) is much slower than working with RAM. Imagine that when you order fries at McDonalds, the cashier runs to the storeroom for a new bag every single time. Imagine how long the line would get!

If you work with small chunks of data, frequent disk or network calls lead to a performance hit. The larger the data volume, the more noticeable the effect.

Brief analogy

So, unbuffered streams are roughly like going to the store 10 times to buy one yogurt each time. A buffered stream is when you grab a whole basket of yogurts at once, minimizing the number of trips.

2. The BufferedStream class: first look

What it’s for

BufferedStream is a wrapper around any Stream that keeps an intermediate buffer in memory. When you write data, it first goes into the buffer and only when the buffer is full is it flushed to disk in a single big operation. Reading is similar: on the first read it loads a significant chunk into memory and then returns pieces from memory until the buffer runs out.

Code example: creating a BufferedStream

Let’s create a simple example. Say we need to write 100,000 lines to a file:


string filePath = "big_output.txt";
using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
using var bufferedStream = new BufferedStream(fileStream);
using var writer = new StreamWriter(bufferedStream);

for (int i = 0; i < 100_000; i++)
{
    writer.WriteLine($"String number {i}");
}

Console.WriteLine("Write completed!");

Comment:

  • We open the file for writing via FileStream.
  • Then we wrap it in a BufferedStream, and then in a StreamWriter (which writes text lines to the stream).
  • As soon as the buffer fills up, data is flushed to disk in one chunk.

3. How buffering works "under the hood"

Let’s break it down with a diagram:


[Your code] → [StreamWriter] → [BufferedStream] → [FileStream] → [File on disk]

When you call WriteLine() on StreamWriter, the text first goes into its internal buffer, then through BufferedStream — into another buffer, and only when the buffer is full or the stream is closed are the bytes flushed to disk.

How many bytes in one bucket?

The default buffer size is 4096 bytes (4 KB), but you can specify it explicitly:


int myBufferSize = 16 * 1024; // 16 KB
using var fileStream = new FileStream(filePath, FileMode.Create);
using var bufferedStream = new BufferedStream(fileStream, myBufferSize);
// ...

Practical tip: On modern systems it’s reasonable to use buffers of 8–64 KB. For very large file operations — even bigger. But don’t overdo it: if you work on a microcontroller with 128 KB of RAM, a 64 KB buffer is a bad idea :)

4. Experiment: compare speed with and without buffer

To see how important this is, let’s write a test that compares writing through FileStream with and without a buffer:


using System.Diagnostics;
using System.Text;

string data = new string('X', 1000); // 1,000 characters

void WriteWithoutBuffer()
{
    using var fs = new FileStream("no_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: false);
    for (int i = 0; i < 10_000; i++)
    {
        byte[] bytes = Encoding.UTF8.GetBytes(data);
        fs.Write(bytes, 0, bytes.Length); // Directly to file – disk hit every time
    }
}

void WriteWithBuffer()
{
    using var fs = new FileStream("with_buffer.txt", FileMode.Create, FileAccess.Write, FileShare.None);
    using var bs = new BufferedStream(fs, 16 * 1024);
    for (int i = 0; i < 10_000; i++)
    {
        byte[] bytes = Encoding.UTF8.GetBytes(data);
        bs.Write(bytes, 0, bytes.Length);
    }
}

// Measure time
Stopwatch sw = Stopwatch.StartNew();
WriteWithoutBuffer();
sw.Stop();
Console.WriteLine("No buffer: " + sw.ElapsedMilliseconds + " ms");

sw.Restart();
WriteWithBuffer();
sw.Stop();
Console.WriteLine("With buffer: " + sw.ElapsedMilliseconds + " ms");

Expected output:
In most cases you’ll see a noticeable speedup with a buffer! Especially on an HDD. On SSD the effect is still visible but less dramatic.

5. Which buffer to choose? Comparison and practice

.NET has many classes related to buffering. Let’s clarify:

Class What it’s used for Buffer built-in? Need to use BufferedStream?
FileStream
Working with files Yes (with 4 KB) Almost not needed (but possible)
NetworkStream
Working with network No Highly recommended
StreamReader/Writer
Reading/writing text Yes (from ~1 KB) Usually not needed
GZipStream
Compression/decompression No Can/should be used for speed

Important:
FileStream with the constructor parameter bufferSize is essentially already buffered. If you explicitly set a sufficiently large buffer, adding another BufferedStream won’t give much benefit. But if you use another stream (for example, a network stream), BufferedStream is your friend.

6. Example: Copying a file with BufferedStream


string source = "big_input.dat";
string dest = "big_output.dat";

int bufferSize = 64 * 1024; // 64 KB

using var inputStream = new FileStream(source, FileMode.Open, FileAccess.Read);
using var outputStream = new FileStream(dest, FileMode.Create, FileAccess.Write);
using var bufferedInput = new BufferedStream(inputStream, bufferSize);
using var bufferedOutput = new BufferedStream(outputStream, bufferSize);

byte[] buffer = new byte[bufferSize];
int bytesRead;
while ((bytesRead = bufferedInput.Read(buffer, 0, buffer.Length)) > 0)
{
    bufferedOutput.Write(buffer, 0, bytesRead);
}
// Don’t forget flush – otherwise the last bytes might not get to disk!
bufferedOutput.Flush();

Console.WriteLine("Copy completed!");

Comment:

  • We read data in large chunks (64 KB) from one file via BufferedStream.
  • We write it to another file, also using a buffer.
  • After the loop ends, be sure to call Flush() to write the last data.

7. Useful nuances

Tip: when you really need BufferedStream

  • If you work with streams that don’t have buffering (for example, network streams or custom Stream implementations);
  • If you work with large volumes of binary data (e.g., file copying, format conversion, backups);
  • If you’re optimizing existing code and see that the bottleneck is a large number of small Write/Read operations.

A bit about async and buffering

With async operations (ReadAsync/WriteAsync) buffering remains useful, but remember: if you use async methods on top of a buffer, processing still happens in memory and physical disk interaction is further minimized.

In .NET 8+ and .NET 9 buffering is integrated deeper, and most classes now have buffers by default. But for compatibility with network streams or your own implementations it still makes sense to use BufferedStream manually.

You’ll learn more about async at level 58 :P

Visual diagram of buffered streams

flowchart LR
    A[Your code] --> B[StreamReader/Writer]
    B --> C[BufferedStream]
    C --> D[FileStream]
    D --> E[File/Device]
  • A — Your code calling Write/Read.
  • B — High-level stream (works with text or data).
  • C — Buffering (groups data for speed).
  • D — Concrete stream implementation (file, network).
  • E — Physical device (HDD, SSD, network, etc.).

Tips and tricks from practice

  1. If you write one line at a time (e.g., logging), specify a buffer size larger than one line. This lets you flush larger batches faster.
  2. If every action must be written immediately (e.g., critical logs), call Flush() after each write. But that reduces the benefits of buffering!
  3. If you create temporary files that are deleted right after creation, it might not matter if something remains in the buffer — but be careful if it’s important that the file is fully written.
  4. When working with very large files (tens of GB), don’t hesitate to increase buffer size to 1_048_576 bytes (1 MB) or more — just make sure you have enough RAM.

8. Typical mistakes and usage caveats

If you feel excited and want to "stick buffers everywhere" — don’t rush. Everything in moderation!

A common mistake is forgetting to call Flush() or to close the stream when needed. If the stream isn’t closed and the program crashes, the last bytes can remain in RAM and never reach disk. For example, when writing logs and the program crashes, the last entry may be lost.

BufferedStream itself doesn’t "see" the end of your logical messages — it just waits until enough data accumulates. So for critical things (logging, backups, etc.) it’s better to periodically force Flush():

bufferedStream.Flush(); // Forces the buffer to flush to disk

If you use StreamWriter, it has its own buffer! So when nested, buffering happens twice (and that’s not always good). Often one layer of buffering is enough, and if you use StreamWriter, an extra BufferedStream may be unnecessary.

2
Task
C# SELF, level 41, lesson 2
Locked
Copying File Contents Using BufferedStream
Copying File Contents Using BufferedStream
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION