How to fix compression and caching headers
Your audit came back with a heading reading "Compression and caching headers" and a list of URLs under it, and the two halves of that heading sound like the same afternoon of server work. They are not. One is usually a single line of configuration and a real saving; the other is a smaller matter than its name suggests, and on some hosts the first is reported against a site already doing it correctly.
Check Engine's perf.compression check emits two findings, under
two ids, at two very different severities:
high perf.no_compression N pages are served without compression
low perf.no_cache_headers Pages are served with no caching headers
The gap between high and low is most of the answer. The check also
grades its own workload, and grades neither as an afternoon: the compression fix is filed as
trivial, which the engine defines as "edit one tag / one line of config, minutes",
and the caching fix as small, "a template change, under an hour".
What this cannot tell you, first
Check Engine reads the headers on the responses it was given. It does not measure how fast anything felt. There is no browser, no paint, no device and no network but ours. A finding here is a fact about the response that arrived at our crawler, and most of this page is about the distance between that and your site.
This check does not measure Core Web Vitals, and nothing that runs on one machine can. Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift come from real people on real connections. The check module says so in its own opening comment. That it measures the network and the document, not a rendered browser, and that "a number invented from static analysis would be worse than no number". The field values come from Google's Chrome UX Report, which Check Engine's optional PageSpeed check reads for sites with enough traffic to be in it, and which Search Console shows too.
It does not measure how much compression saved you, and
it only looked at HTML. Nothing is compressed twice and nothing is compared;
the check asks whether a Content-Encoding header was present and stops. Your
stylesheets, scripts and JSON endpoints were never fetched for it, so it has no opinion about
those. Even though its own fix text asks you to turn compression on for them.
Brotli, and the one extra request Check Engine makes
Check Engine's crawler fetches pages with Accept-Encoding: gzip, deflate. It does not
advertise Brotli on those fetches, because there is no Brotli decoder in the Python standard
library and the crawler reads pages with the standard library alone. The fetcher says so in its
own comment: Check Engine never advertises br, so Brotli cannot arrive from a
specification-abiding server.
Now read that against the specification. Under
RFC 9110 section 12.5.3, the
Accept-Encoding request header is how a client states which content codings it
will accept, and a server that respects it will not send one that was not offered. So an origin
or CDN configured to serve Brotli and nothing else, behaving correctly, answers those fetches with
an uncompressed document and no Content-Encoding header, while its real visitors
have had compressed HTML all along.
So when a page over the size floor arrives uncompressed, Check Engine asks that one URL again, this
time for Brotli, and reads only the response headers; the body is never decoded. If the answer
carries a Content-Encoding, the server compresses for the clients that ask for it,
browsers and Googlebot among them, and perf.no_compression is not reported. If it
comes back uncompressed too, the finding says so in its own words: "Check Engine also asked for Brotli,
and that came back uncompressed too." One request is made, for the first such page, and its answer
stands for the site.
If that request could not be made (an error, or a redirect), the finding keeps an older sentence instead, saying a server that offers only Brotli cannot be told apart. Then check before you touch a configuration file, with the request a browser actually makes:
curl -sI -H 'Accept-Encoding: gzip, deflate, br' https://example.com/ | grep -i content-encoding
If that prints content-encoding: br and the same request without
br prints nothing, compression is on and there is nothing to fix. The general form holds in both directions: a managed host or CDN can
compress for real visitors while answering an unfamiliar client differently, and the response we
got is the only thing this check ever saw.
What the check actually computes
Compression. For every page that returned a success status and parsed as
HTML, Check Engine asks two questions: is the HTML document bigger than the floor, and is a
Content-Encoding header present. That is the whole test:
uncompressed = [
p for p in ctx.ok_pages
if p.html_bytes > 20_000
and not p.headers.get("content-encoding")
]
Two things follow that people get wrong. First, any value satisfies it. The check
never reads what the header says, so it cannot distinguish gzip from Brotli from
identity. Second, the size floor is measured on the decompressed document:
html_bytes is documented in the model as the raw HTML byte length before any
transfer encoding, and is assigned after the fetcher has unpacked the body. It is the size of
your markup, not of what crossed the wire. A page under the floor is never reported however it
is served, because the saving on a small document does not justify a finding. If the list is not
empty, the one Brotli request described above decides whether it is reported.
Caching. Here the shape is different, and this is the part most likely to
surprise you. A page counts as having no caching headers only when all of
Cache-Control, ETag and Last-Modified are absent. Any one
of the three is enough to clear it. And the finding is then gated on the site rather than the
page: it is only emitted when more than half of the crawled OK pages are bare. If a minority of
your pages have no caching headers, this check says nothing at all, and its title, "Pages are
served with no caching headers", is deliberately a statement about the site rather than a
count.
As with compression, presence is the test and the value is never read.
Cache-Control: no-store clears this check completely, because the check is asking
whether you said anything about caching, not whether it was wise.
Turning compression on
On nginx, the gzip module documentation is short and worth reading
in full. The directive defaults to off, and HTML is a special case: the documentation says
plainly that responses with the text/html type are always compressed, so
text/html does not belong in the type list.
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 256;
On Apache, mod_deflate does the same job from the vhost or
.htaccess:
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json image/svg+xml
If you are on a managed platform and control neither file (the common case, and the reason
this page exists) the setting is in your host's dashboard rather than your repository. On a CDN
it is usually one toggle applied at the edge, and the CDN compresses on the way out whether or
not your origin did. That is why the fix is graded trivial: it is not a deploy.
Caching headers, and what to actually set
The reason this one is low rather than high is that a missing
caching header costs you nothing on a first visit, which is most search traffic. It costs you on
repeat visits and on re-crawls.
RFC 9111 section 3 sets out when a cache may store a
response at all, and RFC 9111 section 5.2 defines
Cache-Control as the field carrying directives to caches along the
request/response chain. The other two headers the check looks for are validators rather than
policy: RFC 9110 section 8.8.3 defines
ETag, RFC 9110 section 8.8.2
defines Last-Modified, and either lets a client ask "has this changed" and be told
no: the conditional-request mechanism in
RFC 9111 section 4.3. That is the part crawlers use,
and why a validator alone satisfies the check.
For HTML that changes when you publish, the check's own suggested value is conservative and sensible:
Cache-Control: public, max-age=0, must-revalidate
That does not mean "do not cache". It means the cache may keep a copy and must check with
you before reusing it: which, with an ETag alongside it, turns most repeat fetches
into a short response with no body. For fingerprinted assets, a long max-age with
immutable is the usual pairing. Those are not what this check looked at, but they
are where the saving is.
What this check is not
Header findings attract everything else that is slow, so the boundaries are worth stating. Check Engine's speed area runs this check alongside separate ones with their own ids, their own thresholds and their own findings:
perf.page_weight: the size of the HTML document itself, decompressed. Samehtml_bytesfield, different question: compression is about how it was sent, weight is about how much of it there is.perf.render_blocking. Stylesheets and scripts that block the first paint. Counted from the markup, and reported per page.perf.ttfb: time to first byte, taken from the fetch and reported against the median across the site.perf.modern_images. Images still served as JPEG or PNG where a modern format would be smaller.
None of those is fixed by a compression header and none of them fixes one. The full list of what the engine looks at, area by area, is on what Check Engine checks. The area's other markup-level finding is written up at how to fix layout shift, which carries the same limit from the other side: Check Engine infers layout-shift risk from markup and cannot measure the metric. Scripts and stylesheets that hold back the first paint, the third markup-level finding in this area, are at how to fix render-blocking resources. The fourth, a slow server, is at how to reduce server response time.
The closest relative outside this area is response headers of a different kind. Missing security headers are set in the same file, at the same edge, by the same person, and this site fails two of those checks itself, because its host cannot set response headers at all. If you are opening that file anyway, do both.
So which is worth an afternoon
Neither, on the check's own grading, and that is the useful answer. If the compression
finding does not say Check Engine also asked for Brotli, confirm it with the curl above
first. If it holds, turn compression on: one line, and the largest single win this check can point at. Then
set a Cache-Control value and make sure a validator is present: worth doing, not
urgent. Spend the rest of the afternoon on
something else on the list.
Every external reading on this page was taken on 19 October 2026: RFC 9110, HTTP Semantics and RFC 9111, HTTP Caching at rfc-editor.org, the gzip module at nginx.org, and mod_deflate at httpd.apache.org.
Common questions
Does Check Engine check for Brotli?
With one request. Page fetches advertise only gzip and deflate, because there is no Brotli decoder in the Python standard library. When a large page arrives uncompressed, Check Engine asks that URL again for Brotli and reads only the headers. If an encoding comes back, the server compresses for the clients that ask and perf.no_compression is not reported. If that request fails, the finding says a Brotli-only server cannot be told apart; confirm with curl -sI -H 'Accept-Encoding: gzip, deflate, br' before changing anything.
What counts as compressed?
The presence of a Content-Encoding header on the response, whatever its value. The check never reads the algorithm, never compares sizes and makes no claim about how much was saved. It is a presence test, not a measurement.
Why does the compression finding only cover some pages?
There is a size floor, and it is measured on the decompressed HTML rather than on what crossed the wire. Pages whose markup is smaller than the floor are never reported however they are served, because the saving on a small document does not justify a finding.
What counts as a caching header?
Any one of Cache-Control, ETag or Last-Modified. A page only counts as bare when all three are absent, and the finding is only raised when more than half the crawled pages are bare, so it is a statement about the site, not a count of pages. The value is never read, so Cache-Control: no-store clears the check as thoroughly as a good policy would.
Will fixing this improve my Core Web Vitals score?
It may improve the experience, but this check cannot tell you, and neither can any tool running on one machine. LCP, INP and CLS come from real visitors on real connections. This check measures causes; the field values come from the Chrome UX Report, which Check Engine's optional PageSpeed check reads when Google has data for your site, and which Search Console shows too.
Does compression affect rankings directly?
Not as a header. Nothing in the check claims a ranking effect and this page will not invent one. Smaller responses arrive faster, which is worth having on its own terms.