Vary: Accept-Encoding and Vary: Accept in Apache, Nginx, and cPanel
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.
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.
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.
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.
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:
- Check cPanel's Optimize Website setting if it is available on the account.
- Inspect the account's
.htaccessfor compression or manualHeaderrules. - Run external GET tests with
Accept-Encoding: identityandAccept-Encoding: gzip. - If the response already changes correctly and carries
Vary: Accept-Encoding, do not add another rule. - 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:
- Confirm whether ea-nginx is in the request path.
- Inspect Apache modules with
apachectl -M. - Inspect the effective Nginx configuration with
nginx -T. - Compare the public response with a controlled origin request.
- Use cPanel-supported custom Nginx include mechanisms rather than editing generated files directly.
- Run
nginx -tor the appropriate Apache syntax check before reload. - 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.
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:
- 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.
- Request
Accept-Encoding: identity. - Repeat the identity request to exercise a possible HIT.
- Request
Accept-Encoding: gzip. - Repeat the gzip request.
- If
Vary: Acceptis relevant, repeat the process with two materially differentAcceptvalues. - 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.
- Identify whether Apache, Nginx, the application, or the CDN selects the representation.
- Use
HEADonly 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: Acceptis present, test at least two materially differentAcceptvalues. - Confirm that
Content-Encodingchanges only when expected. - Confirm that the actual media representation changes only when expected.
- Use
ETagas supporting evidence, not as a universal pass/fail rule. - Confirm that
Varycontains 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.


