Every .NET developer has had the same conversation with themselves at least once: "Should I use GZip or Brotli?" GZip is fast to compress but produces larger output. Brotli squeezes harder but burns more CPU on the compression side. You pick one, wire it up in your response compression middleware, and move on — until the next performance review reveals your API latency is dominated by compression overhead on high-throughput endpoints.

.NET 11 introduces a third option that changes the calculus entirely. Zstandard (often abbreviated as zstd) is now a first-class citizen in System.IO.Compression, shipped inbox with the runtime. No NuGet packages, no P/Invoke wrappers, no third-party bindings. The new APIs cover streaming compression, one-shot operations, dictionary-based compression, and direct integration with ASP.NET Core's response compression middleware and HttpClient.

Why Zstandard matters

Zstandard was developed at Meta (then Facebook) and open-sourced in 2016. It was designed from the ground up to offer compression ratios comparable to zlib whilst being dramatically faster at both compression and decompression. In practice, Zstandard at its default compression level compresses roughly 40% faster than Brotli at equivalent ratio, and decompresses significantly faster than both Brotli and GZip.

The algorithm supports compression levels from 1 (fastest, lowest ratio) to 22 (slowest, highest ratio), with a default around level 3. Even at low levels, Zstandard typically matches or beats GZip's compression ratio. At higher levels, it approaches Brotli territory — but without the punishing compression times that make Brotli impractical for real-time compression of API responses.

For .NET developers, the practical implication is straightforward: Zstandard is the best default choice for scenarios where you compress and decompress on the fly, such as HTTP responses, message queues, and data pipelines.

The API surface

The Zstandard APIs live in the System.IO.Compression namespace, shipped in the System.IO.Compression.Zstandard assembly. The design mirrors the existing Brotli APIs, so if you have used BrotliStream, BrotliEncoder, or BrotliDecoder, you will feel immediately at home.

Streaming with ZstandardStream

ZstandardStream is the primary entry point for most scenarios. It wraps a Stream and handles compression or decompression transparently, just like GZipStream or BrotliStream.

Services/FileCompressor.cs
public class FileCompressor
{
    public async Task CompressFileAsync(string sourcePath, string destPath)
    {
        await using var sourceStream = File.OpenRead(sourcePath);
        await using var destStream = File.Create(destPath);
        await using var zstdStream = new ZstandardStream(destStream, CompressionLevel.Optimal);

        await sourceStream.CopyToAsync(zstdStream);
    }

    public async Task DecompressFileAsync(string compressedPath, string destPath)
    {
        await using var sourceStream = File.OpenRead(compressedPath);
        await using var zstdStream = new ZstandardStream(sourceStream, CompressionMode.Decompress);
        await using var destStream = File.Create(destPath);

        await zstdStream.CopyToAsync(destStream);
    }
}

The constructor overloads follow the same patterns you already know:

// TIP

Use CompressionLevel.Optimal for most scenarios. It maps to a Zstandard level that balances speed and ratio well. Reserve SmallestSize for offline or batch compression where latency is not a concern.

One-shot compression with ZstandardEncoder and ZstandardDecoder

For scenarios where you have the entire payload in memory — compressing a serialised message before pushing it to a queue, for example — the streaming API adds unnecessary overhead. ZstandardEncoder and ZstandardDecoder offer span-based, allocation-friendly alternatives.

Services/MessageCompressor.cs
public static class MessageCompressor
{
    public static byte[] Compress(ReadOnlySpan<byte> data)
    {
        var maxLength = ZstandardEncoder.GetMaxCompressedLength(data.Length);
        var buffer = new byte[maxLength];

        ZstandardEncoder.TryCompress(data, buffer, out int bytesWritten);

        return buffer[..bytesWritten];
    }

    public static byte[] Decompress(ReadOnlySpan<byte> compressed, int originalLength)
    {
        var buffer = new byte[originalLength];

        ZstandardDecoder.TryDecompress(compressed, buffer, out int bytesWritten);

        return buffer[..bytesWritten];
    }
}

The static TryCompress and TryDecompress methods are ideal for hot paths. They operate on spans, avoid stream allocations, and return a boolean indicating success. If the destination buffer is too small, they return false rather than throwing.

For more control, create an instance of ZstandardEncoder with a specific compression level:

Example.cs
using var encoder = new ZstandardEncoder(compressionLevel: 6);
encoder.Compress(source, destination, out int bytesRead, out int bytesWritten, isFinalBlock: true);

The instance-based API also supports Flush(), Reset(), and SetSourceLength() for advanced streaming scenarios where you feed data in chunks.

Dictionary compression

This is where Zstandard truly distinguishes itself from GZip and Brotli. When compressing many small, structurally similar payloads — JSON API responses with the same schema, log entries with common prefixes, message queue events — a trained dictionary can dramatically improve compression ratios.

The idea is simple: you train a dictionary on a representative sample of your data, and the compressor uses it as a shared context. Field names, common strings, and structural patterns that appear in every payload get compressed essentially for free.

Services/DictionaryTrainer.cs
public static class DictionaryTrainer
{
    public static ZstandardDictionary TrainFromSamples(IReadOnlyList<byte[]> samples)
    {
        // Concatenate all samples into a single buffer
        var totalLength = samples.Sum(s => s.Length);
        var concatenated = new byte[totalLength];
        var sizes = new int[samples.Count];
        var offset = 0;

        for (int i = 0; i < samples.Count; i++)
        {
            samples[i].CopyTo(concatenated.AsSpan(offset));
            sizes[i] = samples[i].Length;
            offset += samples[i].Length;
        }

        // Train a dictionary (target size: 16 KB)
        return ZstandardDictionary.Train(concatenated, sizes, maxDictionaryLength: 16 * 1024);
    }
}

Once trained, the dictionary is used by both the encoder and decoder:

Example.cs
// Compress with dictionary
using var encoder = new ZstandardEncoder(dictionary, compressionLevel: 3);
encoder.TryCompress(data, buffer, out int bytesWritten);

// Decompress with dictionary — the same dictionary must be used
using var decoder = new ZstandardDecoder(dictionary);
decoder.TryDecompress(compressed, buffer, out int decompressedBytes);

The dictionary can also be passed directly to ZstandardStream:

Example.cs
await using var zstdStream = new ZstandardStream(
    baseStream,
    CompressionMode.Compress,
    dictionary,
    leaveOpen: true);

// WARNING

The compressor and decompressor must use the same dictionary. If you deploy a new dictionary version, you need a strategy for handling payloads compressed with the old one. A common approach is to version your dictionaries and include the version identifier alongside the compressed data.

ASP.NET Core integration

.NET 11 makes Zstandard the highest-priority compression provider in ASP.NET Core's response compression middleware. When a client sends Accept-Encoding: zstd, br, gzip, the middleware will prefer Zstandard over Brotli, and Brotli over GZip.

If you are already using response compression, the upgrade path is straightforward:

Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    // Zstandard is included by default in .NET 11
    // The priority order is: zstd > br > gzip
});

var app = builder.Build();
app.UseResponseCompression();

No additional providers need to be registered. The middleware detects zstd in the Accept-Encoding header and uses ZstandardStream internally. If the client does not support Zstandard, it falls back to Brotli or GZip as before.

HttpClient automatic decompression

On the client side, HttpClientHandler now supports DecompressionMethods.Zstandard:

Program.cs
var handler = new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.All // Includes Zstandard
};

var client = new HttpClient(handler);
var response = await client.GetStringAsync("https://api.example.com/data");

DecompressionMethods.All now includes Zstandard alongside GZip, Deflate, and Brotli. The client will send Accept-Encoding: zstd, br, gzip, deflate and automatically decompress whichever encoding the server chooses.

// NOTE

If you are using IHttpClientFactory (which you should be), the decompression handler can be configured via ConfigurePrimaryHttpMessageHandler.

Advanced options with ZstandardCompressionOptions

For scenarios that need more than a compression level, ZstandardCompressionOptions exposes Zstandard's advanced parameters:

Services/HighThroughputCompressor.cs
var options = new ZstandardCompressionOptions
{
    CompressionLevel = 1,
    WindowLog = 20,             // 1 MB window (2^20 bytes)
    EnableLongDistanceMatching = true,
    AppendChecksum = true
};

await using var zstdStream = new ZstandardStream(baseStream, options, leaveOpen: false);

Key properties include:

When to use which compression

Not every scenario calls for the same algorithm. Here is a practical decision framework:

Scenario Recommended Why
API responses (real-time) Zstandard Best speed-to-ratio balance for on-the-fly compression
Static assets (pre-compressed) Brotli Compression time is irrelevant; smallest output wins
Legacy client support GZip Universal support across all HTTP clients and browsers
Message queues / event streams Zstandard with dictionary Small payloads with shared structure benefit hugely from dictionaries
Log archival Zstandard (high level) Levels 15-19 approach Brotli ratios with better decompression speed

// TIP

For static files served by a CDN or reverse proxy, continue using Brotli. Pre-compressing assets during your build pipeline means you pay the compression cost once and serve the smallest possible files. Zstandard shines when compression happens on every request.

Common pitfalls

Forgetting to flush or dispose. ZstandardStream buffers data internally. If you write to it and then read the underlying stream without disposing (or at least flushing) the ZstandardStream, you will get truncated or corrupt output. Always await using or call FlushAsync().

Using SmallestSize for real-time responses. CompressionLevel.SmallestSize maps to Zstandard's highest compression levels. On a busy API server, this will consume far more CPU than necessary. Stick with Optimal or Fastest for real-time workloads.

Mismatched dictionaries. If the decompressor does not have the same dictionary that was used for compression, decompression will fail. This is not a graceful degradation — it is an error. Version your dictionaries and store them somewhere both producer and consumer can access.

Assuming all clients support zstd. Browser support for Content-Encoding: zstd is relatively recent. Chrome added support in version 123 (March 2024), Firefox in 126, and Safari in 18. If you serve a mix of modern and older clients, ensure your middleware falls back to Brotli or GZip gracefully — which the default ASP.NET Core configuration already handles.

Ignoring the NuGet package on older targets. The inbox System.IO.Compression.Zstandard assembly ships with .NET 11. If you need Zstandard on .NET 10 or earlier, you will still need a third-party package such as ZstdNet or ZstdSharp.

Summary