Response compression is a simple way to reduce the amount of data an ASP.NET Core application sends over the network.
A browser might tell the server:
Accept-Encoding: br, gzip
The server can then return a compressed response:
Content-Encoding: br
This can reduce response size and improve transfer time, especially for JSON, HTML, CSS, JavaScript, and other text-based content.
But there is another header that becomes very important when compression is involved:
Vary: Accept-Encoding
The reason is caching.
A response can have different representations depending on what compression formats the client supports. A client that supports Brotli may receive Brotli-compressed content, while another client may receive gzip or an uncompressed response.
If a shared cache or CDN does not understand that difference, it can accidentally serve the wrong representation to another client.
ASP.NET Core 11 changes the response-compression middleware so that it always emits:
Vary: Accept-Encoding
when response compression is enabled, even when the particular response was not compressed. The goal is to make shared caches and CDNs correctly vary their cached representation based on the request's Accept-Encoding header.
This article explains why that matters, how response compression works, and what developers should check when deploying ASP.NET Core behind a CDN or reverse proxy.
Why Response Compression Exists
Consider an API returning a large JSON response:
{
"products": [
{
"id": 1,
"name": "Laptop",
"description": "..."
}
]
}
JSON contains a lot of repeated text and structural characters.
Compression can reduce the amount of data transferred:
Uncompressed
|
| 250 KB
v
Network
Compressed
|
| 45 KB
v
Network
The actual size depends on the response.
The basic idea is that less data has to travel across the network.
ASP.NET Core provides response compression middleware for this purpose.
How Content Negotiation Works
The browser or HTTP client tells the server which content encodings it supports.
For example:
GET /api/products HTTP/1.1
Accept-Encoding: br, gzip
The server chooses a supported encoding.
A response might be:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: br
Vary: Accept-Encoding
Another client might send:
Accept-Encoding: gzip
and receive:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: gzip
Vary: Accept-Encoding
The application returned the same logical resource, but the actual representation is different.
That difference is what Vary communicates to caches.
What Does Vary Mean?
The Vary header tells a cache that the response depends on one or more request headers.
For example:
Vary: Accept-Encoding
means:
This response can be different depending on
the request's Accept-Encoding header.
A cache should therefore avoid treating every request for the same URL as equivalent.
Without Vary:
GET /products
|
v
Cache key:
/products
With:
Vary: Accept-Encoding
the cache can distinguish representations based on the relevant request header:
/products + Accept-Encoding: br
/products + Accept-Encoding: gzip
/products + Accept-Encoding: identity
The exact cache-key implementation is controlled by the caching system, but the Vary header tells it which request headers affect representation selection.
The CDN Problem
This becomes especially important when ASP.NET Core is behind a CDN.
Consider this architecture:
Browser
|
v
CDN
|
v
ASP.NET Core
The CDN may cache the response from ASP.NET Core.
Now suppose the first client supports Brotli:
Accept-Encoding: br, gzip
ASP.NET Core returns:
Content-Encoding: br
The CDN stores the response.
Then another client arrives:
Accept-Encoding: identity
That client does not want a Brotli-compressed response.
If the cache incorrectly treats the two requests as the same representation, it could return the cached Brotli response.
That is the problem Vary: Accept-Encoding helps prevent.
A Simple Example of the Wrong Behavior
Imagine:
Client A
Accept-Encoding: br
requests:
/api/products
The server returns:
Content-Encoding: br
The CDN caches it.
Then:
Client B
Accept-Encoding: gzip
requests the same URL.
If the CDN ignores the representation difference, it might return:
Content-Encoding: br
to Client B.
Client B may not support Brotli.
The result can be a broken or unusable response.
The correct behavior is for the cache to understand that:
Accept-Encoding: br
and:
Accept-Encoding: gzip
represent different variants.
Why ASP.NET Core 11 Changed This
Before .NET 11, response compression middleware added Vary: Accept-Encoding when it actually compressed the response.
ASP.NET Core 11 changes that behavior.
When response compression is enabled, the middleware now adds:
Vary: Accept-Encoding
to every response, including responses that were not compressed.
At first, that may seem unnecessary.
If a particular response was not compressed, why tell the cache to vary by encoding?
The answer is that the cache needs to understand the representation decision for the resource.
A response that happens to be uncompressed does not necessarily mean the resource is independent of Accept-Encoding.
The compression middleware may have considered the request's encoding capabilities and decided not to compress for a specific reason.
For shared caches, explicitly declaring the variation is safer.
Enabling Response Compression
A basic configuration looks like this:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResponseCompression();
var app = builder.Build();
app.UseResponseCompression();
app.MapGet("/api/products", () =>
{
return Results.Ok(new
{
Name = "Laptop",
Price = 75000
});
});
app.Run();
The middleware examines the request and response and determines whether compression should be applied.
For supported content types and encodings, it can compress the response.
Compression Providers
ASP.NET Core supports several compression providers.
Common choices include:
Brotli
Gzip
Zstandard
.NET 11 adds Zstandard support to ASP.NET Core response compression and request decompression. Zstandard is enabled by default in the .NET 11 response-compression setup.
A configuration can explicitly add providers:
builder.Services.AddResponseCompression(options =>
{
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
The client still needs to advertise support for the selected encoding.
For example:
Accept-Encoding: br
allows the server to consider Brotli.
Zstandard in .NET 11
.NET 11 adds Zstandard support to ASP.NET Core response compression.
A configuration can customize its compression quality:
builder.Services.Configure<ZstandardCompressionProviderOptions>(
options =>
{
options.CompressionOptions =
new ZstandardCompressionOptions
{
Quality = 6
};
});
The quality setting represents a trade-off.
Generally:
Lower quality
↓
Less CPU work
↓
Potentially larger output
Higher quality
↓
More CPU work
↓
Potentially smaller output
There is no single best value for every application.
Measure CPU usage and response size using your actual traffic.
Why Compression and Caching Must Be Considered Together
It is easy to configure compression separately from caching:
Compression
|
v
Smaller response
Caching
|
v
Fewer application requests
But once they are combined, the response representation matters.
Consider:
Client
|
| Accept-Encoding
v
CDN
|
| cached representation
v
ASP.NET Core
The CDN may serve the response without contacting ASP.NET Core.
That means the CDN needs enough information to choose the correct representation.
This is where:
Vary: Accept-Encoding
becomes important.
Content-Encoding and Vary Are Different
These two headers are related but do different jobs.
Content-Encoding
Tells the client how the response body is encoded.
For example:
Content-Encoding: br
means the response body is Brotli-compressed.
Vary
Tells caches that the response depends on a request header.
For example:
Vary: Accept-Encoding
means the cache needs to account for the client's Accept-Encoding.
Think of it this way:
Content-Encoding
|
v
How is THIS response encoded?
Vary
|
v
What request information affected
which representation was selected?
Both are important.
What Happens When a Response Is Not Compressed?
This is the interesting part of the .NET 11 change.
Suppose the client sends:
Accept-Encoding: gzip
but the response is too small to make compression worthwhile.
The server may return:
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept-Encoding
There is no:
Content-Encoding: gzip
because the response is not compressed.
But the Vary header is still useful.
It tells shared caches that representation selection depends on the Accept-Encoding request header.
ASP.NET Core 11 now emits this behavior when response compression is enabled.
Why Small Responses May Not Be Compressed
Compression itself has a cost.
The server needs CPU time to compress the response.
For a large JSON document, that cost can be worthwhile:
500 KB
|
v
Compression
|
v
80 KB
For a tiny response:
200 bytes
|
v
Compression
|
v
180 bytes
The savings may not justify the CPU work.
Response compression middleware therefore does not simply compress every response.
That is another reason developers should not assume:
Vary: Accept-Encoding
means:
Content-Encoding exists
They represent different pieces of information.
What This Means for CDN Configuration
If your ASP.NET Core application is behind a CDN, do not assume the CDN automatically handles every compression variation correctly.
Check:
Whether the CDN honors
VaryWhether it compresses responses itself
Whether it decompresses and recompresses responses
Whether it caches compressed representations
Whether origin compression is enabled
Which encodings the CDN supports
How cache keys are constructed
A typical architecture could be:
┌───────────────┐
│ Browser │
└───────┬───────┘
│
│ Accept-Encoding
v
┌───────────────┐
│ CDN │
│ │
│ Cache │
└───────┬───────┘
│
v
┌───────────────┐
│ ASP.NET Core │
│ │
│ Compression │
└───────────────┘
The more layers you add, the more important it becomes to know which layer owns compression.
Do Not Compress Twice
A common deployment mistake is enabling compression at multiple layers without understanding how they interact.
For example:
ASP.NET Core
↓
Brotli compression
CDN
↓
Brotli compression again
A response that is already compressed should not be compressed again using the same mechanism.
In many deployments, it is cleaner to decide whether compression is handled:
Option A
ASP.NET Core
or
Option B
CDN / reverse proxy
rather than blindly enabling it everywhere.
The right choice depends on the infrastructure.
A CDN may be better positioned to handle compression because it can compress once and serve many users.
On the other hand, application-level compression can be useful when the application itself needs to control encoding or when responses bypass the CDN.
Response Compression Behind a Reverse Proxy
A common deployment looks like:
Browser
|
v
Reverse Proxy
|
v
ASP.NET Core
The proxy needs to preserve the relevant headers.
For compression, pay particular attention to:
Accept-Encoding
Content-Encoding
Vary
The ASP.NET Core documentation also notes that reverse-proxy configurations can affect whether the application sees Accept-Encoding. For example, if a proxy removes the header, the ASP.NET Core compression middleware cannot use it to negotiate compression.
This means a local test can work perfectly while the production deployment behaves differently.
Always test through the actual proxy path.
Testing the Header
You can inspect the response using curl.
For example:
curl -I \
-H "Accept-Encoding: br" \
https://localhost:5001/api/products
Look for:
Vary: Accept-Encoding
and, when compression is used:
Content-Encoding: br
Then test without compression support:
curl -I \
-H "Accept-Encoding: identity" \
https://localhost:5001/api/products
The response should not contain:
Content-Encoding: br
The important .NET 11 change is that Vary: Accept-Encoding can still be present even when Content-Encoding is not.
Testing With a CDN
Testing only against localhost is not enough if your production application uses a CDN.
The complete test should look like:
Client
|
v
CDN
|
v
Application
Test at least these cases:
Client A
Accept-Encoding: br
Client B
Accept-Encoding: gzip
Client C
Accept-Encoding: identity
Verify that each client receives a representation it can decode.
Also inspect CDN cache headers.
For example:
Age: 120
or a CDN-specific cache status header can indicate that the response came from a shared cache.
The exact headers depend on your CDN.
A Practical API Example
Suppose you have:
app.MapGet("/api/orders", async (
OrderService service) =>
{
var orders = await service.GetOrdersAsync();
return Results.Ok(orders);
});
Configure compression:
builder.Services.AddResponseCompression();
var app = builder.Build();
app.UseResponseCompression();
app.MapGet("/api/orders", async (
OrderService service) =>
{
var orders = await service.GetOrdersAsync();
return Results.Ok(orders);
});
A Brotli-capable client may receive:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: br
Vary: Accept-Encoding
Another client may receive:
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept-Encoding
The second response is not compressed, but the cache still knows that representation selection can depend on Accept-Encoding.
Vary Can Contain Multiple Headers
Vary is not limited to one header.
You might see:
Vary: Accept-Encoding, Accept-Language
That means the representation can vary based on both:
Accept-Encoding
Accept-Language
For example:
English + Brotli
English + Gzip
French + Brotli
French + Gzip
The number of possible variants can grow quickly.
This is another reason to avoid adding unnecessary values to Vary.
Every additional variation can increase cache fragmentation.
Cache Fragmentation
Suppose a CDN receives many different Accept-Encoding values:
br
gzip
br, gzip
gzip, deflate
identity
Depending on the cache's normalization behavior, you can end up with multiple representations.
That can reduce cache efficiency.
However, removing Vary: Accept-Encoding simply to increase cache hits is not a good solution.
Correctness comes first.
A client must receive a representation it can understand.
The cache should be configured to normalize supported encoding variants where possible.
Vary and Cache-Control Are Different
Do not confuse:
Vary: Accept-Encoding
with:
Cache-Control: max-age=300
Cache-Control controls whether and how a response can be cached.
Vary tells the cache which request headers influence the representation.
For example:
Cache-Control: public, max-age=300
Vary: Accept-Encoding
means roughly:
This response can be shared and cached
for the specified period.
But the cached representation depends
on Accept-Encoding.
Both headers can be useful at the same time.
Response Compression and HTTPS
Compression over HTTPS requires additional security consideration.
ASP.NET Core documentation warns that compression of dynamically generated responses over secure connections can introduce risks associated with attacks such as BREACH.
This does not mean:
HTTPS + compression = always unsafe
It means developers need to consider whether sensitive information and attacker-controlled input can appear together in compressed responses.
For example, be careful with responses that contain:
Secret token
+
User-controlled text
Compression can sometimes leak information through response-size differences.
For public static content, the risk profile is different from dynamic responses containing secrets.
Security review should consider the actual data being returned.
When Should You Use Compression?
Compression is usually useful for text-based responses such as:
JSON
HTML
CSS
JavaScript
SVG
XML
It is generally less useful for content that is already compressed, such as:
JPEG
PNG
WebP
MP4
ZIP
Trying to compress already-compressed content can waste CPU without providing meaningful size reduction.
Middleware Order Matters
Register response compression before middleware that produces the response.
For example:
var app = builder.Build();
app.UseResponseCompression();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
The exact pipeline depends on the application, but compression needs to be positioned so it can process the response body before it is sent.
If the compression middleware is placed incorrectly, you may see that the application returns successful responses but they are not compressed.
Checking the Actual Response
Do not rely only on configuration.
Use browser developer tools or an HTTP client to inspect:
Request:
Accept-Encoding
Response:
Content-Encoding
Vary
Content-Type
Content-Length
For a compressed response:
Accept-Encoding: br, gzip
|
v
Content-Encoding: br
Vary: Accept-Encoding
For an uncompressed response:
Accept-Encoding: br, gzip
|
v
No Content-Encoding
Vary: Accept-Encoding
The second case is particularly relevant to the .NET 11 change.
Migration Considerations for .NET 11
If you are upgrading an existing application to .NET 11, this is one of the behavioral changes worth testing.
The framework now emits:
Vary: Accept-Encoding
more consistently when response compression is enabled. Microsoft lists this as a behavioral change in ASP.NET Core 11.
For most applications, this is a correctness improvement.
But it can affect:
CDN cache keys
Cache hit rates
Reverse proxy behavior
Response-header tests
Integration tests
Snapshot tests
If you have tests that expect the exact response-header set, review them during migration.
Common Mistakes
Removing Vary to Improve Cache Hits
Do not remove:
Vary: Accept-Encoding
just because you want fewer cache variants.
Incorrect caching is worse than a slightly lower cache hit rate.
Assuming Vary Means Compression Happened
It does not.
You need:
Content-Encoding
to determine whether the response body was actually encoded.
Compressing Everything
Already-compressed media usually does not benefit.
Compressing at Every Layer
Decide where compression belongs in your architecture.
Ignoring the CDN
Local testing does not prove that your CDN caches compressed and uncompressed responses correctly.
Ignoring HTTPS Compression Risks
Review whether sensitive dynamic content is being compressed.
Not Testing Different Clients
At minimum, test clients that request:
br
gzip
identity
Production Checklist
Before deploying ASP.NET Core response compression behind a CDN, verify:
Response compression is enabled intentionally.
Supported encodings are understood by your clients.
Vary: Accept-Encodingis preserved.CDN behavior respects
Vary.Reverse proxies preserve
Accept-Encodingwhere required.You are not compressing responses twice.
Already-compressed content is excluded where appropriate.
Dynamic sensitive responses have been reviewed for compression risks.
Cache hit rates are monitored.
Different encoding requests return usable responses.
Response-header integration tests cover compression behavior.
.NET 11 migration tests account for the new
Varybehavior.
Summary
The Vary: Accept-Encoding header can look like a small HTTP detail, but it becomes important as soon as response compression and shared caching are used together.
A response can have multiple valid representations. One client may accept Brotli, another may accept gzip, and another may request an uncompressed response. A CDN or proxy needs to know that these requests cannot always share the same cached representation.
ASP.NET Core 11 makes this safer by having the response-compression middleware emit Vary: Accept-Encoding whenever compression is enabled, even when a particular response is not actually compressed.
The practical lesson is simple: do not look at compression and caching as two completely separate features. If your application sits behind a CDN, reverse proxy, or shared cache, response headers become part of the caching contract.
Use Content-Encoding to understand how the current response body is encoded, and use Vary: Accept-Encoding to tell caches that the representation can depend on what the client supports.
For developers upgrading to .NET 11, this is mostly a positive change, but it is still worth checking CDN configuration, cache behavior, and tests that make assumptions about response headers.

Join the conversation! Your thoughts help the community grow.