Site icon GetPageSpeed

NGINX Compression Vary: Fix Broken Vary Headers

NGINX Compression Vary Module: Fix Duplicate and Broken Vary Headers

The NGINX compression vary module is a drop-in replacement for the standard gzip_vary directive. It fixes a long-standing problem with how NGINX handles the Vary response header when compression is active.

When you enable compression in NGINX, the gzip_vary on directive adds Vary: Accept-Encoding to tell caches that the response differs based on encoding. However, this directive has a significant flaw: it does not merge with existing Vary headers from upstream applications. If your backend already sends Vary: Cookie, NGINX creates a second, separate Vary header instead of combining them. This breaks CDN caching and causes cache fragmentation.

The NGINX compression vary module solves this by merging, consolidating, and deduplicating Vary values into a single, clean header. It works with gzip, Brotli, and Zstandard compression.

The Problem with gzip_vary

Consider a reverse proxy setup where NGINX compresses responses from an upstream application. The upstream sends Vary: Cookie, and NGINX has gzip_vary on.

Here is what happens with the native gzip_vary directive:

$ curl -sI -H "Accept-Encoding: gzip" http://example.com/
Vary: Accept-Encoding
Vary: Cookie

NGINX outputs two separate Vary headers. While RFC 9110 allows multiple Vary headers, many CDNs handle this inconsistently. Some treat multiple Vary headers as a single list. Others ignore the second header or create separate cache entries for each combination.

The result is unpredictable behavior:

Multiple Upstream Vary Headers Make It Worse

Modern web applications often set several Vary values. An upstream might send:

Vary: Cookie
Vary: Accept-Language
Vary: Accept-Encoding

With gzip_vary on, NGINX adds yet another Vary: Accept-Encoding:

Vary: Accept-Encoding
Vary: Cookie
Vary: Accept-Language
Vary: Accept-Encoding

Four separate headers with a duplicate Accept-Encoding. This wastes bandwidth and confuses caches.

How the Module Solves This

The NGINX compression vary module is a header output filter. Instead of blindly adding a new Vary header, it does the following:

  1. Collects all existing Vary headers from the response
  2. Parses them into individual values
  3. Removes duplicates using case-insensitive comparison
  4. Appends Accept-Encoding only if not already present
  5. Outputs a single, consolidated Vary header

Using the same scenario from above, the module produces:

$ curl -sI -H "Accept-Encoding: gzip" http://example.com/
Vary: Cookie, Accept-Encoding

A single, clean header. For the multiple-header scenario:

$ curl -sI -H "Accept-Encoding: gzip" http://example.com/
Vary: Cookie, Accept-Language, Accept-Encoding

All values in one header with no duplicates.

How It Works Internally

The module registers itself as a header output filter in the NGINX filter chain. When a response passes through, it scans all outgoing headers for Vary entries. Each Vary header is tokenized by splitting on commas and whitespace. The tokens are collected into an array and compared case-insensitively to eliminate duplicates.

After processing, the module checks whether compression is active by reading the internal gzip_vary flag. This flag is set by NGINX’s built-in gzip module as well as by third-party modules like Brotli and Zstandard. If compression is active and Accept-Encoding is not already in the collected tokens, the module appends it.

Finally, all original Vary headers are removed from the response, and a single new Vary header is emitted with the deduplicated values separated by commas.

Supported Compression Modules

The NGINX compression vary module works with all major compression modules:

Module Directives
gzip (native) gzip, gzip_static, gunzip
Brotli (third-party) brotli, brotli_static, unbrotli
Zstandard (third-party) zstd, zstd_static, unzstd

You do not need separate configuration for each compression type. A single compression_vary on directive covers them all.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux, and Amazon Linux

Enable the GetPageSpeed repository, then install the module:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-compression-vary

Load the module by adding this line at the top of /etc/nginx/nginx.conf:

load_module modules/ngx_http_compression_vary_filter_module.so;

Debian and Ubuntu

First, set up the GetPageSpeed APT repository, then install:

sudo apt-get update
sudo apt-get install nginx-module-compression-vary

On Debian/Ubuntu, the package handles module loading automatically. No load_module directive is needed.

For more details, see the compression-vary module package page.

Configuration

The module provides a single directive.

compression_vary

Syntax: compression_vary on | off;
Default: compression_vary off;
Context: http, server, location

Enables or disables enhanced Vary header handling. When enabled, disable gzip_vary to avoid conflicts.

Basic Setup

A minimal configuration with gzip compression:

http {
    gzip on;
    gzip_types text/plain text/css application/json application/javascript;
    compression_vary on;

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass http://backend;
        }
    }
}

With Multiple Compression Algorithms

When using both gzip and Brotli:

http {
    gzip on;
    gzip_types text/plain text/css application/json application/javascript;

    brotli on;
    brotli_types text/plain text/css application/json application/javascript;

    compression_vary on;

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass http://backend;
        }
    }
}

One directive handles Vary for all compression modules simultaneously.

Per-Location Control

Enable or disable the module at different levels of the configuration:

http {
    gzip on;
    compression_vary on;

    server {
        listen 80;

        location /api/ {
            compression_vary off;
            proxy_pass http://api-backend;
        }

        location / {
            proxy_pass http://web-backend;
        }
    }
}

Testing Your Setup

Verify Configuration Syntax

sudo nginx -t

Reload NGINX

sudo systemctl reload nginx

Check the Vary Header

Test with a compressed request to verify correct behavior:

curl -sI -H "Accept-Encoding: gzip" https://example.com/ | grep -i vary

You should see a single, merged Vary header:

Vary: Cookie, Accept-Encoding

Multiple separate Vary headers mean the module is not active. Check for conflicting gzip_vary directives.

Test Without Compression

Verify that Vary is sent even without requesting compression:

curl -sI https://example.com/ | grep -i vary

The response should still include Vary: Accept-Encoding. Caches need to know the response could vary by encoding, even when the current response is uncompressed. This tells downstream caches to store the response separately based on the Accept-Encoding request header.

Why This Matters for CDN Performance

The Vary header controls how CDNs store and serve content. Getting it right directly improves cache hit rates and reduces origin server load.

Cache Key Construction

CDNs use Vary to construct cache keys. A Vary: Accept-Encoding header tells the CDN to store separate copies for gzip, Brotli, and uncompressed responses. This is correct and expected behavior.

However, duplicated or split Vary headers cause problems:

How CDNs Process Multiple Vary Headers

Different CDN providers handle multiple Vary headers differently. Cloudflare normalizes them into a single header, but not all providers do this. Fastly and AWS CloudFront may treat each Vary header independently when constructing cache keys, potentially creating unnecessary cache variants. By consolidating Vary headers at the origin, you remove this ambiguity entirely and ensure consistent caching behavior regardless of which CDN sits in front of your server.

Standards Compliance

RFC 9110 Section 12.5.5 defines the Vary header. The RFC recommends combining field values into a single header. The NGINX compression vary module ensures your responses follow this best practice automatically.

When to Use This Module

Use the NGINX compression vary module when:

The module has near-zero overhead. It processes only the Vary headers — no body processing, no additional I/O, no external dependencies.

Migrating from gzip_vary

Switching is straightforward:

  1. Install the module
  2. Replace gzip_vary on; with compression_vary on;
  3. Remove all gzip_vary directives
  4. Test with nginx -t and reload
# Before
gzip on;
gzip_vary on;

# After
gzip on;
compression_vary on;

Both directives behave identically when there are no upstream Vary headers. The module adds value when upstream headers need merging. There is no downside to using it in all cases, and it future-proofs your configuration.

Troubleshooting

Multiple Vary Headers Still Appear

Check for gzip_vary on directives that conflict with compression_vary:

grep -rn "gzip_vary" /etc/nginx/

Remove all gzip_vary directives and reload NGINX.

No Vary Header at All

Verify that compression is enabled. The module only adds Accept-Encoding when a compression module is active. Ensure gzip on or brotli on is configured for the relevant location.

Unknown Directive Error

If nginx -t fails with unknown directive "compression_vary":

ls /usr/lib64/nginx/modules/ngx_http_compression_vary_filter_module.so

Conclusion

The NGINX compression vary module is a small but impactful upgrade over the native gzip_vary directive. It merges, consolidates, and deduplicates Vary headers so that CDNs and caching proxies handle compressed content correctly.

For production servers behind a CDN or caching proxy, switching from gzip_vary to compression_vary eliminates a common source of caching inefficiency with zero performance cost.

The module source code is available on GitHub. You can also check your website’s compression and caching headers with the GetPageSpeed speed checker.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Exit mobile version