# ASP.NET Core 11 Zstandard Compression for HTTP Requests and Responses

> ASP.NET Core 11 can send a Zstandard-compressed CSV and read it back through an import endpoint. The client uses different HTTP headers for the download and the upload.

Published: 2026-11-13. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/aspnetcore-11-http-zstd

[**ASP.NET Core in .NET 11**](https://milanjovanovic.tech/blog/whats-new-dotnet-11) adds Zstandard to its response compression and request decompression middleware.
A client can ask for a compressed CSV with `Accept-Encoding: zstd` and upload the same bytes with `Content-Encoding: zstd`.
In both cases the endpoint handles ordinary text.

## Which Direction Is Compressed?

On the download, the client advertises which encodings it can read.
On the upload, it declares how it encoded the file it sent.
Sending `Accept-Encoding` alone on the upload leaves the server with no indication that the request body needs decoding.

| Operation | Request header | What crosses the wire |
| --- | --- | --- |
| GET the export uncompressed | No Accept-Encoding header | Plain CSV in this example |
| GET the export using Zstandard | Accept-Encoding: zstd | A CSV response with Content-Encoding: zstd |
| POST an already compressed CSV | Content-Encoding: zstd | Compressed request bytes. The handler reads CSV. |

Zstandard is a [**built-in provider in ASP.NET Core 11**](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11?view=aspnetcore-11.0#zstandard-response-compression-and-request-decompression).
Register the services and add the middleware in `Program.cs`.

## Wire Up a Download and an Import

Create the project with `dotnet new web -n HttpOrders -f net11.0 --no-https` and replace `Program.cs` with this code.
The download serves CSV as `text/plain`, a MIME type the middleware compresses by default.

```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResponseCompression();
builder.Services.AddRequestDecompression();

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

app.MapGet("/orders.csv", () =>
    Results.Text("id,total\n1,42\n2,18\n", "text/plain"));

app.MapPost("/imports", async (HttpRequest request) =>
{
    using var reader = new StreamReader(request.Body);
    var count = 0;
    while (await reader.ReadLineAsync() is not null)
    {
        count++;
    }

    return Results.Text($"{count} lines");
});

app.Run();
```

From the project directory, run `dotnet run --no-launch-profile --urls http://127.0.0.1:48719`.
The demo runs over HTTP on localhost.
The middleware skips HTTPS responses by default.

## Check the Wire Bytes With curl

In a second terminal, download the CSV twice and compare the files after decoding the second one.
You'll need `curl`, `cmp`, and the `zstd` CLI.

```bash
curl -fsS http://127.0.0.1:48719/orders.csv -o plain.csv
curl -fsS -D response.headers -H 'Accept-Encoding: zstd' \
  http://127.0.0.1:48719/orders.csv -o response.zst
zstd -dc response.zst | cmp - plain.csv
```

Kestrel sent `Content-Encoding: zstd` and `Vary: Accept-Encoding` on the second response.
The first had no `Content-Encoding` header.
After decoding, `cmp` found the files identical.

The original CSV was **19 bytes**.
The compressed version was **31 bytes**, so this export got larger.
Compare both sizes on a full order export before you enable compression for it.

Leave `curl --compressed` off here.
It tells curl to decode the response, which would leave plain CSV in the file named `response.zst`.

## Send Those Bytes Back to the API

Send `response.zst` back unchanged, with `Content-Type` set to the type of the original file.

```bash
curl -fsS -H 'Content-Encoding: zstd' -H 'Content-Type: text/csv' \
  --data-binary @response.zst http://127.0.0.1:48719/imports
```

The endpoint returns `3 lines` for the header and two orders.
As the handler reads `HttpRequest.Body`, the [**request decompression middleware**](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/request-decompression?view=aspnetcore-11.0) decodes the stream.
Set a request body limit that accounts for how large an upload can get **after decompression**.

## Why Might Production Still Return Plain Text?

Start at the reverse proxy.
It may handle compression itself or change the headers it forwards to Kestrel.
If Kestrel handles compression, check the `Accept-Encoding` header it receives, the response MIME type, and whether the app has replaced the default providers.

A second difference from localhost is HTTPS.
The [**response compression middleware**](https://learn.microsoft.com/en-us/aspnet/core/performance/response-compression?view=aspnetcore-11.0) skips HTTPS by default because compressing a response that contains both a secret and user-controlled content can leak information about the secret.

## Summary

Keep the download and upload as a pair of checks when a proxy or partner client changes.
The download should have a `Content-Encoding: zstd` header and decode to the original CSV.
Sending those same compressed bytes back should give you the original line count.

If the partner wants to keep a `.zst` archive after the HTTP transfer, create the file with [**ZstandardStream**](https://milanjovanovic.tech/blog/dotnet-11-zstandard-compression).

## Frequently asked questions

### Does installing ASP.NET Core 11 automatically compress HTTP traffic?

No. Zstandard is a built-in provider in ASP.NET Core 11. Register the compression services and add the middleware. For a response, the client must advertise a supported encoding and the response must meet the middleware rules.

### Which header should I send for a Zstandard upload?

Send Content-Encoding: zstd for a body you have already compressed. Keep the Content-Type of the underlying data, such as text/csv. Accept-Encoding describes which encodings you can receive in the response.

### What does a Minimal API receive when request decompression is enabled?

For a supported single encoding, the middleware wraps HttpRequest.Body with a decoder and removes the consumed Content-Encoding header before the endpoint runs. Reading the stream yields the original content.
