Era Host hosting
EraHost – Free Domain, Cheap Hosting!
Client Area
Support 24/7
Menu

Vary: Accept-Encoding and Vary: Accept in Apache, Nginx, and cPanel

29 min read
07.05.2026

A server can return two different responses for the same URL without anything being wrong. One client may receive a gzip-compressed CSS file, another may receive the uncompressed version, and an API may return JSON or HTML depending on the request headers. The problem starts when a browser, reverse proxy, or CDN caches one representation and later reuses it for a request that should have received another.

The Vary response header exists to prevent that mix-up. If compression is selected according to Accept-Encoding, the response normally needs Vary: Accept-Encoding. If the media representation is selected according to Accept, the relevant field is Vary: Accept. When both request headers influence the response, both can appear in Vary.

The harder part on a real hosting stack is deciding who should generate the header. Apache may already add it through mod_deflate, mod_brotli, content negotiation, or rewrite logic. Nginx has gzip_vary. PHP or the application can emit Vary itself. cPanel can place Nginx in front of Apache, and a CDN may add another cache and compression layer above both. Pasting another Header append or add_header directive into that chain can turn a correct setup into one with duplicate or misleading cache metadata.

Quick answer: use Vary: Accept-Encoding when the selected content encoding changes because of Accept-Encoding. Use Vary: Accept when the returned media representation changes because of Accept. Before adding either field manually, verify which component selects the representation and whether that component already sends the correct Vary.

What does the Vary header actually tell a browser, proxy, or CDN?

Consider one URL requested twice. The first client sends:

GET /assets/app.css
Accept-Encoding: gzip

and receives:

HTTP/1.1 200 OK
Content-Type: text/css
Content-Encoding: gzip
Vary: Accept-Encoding

The second client asks for the same URL with:

GET /assets/app.css
Accept-Encoding: identity

and receives the CSS without Content-Encoding: gzip. Same URL, different representation. A shared cache must know that the value of Accept-Encoding affected the response; otherwise it can reuse a stored representation for a client that should have received another one.

That is what Vary describes. It does not compress a response, convert an image, choose JSON, or enable caching. It tells caches which request-header values mattered when the representation was selected.

Why can one URL have several cached representations?

Compression is the simplest example, but the same principle applies elsewhere. An image endpoint can return AVIF to one client and JPEG to another. An API can return JSON or HTML from the same URL. A language-negotiated page can return different content according to Accept-Language.

The response headers describe different parts of that process:

Header What it tells you Typical example
Content-Encoding How the response body is encoded gzip, br
Content-Type The selected media type text/html, application/json
Vary Which request headers influenced representation selection Accept-Encoding, Accept
Cache-Control Whether and for how long a response may be cached public, max-age=3600

Seeing Vary: Accept-Encoding does not prove that the current response was compressed. Seeing Content-Encoding: gzip proves that this response is gzip-encoded, but it does not by itself prove that a shared cache separates the gzip and identity variants correctly.

Does Vary itself enable gzip, Brotli, JSON, WebP, or AVIF?

No. Apache, Nginx, an application, an image optimizer, or a CDN performs the actual selection or transformation. Vary only describes the request fields involved in that decision.

For a quick header check, HEAD is convenient:

curl -sI https://example.com/

Look at Vary, Content-Encoding, Content-Type, Cache-Control, ETag, and cache diagnostics such as Age when they are available. Do not use ETag as a universal pass/fail rule: servers and compression stacks can generate or transform validators differently. Treat it as supporting evidence.

For the final check, test the actual GET path as well:

curl -sD - -o /dev/null https://example.com/

Some stacks do not process HEAD exactly like a body-producing GET. If compression or content negotiation is the issue, a GET request with the body discarded is the safer confirmation.

Same URL does not mean same representation. During troubleshooting, find the component that selected the representation before changing the header that describes it.

When is Vary: Accept-Encoding required, and when is it redundant?

Vary: Accept-Encoding belongs on responses whose representation changes according to Accept-Encoding. The common case is compression: one client gets gzip, another gets Brotli, and another gets the identity representation.

One browser request proves very little because browsers normally advertise compression support. Test the branches explicitly with GET requests:

curl -sD - -o /dev/null \
  -H "Accept-Encoding: identity" \
  https://example.com/assets/app.css

curl -sD - -o /dev/null \
  -H "Accept-Encoding: gzip" \
  https://example.com/assets/app.css

If Brotli is actually configured on the stack, add:

curl -sD - -o /dev/null \
  -H "Accept-Encoding: br" \
  https://example.com/assets/app.css
Request Expected Content-Encoding Expected Vary What the test proves
Accept-Encoding: identity Usually absent Accept-Encoding if encoding negotiation is active The uncompressed branch works
Accept-Encoding: gzip gzip when gzip is enabled for the resource Accept-Encoding The gzip branch works
Accept-Encoding: br br only when Brotli is supported and selected Accept-Encoding The Brotli branch works

Also compare Content-Length when present, any cache-status header, and optionally ETag. Different ETag behavior across compressed and uncompressed variants does not automatically mean something is broken; it is another clue, not the main acceptance criterion.

What happens if compressed and uncompressed responses share the same cache entry?

A shared cache can reuse the wrong stored representation if the cache policy does not distinguish requests that produce different encodings. That failure often stays hidden when you test only the origin because the origin can answer every request correctly while the proxy or CDN reuses the wrong object.

Warm the cache deliberately:

curl -sD - -o /dev/null \
  -H "Accept-Encoding: identity" \
  https://example.com/assets/app.css

curl -sD - -o /dev/null \
  -H "Accept-Encoding: identity" \
  https://example.com/assets/app.css

curl -sD - -o /dev/null \
  -H "Accept-Encoding: gzip" \
  https://example.com/assets/app.css

curl -sD - -o /dev/null \
  -H "Accept-Encoding: gzip" \
  https://example.com/assets/app.css

If the cache exposes Age, X-Cache, Cache-Status, or another diagnostic field, watch it together with Content-Encoding. A cache HIT is not enough. You need a HIT on the correct representation.

Should Vary: Accept-Encoding be added manually when the web server already generates it?

Usually no. Apache compression modules and Nginx's native gzip handling can generate the appropriate variation metadata themselves. Adding a manual rule can create duplicate ownership.

Header append Vary Accept-Encoding

or:

add_header Vary Accept-Encoding;

may be unnecessary if the server already emits the same field. Run the identity/gzip tests first. If the representation changes correctly and the public response already contains Vary: Accept-Encoding, stop there.

When should a server send Vary: Accept instead of Vary: Accept-Encoding?

Use Vary: Accept when the Accept request header influences which media representation is returned. It solves a different problem from Vary: Accept-Encoding.

Request header Negotiates Response header to inspect Typical Vary field Example
Accept-Encoding Content encoding Content-Encoding Accept-Encoding gzip versus identity
Accept Media representation Content-Type and the selected body Accept JSON versus HTML
Accept-Language Language representation Content-Language and body Accept-Language English versus German content

An API that can return HTML and JSON from one URL is an easy case to verify:

curl -sD - -o /dev/null \
  -H "Accept: text/html" \
  https://example.com/resource

curl -sD - -o /dev/null \
  -H "Accept: application/json" \
  https://example.com/resource

If one response is Content-Type: text/html and the other is Content-Type: application/json, Accept clearly influences the representation. A shared cache needs to account for that dependency.

Image negotiation can be checked in the same way:

curl -sD - -o /dev/null \
  -H "Accept: image/avif,image/webp,*/*" \
  https://example.com/image

curl -sD - -o /dev/null \
  -H "Accept: image/jpeg,*/*" \
  https://example.com/image

Do not reduce the test to Content-Type alone. Compare the media type, its parameters where relevant, Content-Length when useful, validators as supporting evidence, and the actual selected representation if the headers remain ambiguous. Two responses can share a broad Content-Type yet still differ in a way caused by Accept.

Does every API that receives an Accept header need Vary: Accept?

No. A client can send Accept: application/json while the API ignores it and always returns the same JSON representation. The existence of the request header does not automatically create a cache variant.

curl -sD - -o /tmp/json.headers \
  -H "Accept: application/json" \
  https://example.com/api/item

curl -sD - -o /tmp/html.headers \
  -H "Accept: text/html" \
  https://example.com/api/item

If repeated tests with materially different Accept values produce the same media type, same selected representation, and no other meaningful representation change, Vary: Accept deserves investigation. This often exposes an old rewrite rule, application middleware, or copied header configuration that no longer affects output.

Can Vary contain both Accept and Accept-Encoding?

Yes. If an endpoint can return JSON or HTML and each representation can also be compressed, both request fields can influence the final response:

Vary: Accept, Accept-Encoding

A useful four-way test is:

Accept: text/html
Accept-Encoding: identity

Accept: text/html
Accept-Encoding: gzip

Accept: application/json
Accept-Encoding: identity

Accept: application/json
Accept-Encoding: gzip

Each combination should map predictably to its media representation and encoding. Adding more fields to Vary does not make the cache safer if those fields do not actually affect the response.

How can you determine which layer is adding the Vary header?

If you remove a Header rule from .htaccess and the public response still contains the same Vary, you are probably looking at the wrong layer. The header can come from Apache modules, Nginx, PHP, application middleware, a reverse proxy, or a CDN.

Start with the public GET response:

curl -sD - -o /dev/null https://example.com/

On a server with root access, check Apache modules and virtual hosts:

apachectl -M | grep -E 'deflate|brotli|headers|negotiation'

apachectl -t -D DUMP_VHOSTS

For Nginx:

nginx -T 2>&1 | grep -Ei \
'gzip|vary|add_header|proxy_hide_header|proxy_cache'

A configuration search can cover both common Apache layouts:

grep -RniE \
'Vary|gzip_vary|add_header|Header (set|append|merge)' \
/etc/nginx /etc/httpd /etc/apache2 2>/dev/null

Hosting panels can generate configuration elsewhere, so an empty grep result does not prove that the header is application-generated.

How do you distinguish an origin header from a CDN or reverse-proxy header?

For HTTPS, querying the origin by bare IP is a common diagnostic mistake. The request can land on the wrong virtual host, and TLS SNI can select a different certificate or server block. If that test exposes a certificate mismatch, check the virtual-host and SNI path before changing Vary; the guide to SSL certificate errors in the browser covers the certificate-side diagnostics. Use the original hostname while resolving it to the origin IP:

curl -sD - -o /dev/null \
  --resolve example.com:443:ORIGIN_IP \
  https://example.com/

That keeps the hostname in the URL and preserves the expected TLS SNI and HTTP Host selection while bypassing normal DNS resolution for the test.

Now compare it with the public request:

curl -sD - -o /dev/null https://example.com/

A typical diagnostic result can look like this:

Public endpoint:
Content-Encoding: br
Vary: Accept-Encoding

Origin through --resolve:
Content-Encoding: gzip
Vary: Accept-Encoding

Apache at the origin is not producing the public Brotli representation in that scenario. Something after the origin is changing the encoding, so another round of edits in .htaccess will not explain the public br response.

Compare at least Vary, Content-Encoding, Content-Type, Cache-Control, cache diagnostics, and any useful validator such as ETag. Again, ETag is evidence, not a universal success criterion.

Why can deleting Vary from .htaccess appear to do nothing?

Apache can generate Vary as a consequence of compression, rewrite conditions, or content negotiation. Nginx can generate it through gzip_vary. PHP or the application can send it before the response reaches either web server. A CDN can modify the public response again.

Usually the support trail looks simple: a manual Header line is removed, nothing changes, and the administrator keeps searching the same file. At that point, switch layers. Check nginx -T, Apache modules, and an origin-versus-public request instead.

If the header survives an .htaccess change, do not keep editing .htaccess until you have proved Apache is the component generating that public header.

How should Vary: Accept-Encoding work with Apache mod_deflate and mod_brotli?

Apache's compression modules already understand that compression selection depends on Accept-Encoding, so manually appending the same field is often unnecessary. First confirm which modules are loaded:

apachectl -M | grep -E 'deflate|brotli|headers'

A conventional mod_deflate configuration may look similar to:

<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json
</IfModule>

The MIME list is only an example. Compression policy should match the application and the rest of the stack.

If mod_deflate is loaded, check whether it actually changes this URL:

curl -sD - -o /dev/null \
  -H "Accept-Encoding: identity" \
  https://example.com/

curl -sD - -o /dev/null \
  -H "Accept-Encoding: gzip" \
  https://example.com/

If the second response is gzip-encoded and Apache already emits the appropriate variation metadata, adding:

Header append Vary Accept-Encoding

creates another place capable of modifying the same field without adding any new information.

Should you use Header set, append, or merge for Vary in Apache?

Manual manipulation needs care. set can replace an existing value, while append can add another value even when a module has already supplied it. merge can avoid some duplicate values, but the first question is still whether a manual Header directive is necessary.

This configuration:

Header append Vary Accept-Encoding

can end up alongside module-generated output as:

Vary: Accept-Encoding
Vary: Accept-Encoding

or as a combined repeated value after normalization. That may not immediately break a browser, but it gives two parts of the stack responsibility for one cache decision.

After any header change, inspect a real GET response:

curl -sD - -o /dev/null \
  -H "Accept-Encoding: gzip" \
  https://example.com/

What changes when Apache compression depends on User-Agent or another request header?

If compression behavior changes according to another request field, that field can also become part of the representation-selection decision. A legacy User-Agent exclusion, for example, may cause two otherwise identical requests to receive different encodings.

Do not add User-Agent to Vary as a precaution. User-Agent values have high cardinality and can fragment a shared cache heavily. Send controlled requests that differ only in the header involved in the condition. If the representation does not change, that request field does not belong in the variation policy.

Keep the rule narrow: vary on the request fields that actually select different representations.

How should Vary: Accept be configured for Apache content negotiation or application responses?

Vary: Accept makes sense on Apache only when Accept helps select the response representation. The selector may be mod_negotiation, mod_rewrite, or the application behind Apache.

Check whether Apache content negotiation is loaded:

apachectl -M | grep negotiation

Then inspect the relevant virtual host, directory configuration, and .htaccess for:

Options MultiViews
AddHandler type-map

Also look for rewrite conditions that inspect Accept:

RewriteCond %{HTTP:Accept} ...

A grep for the literal word Vary can miss the cause completely. A rewrite rule can create the request-header dependency even when there is no explicit Header set Vary line.

Can Apache add Vary because RewriteCond checks HTTP:Accept?

Yes. Consider a deliberately simplified image-negotiation rule where the public URL stays the same but Apache selects a different file when the client advertises AVIF support:

RewriteEngine On

RewriteCond %{HTTP:Accept} image/avif
RewriteRule ^hero$ /images/hero.avif [END]

RewriteRule ^hero$ /images/hero.jpg [END]

The exact rewrite pattern in production depends on directory context, file layout, and Apache configuration, but the diagnostic method is the same. Test the public resource twice:

curl -sD - -o /dev/null \
  -H "Accept: image/avif,image/webp,*/*" \
  https://example.com/hero

curl -sD - -o /dev/null \
  -H "Accept: image/jpeg,*/*" \
  https://example.com/hero

A working negotiation path might produce:

AVIF request:
Content-Type: image/avif
Vary: Accept

JPEG request:
Content-Type: image/jpeg
Vary: Accept

If both requests return the same file and no meaningful representation metadata changes, investigate why Vary: Accept is still present. An old rewrite condition or application rule may be surviving after the original optimization was removed.

Should MultiViews be enabled just to obtain content negotiation?

No. MultiViews changes URI-to-resource selection. It is not a mechanism for “turning on Vary,” and it can interact with extensionless application routes in ways that are difficult to spot later.

If PHP, a framework, or an API controller chooses JSON, HTML, XML, or another media type, test that application path directly. Apache should not be forced to describe a decision made entirely in the application layer.

A useful support symptom here is Vary: Accept surviving after image-format rewrites were supposedly removed. Run several materially different Accept requests. If they all select the same representation, search both Apache rewrite configuration and application middleware before assuming the header is still justified.

How do you configure Vary: Accept-Encoding correctly in Nginx?

For Nginx gzip negotiation, gzip_vary on is the native mechanism for adding Vary: Accept-Encoding. A manual add_header Vary Accept-Encoding; should not be the first fix.

gzip on;
gzip_vary on;

Inspect the effective configuration, including included files:

nginx -T 2>&1 | grep -nE \
'gzip|gzip_vary|gzip_static|gunzip|add_header'

Validate syntax before a reload:

nginx -t

If the syntax check fails or Nginx refuses to reload, fix that first; the Nginx failed to start troubleshooting guide covers the startup and configuration-error path.

Then verify the actual GET path:

curl -sD - -o /dev/null \
  -H "Accept-Encoding: identity" \
  https://example.com/

curl -sD - -o /dev/null \
  -H "Accept-Encoding: gzip" \
  https://example.com/

What is the difference between gzip on and gzip_vary on?

gzip on controls compression. gzip_vary on controls the related Vary: Accept-Encoding response behavior for the relevant Nginx gzip processing. They are not interchangeable.

If gzip works but gzip_vary is disabled, a response can be compressed without the variation metadata you expected. If gzip_vary is already active, a second manual add_header rule usually adds confusion rather than protection.

Why can add_header Vary Accept-Encoding create duplicate output?

Nginx may already emit the field, or Apache/application upstream may send it before Nginx touches the response. A manual rule on the proxy can then add another value.

If Apache already returns:

Vary: Accept-Encoding

and Nginx passes that field through unchanged, Nginx does not need a second copy merely because it is the front server.

Also check where add_header is defined. Nginx configuration inheritance can cause one location to behave differently from another. Testing only / does not prove that a CSS location, PHP location, or proxied application route has the same response policy.

How should Vary be handled when Nginx proxies Apache?

In an Nginx-to-Apache stack, the public Vary must still describe the representation the client actually receives. Apache can compress, Nginx can compress, the application can negotiate a media type, and a CDN can transform the response again.

client
  |
  v
CDN (optional)
  |
  v
Nginx
  |
  v
Apache
  |
  v
PHP / application

Several architectures are valid:

  • Apache compresses and Nginx passes the encoded response through.
  • Apache returns identity and Nginx performs gzip compression.
  • The application performs content negotiation before either server adds caching metadata.
  • The origin serves gzip while a CDN recompresses or serves Brotli at the edge.

What matters is that each transformation can be traced and that the public response remains correct.

Observed response Likely area to inspect Next check
Apache origin already returns gzip and Vary: Accept-Encoding Apache compression Check whether Nginx passes the response unchanged
Apache origin is identity, public Nginx response is gzip Nginx compression Inspect gzip and gzip_vary
Origin returns gzip, public endpoint returns Brotli Front proxy or CDN transformation Compare origin with the outermost public layer
Duplicate Vary values Multiple layers or manual header rule Compare upstream and public headers
Vary changes only when CDN is enabled Edge behavior Inspect CDN transformation and cache policy

Should Nginx remove Apache's Vary header and create its own?

Not by default. If Apache's header accurately describes the representation and Nginx does not change the negotiation, preserving it is easier to reason about than deleting and rebuilding it.

Inspect Nginx for directives that can hide or replace upstream fields:

nginx -T 2>&1 | grep -Ei \
'proxy_hide_header|add_header|gzip|gzip_vary|proxy_cache'

If Nginx deliberately changes the representation, retest the final public response after the transformation. That is where the acceptance criterion belongs.

How do you test whether the proxy cache mixes encoded variants?

Warm both variants and alternate them:

curl -sD - -o /dev/null \
  -H "Accept-Encoding: identity" \
  https://example.com/file.css

curl -sD - -o /dev/null \
  -H "Accept-Encoding: identity" \
  https://example.com/file.css

curl -sD - -o /dev/null \
  -H "Accept-Encoding: gzip" \
  https://example.com/file.css

curl -sD - -o /dev/null \
  -H "Accept-Encoding: gzip" \
  https://example.com/file.css

Watch Content-Encoding, Vary, and the cache indicator. The identity request must remain identity after the cache is warm, and the gzip request must remain gzip. Test the chain, not just the first response.

cPanel Hosting
Full control over your website
  • Convenient
  • Simple
  • Fast
  • Free 7-day trial
cPanel Hosting

What changes on a cPanel server with Apache only versus cPanel with ea-nginx?

A cPanel server is not one fixed web-server layout. On cPanel hosting, a shared-hosting user may only control .htaccess and cPanel interfaces, while a WHM/root administrator may have Apache, ea-nginx, PHP-FPM, server-level compression rules, and an external CDN in the same path.

Environment What you can usually inspect Likely Vary owner Best verification
cPanel shared hosting with Apache .htaccess, Optimize Website, public response headers Apache compression or application External GET requests with different headers
WHM with Apache only Apache modules, vhosts, global config Apache or application apachectl -M plus response tests
WHM with ea-nginx Nginx and Apache effective config Nginx, Apache, or application Origin versus public comparison
cPanel origin behind external CDN Origin plus edge response Origin and/or CDN Cache-variant matrix at both layers

What should you do if you only have normal cPanel user access?

Do not start by pasting a custom Vary directive into .htaccess. Use the parts of the stack you can actually verify:

  1. Check cPanel's Optimize Website setting if it is available on the account.
  2. Inspect the account's .htaccess for compression or manual Header rules.
  3. Run external GET tests with Accept-Encoding: identity and Accept-Encoding: gzip.
  4. If the response already changes correctly and carries Vary: Accept-Encoding, do not add another rule.
  5. If the public response is wrong but no account-level setting explains it, the next check belongs to hosting support because the responsible layer may be outside the account.

A shared-hosting customer cannot diagnose an ea-nginx include or global Apache module by editing user-level files. That boundary matters.

Does cPanel Optimize Website automatically require a manual Vary rule?

No. cPanel's Optimize Website feature configures Apache compression through mod_deflate and stores user-level compression settings in .htaccess. Apache can already provide the relevant Accept-Encoding variation metadata.

Confirm it with real GET requests:

curl -sD - -o /dev/null \
  -H "Accept-Encoding: identity" \
  https://example.com/

curl -sD - -o /dev/null \
  -H "Accept-Encoding: gzip" \
  https://example.com/

If gzip is selected correctly and the response already includes Vary: Accept-Encoding, leave it alone.

What should a WHM or root administrator check on an ea-nginx server?

With root access, work from the outside in:

  1. Confirm whether ea-nginx is in the request path.
  2. Inspect Apache modules with apachectl -M.
  3. Inspect the effective Nginx configuration with nginx -T.
  4. Compare the public response with a controlled origin request.
  5. Use cPanel-supported custom Nginx include mechanisms rather than editing generated files directly.
  6. Run nginx -t or the appropriate Apache syntax check before reload.
  7. Repeat the public identity/gzip test after the change.

Do not use the public Server header as definitive proof of the internal path. It can be hidden, rewritten, or only describe the outermost server.

Can PHP zlib.output_compression affect the result?

Yes. PHP's zlib.output_compression can become another component capable of changing the response encoding. If PHP compresses output while Apache or Nginx also has compression logic, identify which layer is active before changing Vary.

A root administrator might start with:

php -i | grep -i zlib.output_compression

but that output reflects the CLI PHP configuration. The website may use a different PHP version, PHP-FPM pool, or per-directory configuration. If CLI and web behavior disagree, the PHP error logging and PHP-FPM diagnostics guide is useful for tracing the active PHP configuration and logs. A CLI result is a clue, not proof of what the web request uses.

The public GET response remains the final check.

Why do duplicate, missing, or oversized Vary headers appear?

Unexpected Vary output usually comes from duplicate ownership, hidden negotiation logic, header rewriting at a proxy, or a cache key that accumulated dimensions that no longer affect the response.

Typical symptoms include:

Vary: Accept-Encoding
Vary: Accept-Encoding

or:

Vary: Accept-Encoding, Accept-Encoding

or a broad value such as:

Vary: Accept-Encoding, User-Agent, Cookie, Accept, Accept-Language
Symptom Probable cause What to check Fix direction
Vary missing while encoding changes Compression layer does not advertise variation, or proxy stripped it Origin versus public response Fix the responsible compression/proxy layer
Duplicate Accept-Encoding Module plus manual header, or upstream plus proxy Effective Apache/Nginx config Remove duplicate ownership
Vary: Accept, but different Accept values appear to return the same representation Obsolete rewrite/application logic or copied header rule Compare Content-Type, parameters, selected body, length where useful, validators, and application behavior Remove variation only after confirming the representation is truly identical
Vary disappears behind proxy Header hiding or response rewriting Proxy configuration Preserve or deliberately replace the correct upstream field
Very large Vary value Several independent conditions added over time Rewrite rules, application logic, CDN rules Keep only real representation dimensions
Vary: * The response is not reusable through ordinary request-header matching semantics Find why specific request fields cannot describe the selection Do not replace it blindly with a guessed list of headers

Is duplicate Vary: Accept-Encoding harmless?

It may not immediately break a browser, but duplication usually means two components believe they own the same decision. That becomes a maintenance problem as soon as compression, proxying, or caching changes.

Dump the raw response:

curl -sD - -o /dev/null https://example.com/

Then compare Apache, Nginx, application, and edge behavior as appropriate. One clean header line is useful, but the real target is a configuration where you know which component owns the representation and which component only forwards it.

Why is Vary: User-Agent usually expensive for caching?

User-Agent has many possible values. If a shared cache varies objects by the entire field, browsers, versions, operating systems, and devices can stop sharing the same stored representation even when the body is identical.

There are valid User-Agent-dependent responses, but prove the difference first. Old compatibility snippets are a common place to find variation that no longer serves a purpose.

The same caution applies to cookies and other high-cardinality inputs. If the response truly changes because of them, caching requires deliberate design. If it does not, they only fragment the cache.

What should you do if the response contains Vary: *?

Vary: * is not a generic “maximum compatibility” setting and should not be used as a fallback when you are unsure which field matters. It tells caches that matching a future request by a normal set of named request fields is not sufficient for reuse.

If you encounter it unexpectedly, trace the response generator and the cache policy. Find out why the server or application cannot describe the representation choice with specific request fields such as Accept or Accept-Encoding.

Do not replace Vary: * with a guessed list merely to improve cache hits. First understand what made the response non-reusable under ordinary variation rules.

The best Vary value is not the longest one. It is the smallest accurate description of the request inputs that really changed the representation.

How do you test Vary behavior through a CDN or shared cache?

A correct origin response does not prove that the edge cache is correct. A CDN can compress content, optimize images, normalize request headers, use a custom cache key, or keep serving an object created before you fixed the origin.

Test the public cache in a controlled sequence:

  1. Start with a clean test object when you can safely purge it, or use a known test URL according to the cache platform's rules.
  2. Request Accept-Encoding: identity.
  3. Repeat the identity request to exercise a possible HIT.
  4. Request Accept-Encoding: gzip.
  5. Repeat the gzip request.
  6. If Vary: Accept is relevant, repeat the process with two materially different Accept values.
  7. Compare Content-Encoding, Content-Type, Vary, Age, and the platform's cache-status fields.
curl -sD - -o /dev/null \
  -H "Accept-Encoding: identity" \
  https://example.com/test.css

curl -sD - -o /dev/null \
  -H "Accept-Encoding: gzip" \
  https://example.com/test.css

curl -sD - -o /dev/null \
  -H "Accept: application/json" \
  https://example.com/resource

curl -sD - -o /dev/null \
  -H "Accept: text/html" \
  https://example.com/resource

Why can a problem continue after the server configuration is fixed?

The cache may still contain objects created under the previous variation policy. Correcting Apache or Nginx does not necessarily evict an incorrectly keyed object that already exists at the edge.

Compare a bypassed or uncached response with the cached response if the platform provides a supported way to do that. If the origin is now correct but the edge still serves the old representation, invalidate the affected objects according to the CDN or proxy's normal purge procedure.

Do not add random query strings unless you know how the platform treats them. One CDN may cache each query separately, another may ignore selected parameters, and another may bypass caching. A diagnostic URL should not silently change the cache rule you are trying to test.

Should Vary be treated as the only CDN cache-key control?

No. CDNs and reverse proxies can have cache-key policies independent of the origin's Vary field. They may normalize request headers, perform edge compression, optimize images, or explicitly include and exclude values from their cache key.

That gives you two separate acceptance tests:

  • Does the origin describe its own representation selection correctly?
  • Does the public cache keep or transform those variants correctly?

If the origin passes and the public cache fails, changing Apache headers again will not fix the edge policy.

A frequent support pattern is exactly that: the origin responds with gzip as expected, while the public endpoint responds with Brotli and different cache metadata. That is not evidence that Apache is broken. It tells you that another layer is participating.

What should you verify before leaving Vary configuration in production?

A production-safe setup should pass a small request matrix at both the origin and the public cache layer. One successful page load is not enough.

10-minute acceptance checklist
  • Identify whether Apache, Nginx, the application, or the CDN selects the representation.
  • Use HEAD only as a quick header check; confirm critical behavior with a GET request.
  • Test Accept-Encoding: identity.
  • Test Accept-Encoding: gzip.
  • Test Brotli only when the stack actually supports and serves it.
  • If Vary: Accept is present, test at least two materially different Accept values.
  • Confirm that Content-Encoding changes only when expected.
  • Confirm that the actual media representation changes only when expected.
  • Use ETag as supporting evidence, not as a universal pass/fail rule.
  • Confirm that Vary contains only request fields that really influence representation selection.
  • Check for repeated or duplicate Vary values.
  • Investigate unexpected Vary: * instead of replacing it blindly.
  • Test cached or revalidated responses, not only the first 200 response.
  • Repeat the same tests through the CDN or reverse proxy.
  • Observe Age, HIT/MISS, or other cache diagnostics when available.
  • Validate Apache or Nginx configuration before reload.
  • After changing cache-key or negotiation behavior, invalidate stale cached representations when necessary.

For Nginx:

nginx -t

For Apache, a typical syntax check is:

apachectl -t

Keep a small known-good response matrix after the change. It becomes useful later when someone enables another compression layer, changes ea-nginx, installs application middleware, or places the site behind a CDN.

Test Expected response Pass condition If it fails
Accept-Encoding: identity No compressed encoding Identity representation remains consistent Inspect origin/proxy compression
Accept-Encoding: gzip gzip where enabled Content-Encoding and Vary match the policy Inspect Apache/Nginx compression owner
Two different Accept values Different representation only when negotiation is configured Headers and actual selected body agree with the negotiation logic Inspect application, rewrite, or Apache negotiation
Repeated request through cache Correct variant on HIT No cross-variant reuse Inspect proxy/CDN cache key
Origin versus public endpoint Differences are explainable by a known layer Every transformation has a clear source Trace the first layer where behavior diverges

When the identity, gzip, media-type, origin, and cached paths all behave predictably, stop changing headers. At that point you have evidence that the variation policy matches the representations the stack actually serves.

Frequently asked questions
Vary: Accept-Encoding is used when the selected content encoding changes with Accept-Encoding, such as gzip versus identity. Vary: Accept is used when Accept changes the media representation, such as HTML versus JSON.
Usually not when mod_deflate or mod_brotli already handles compression negotiation. Test identity and gzip responses first and add a manual Header rule only if the resulting response is actually missing the required variation metadata.
Not as the first fix. For Nginx gzip processing, check gzip_vary on and inspect the effective configuration. A manual add_header can duplicate a Vary value already produced by Nginx or an upstream server.
Two layers are often emitting the same field, for example Apache plus a manual Header directive, or an upstream server plus Nginx. Compare the origin and public responses and inspect both configurations.
Check Optimize Website, inspect .htaccess, and test the public URL with identity and gzip GET requests. If the public response is still wrong and no account-level rule explains it, the responsible layer may require hosting support.
Vary: * means normal request-header matching is not sufficient to determine whether a stored response can be reused. Do not replace it with a guessed list of headers until you understand why the response generator used it.
It is useful for a quick header check, but confirm critical behavior with a GET request such as curl -sD - -o /dev/null. Some stacks do not process HEAD exactly like the body-producing GET path.
Yes. Extra dimensions such as User-Agent or unused Accept values can create more cache variants and reduce reuse. Keep Vary limited to request fields that actually change the representation.
Related articles
Gzip and Vary: Accept-Encoding Header
How to Specify a Vary: Accept-Encoding Header in Nginx