Compression is often introduced into an application for one simple reason: reduce the amount of data that needs to be stored or transferred.
The engineering trade-off is more complicated.
Compression consumes CPU and can introduce additional memory and latency costs. A highly compressed payload may save network bandwidth while requiring substantially more work to produce. A faster compression configuration may reduce CPU usage while producing a larger output.
.NET 11 adds native Zstandard support to System.IO.Compression, alongside existing compression technologies such as GZip, Brotli, Deflate, and ZLib. The new APIs include ZstandardStream, ZstandardEncoder, ZstandardDecoder, compression options, and dictionary support.
That makes Zstandard an interesting option for .NET applications handling:
Large data exports
HTTP payloads
Logs
Event streams
Object storage
Backups
Data pipelines
High-throughput services
Internal service-to-service communication
But adding another compression algorithm does not automatically make an application faster.
The useful question is:
How does Zstandard behave for the specific data, CPU budget, and throughput requirements of an application?
What Is Zstandard?
Zstandard, commonly called zstd, is a general-purpose lossless compression format developed by Facebook/Meta.
Its design focuses on providing configurable compression while maintaining fast decompression.
The practical trade-off looks like this:
Less Compression
|
v
More Speed
|
v
Larger Output
versus:
More Compression
|
v
More CPU Work
|
v
Smaller Output
The correct point on this curve depends on the workload.
For a network-heavy application, smaller payloads may be valuable.
For a CPU-constrained service, compression speed may be more important.
Zstandard Support in .NET 11
.NET 11 exposes Zstandard through System.IO.Compression.
A stream-based example is:
using System.IO.Compression;
await using var output =
File.Create("data.zst");
await using var zstd =
new ZstandardStream(
output,
CompressionLevel.Fastest);
await input.CopyToAsync(zstd);
The API follows the same general stream model used by other .NET compression implementations. Microsoft documents constructors for compression and decompression as well as configurable Zstandard options.
This is important for existing .NET applications because developers can use the familiar Stream abstraction rather than introducing a completely separate compression programming model.
Why Benchmark Compression?
Compression performance has multiple dimensions.
A useful benchmark should measure at least:
Compression speed
Decompression speed
Compressed size
CPU consumption
Memory usage
Allocation behavior
For example:
Input
|
v
Compression
|
+---- CPU
+---- Memory
+---- Time
|
v
Compressed Output
Optimizing only one measurement can produce the wrong architecture.
Compression Ratio vs Compression Speed
One of the most important metrics is the compression ratio.
A simple definition is:
Compression Ratio =
Uncompressed Size / Compressed Size
For example, if:
Uncompressed = 10 MB
Compressed = 2 MB
then:
Compression Ratio = 5
The higher the ratio, the smaller the resulting compressed representation.
However, ratio alone does not tell you whether the configuration is suitable.
Consider:
Configuration A
Small output
High CPU cost
Configuration B
Larger output
Low CPU cost
If the application is CPU-bound, B may be preferable.
If the application is bandwidth-constrained, A may be preferable.
Compare Zstandard With Existing Algorithms
A benchmark should not evaluate Zstandard in isolation.
.NET applications already have several compression choices.
| Algorithm | .NET API | Typical Consideration |
|---|---|---|
| GZip | GZipStream | Broad compatibility |
| Deflate | DeflateStream | General-purpose compression |
| Brotli | BrotliStream | Strong web-oriented use cases |
| ZLib | ZLibStream | zlib-compatible workloads |
| Zstandard | ZstandardStream | New high-performance general-purpose option |
The correct choice depends on the application and interoperability requirements.
A benchmark should therefore compare algorithms using the same input data and equivalent test conditions.
Create a Representative Dataset
Compression results are heavily influenced by the input.
Do not benchmark only:
Hello world
or:
AAAAAAAAAAAAAAAAAAAA
Highly repetitive data can produce results that are irrelevant to real applications.
Use representative datasets such as:
JSON
CSV
Logs
XML
Binary records
Application traces
Database exports
For example:
{
"id": 1001,
"customer": "Alice",
"status": "active",
"orders": [
{
"id": 5001,
"amount": 125.50
}
]
}
A real application should use production-shaped data where possible.
Create Multiple Dataset Sizes
One dataset size is not enough.
Use several workload sizes:
| Dataset | Purpose |
|---|---|
| Small | Per-operation overhead |
| Medium | Typical payload |
| Large | Throughput |
| Very large | Sustained processing |
For example:
100 KB
1 MB
10 MB
100 MB
The actual sizes should reflect the application being tested.
Do not assume that an algorithm's behavior at 100 KB represents its behavior on a 1 GB export.
Benchmark With BenchmarkDotNet
Create a dedicated benchmark project:
dotnet new console -n CompressionBenchmarks
cd CompressionBenchmarks
dotnet add package BenchmarkDotNet
Then create the benchmark class:
using BenchmarkDotNet.Attributes;
[MemoryDiagnoser]
public class CompressionBenchmark
{
private byte[] _data = [];
[Params(100_000, 1_000_000, 10_000_000)]
public int Size { get; set; }
[GlobalSetup]
public void Setup()
{
_data = GenerateData(Size);
}
private static byte[] GenerateData(int size)
{
var data = new byte[size];
Random.Shared.NextBytes(data);
return data;
}
}
This creates a baseline for testing different payload sizes.
However, random bytes are generally difficult to compress.
For a realistic compression experiment, generate datasets with characteristics similar to the application's actual data.
Benchmark Zstandard Compression
A basic benchmark can use ZstandardStream:
[Benchmark]
public byte[] Zstandard()
{
using var output =
new MemoryStream();
using (var zstd =
new ZstandardStream(
output,
CompressionLevel.Fastest))
{
zstd.Write(_data);
}
return output.ToArray();
}
The benchmark measures the complete operation:
Input
|
v
Zstandard
|
v
MemoryStream
|
v
byte[]
This is useful for an initial experiment.
However, returning the resulting byte[] also introduces a copy and allocation.
For a precise production study, measure the exact API pattern used by the application.
Benchmark GZip
Create a comparable benchmark:
[Benchmark]
public byte[] Gzip()
{
using var output =
new MemoryStream();
using (var gzip =
new GZipStream(
output,
CompressionLevel.Fastest))
{
gzip.Write(_data);
}
return output.ToArray();
}
The important point is consistency.
Use:
Same input
Same output destination
Same compression level semantics where comparable
Same measurement environment
Do not change several variables between tests.
Benchmark Brotli
You can also include Brotli:
[Benchmark]
public byte[] Brotli()
{
using var output =
new MemoryStream();
using (var brotli =
new BrotliStream(
output,
CompressionLevel.Fastest))
{
brotli.Write(_data);
}
return output.ToArray();
}
This provides a three-way comparison:
Zstandard
vs
GZip
vs
Brotli
The result should be interpreted as workload-specific rather than as a universal ranking.
Measure Compressed Size
Execution time alone is insufficient.
Capture the output size:
private static long CompressWithZstd(
byte[] data)
{
using var output =
new MemoryStream();
using (var zstd =
new ZstandardStream(
output,
CompressionLevel.Fastest))
{
zstd.Write(data);
}
return output.Length;
}
Then calculate:
double ratio =
(double)data.Length /
compressedLength;
This gives the compression ratio.
Measure Compression Throughput
A useful metric is:
Throughput =
Input Bytes / Compression Time
For example:
Throughput = MB processed per second
BenchmarkDotNet can provide execution time, after which throughput can be derived from the known input size.
Do not calculate throughput from one manual stopwatch measurement.
Use multiple benchmark iterations.
Benchmark Decompression Separately
Compression and decompression have different characteristics.
First create a compressed payload:
private byte[] Compress()
{
using var output =
new MemoryStream();
using (var zstd =
new ZstandardStream(
output,
CompressionLevel.Fastest))
{
zstd.Write(_data);
}
return output.ToArray();
}
Then benchmark decompression:
[Benchmark]
public byte[] ZstandardDecompress()
{
using var input =
new MemoryStream(_compressed);
using var zstd =
new ZstandardStream(
input,
CompressionMode.Decompress);
using var output =
new MemoryStream();
zstd.CopyTo(output);
return output.ToArray();
}
This separates:
Compression Cost
from:
Decompression Cost
That distinction matters for client/server systems.
Why Decompression Matters
Suppose a server compresses every response but thousands of clients decompress them.
The cost distribution becomes:
Server
Compression
|
v
Network
|
v
Many Clients
Decompression
A compression strategy should therefore consider both ends of the pipeline.
For distributed systems, measure:
Server CPU
+
Client CPU
+
Network Bytes
+
End-to-End Latency
Benchmark Streaming
Compression is often used with streams rather than complete byte arrays.
For example:
await using var output =
File.Create("archive.zst");
await using var zstd =
new ZstandardStream(
output,
CompressionLevel.Fastest);
await input.CopyToAsync(zstd);
This is fundamentally different from:
byte[] data =
await File.ReadAllBytesAsync(
"large-file.dat");
Join the conversation! Your thoughts help the community grow.