SEO check

Image Caching: How to Check and Fix Cache Headers

What image caching is, how our report tests it, and the Cache-Control settings that keep your images loading fast on repeat visits.

Free to use 15 min read Updated Oct 2026

Image caching: the quick answer

Quick answer: Image caching means the browser or a CDN keeps a copy of each image after the first download, so repeat views skip the transfer entirely. Our report reads only the Cache-Control header of the first 15 images on your rendered page and flags any image that is sent with no-cache or max-age=0.

  • Pass: None of the images we check is sent with no-cache or max-age=0, and none of the requests fails.
  • Fail: 20 percent or more of the first 15 images are flagged or cannot be fetched, which is one image on a page with five or fewer.
  • Fix: Send Cache-Control: public, max-age=31536000, immutable on image files that have versioned names, and a shorter lifetime on files that keep the same name.

Check my site free

What is image caching?

Image caching is the practice of storing a copy of an image, keyed by its URL, and reusing that copy for as long as the response headers say it is still fresh. The first time a visitor loads your logo, the whole file downloads. Every later view inside the freshness window comes from local storage with no request at all. The MDN HTTP caching guide is the neutral reference for the mechanics.

This page is about the headers your server sends, not the cached images and files option visitors clear in browser settings, or image libraries in mobile app frameworks.

Browser cache and CDN cache

The browser cache is private: each visitor has their own copy on their own device. A shared cache, such as a CDN edge server or a corporate proxy, keeps one copy and serves it to many people. The same headers steer both.

The directives split along that line. max-age sets the freshness lifetime in seconds for every cache, public allows shared caches to store the response, private limits it to the browser, and s-maxage applies only to shared caches. A CDN edge also has its own lifetime, separate from the browser lifetime your header announces.

How a cached image is reused

While a stored image is fresh, the browser uses it without asking the server anything. Once the lifetime runs out the copy is stale, and the browser can revalidate it with a conditional request: it sends the ETag in If-None-Match, or the Last-Modified date in If-Modified-Since. If the file has not changed, the server replies 304 Not Modified with no body.

no-store tells caches never to keep the response. no-cache lets them keep it but demands a check with the server before every reuse, and max-age=0 has almost the same effect, because the copy is already stale when it arrives, and our report treats the two identically. Both cost a round trip on every view. If the server also sends an ETag or Last-Modified, the reply can be a small 304 Not Modified, but dynamic image endpoints often send neither, and then the whole file downloads again each time.

With no freshness information at all, browsers may guess a lifetime. The HTTP caching specification suggests roughly 10 percent of the time since the file last changed, so an explicit header is the safer choice.

How SEO Site Checker tests image caching

Headless Chromium loads your page and lets it render, then we read every img src from the rendered DOM, so images that scripts have inserted by the time the page finishes loading are included, while ones added later by scrolling are not. We resolve relative addresses against the page URL and keep the first 15 in document order, so a logo used three times counts three times.

The exact rule: an image is flagged when its Cache-Control contains no-cache or max-age=0 (case insensitive), or when its request fails. If 20 percent or more of the checked images are flagged the result is a fail, under 20 percent is a warning, and none flagged is a pass.

Read this before celebrating a pass: an image with no Cache-Control header at all is not flagged. A pass therefore means we found no anti-cache directive, not that you set a long lifetime. Set a long lifetime anyway, because Lighthouse and PageSpeed Insights judge the lifetime itself.

What the report sends and reads

Each of the 15 addresses gets a HEAD request with a 3 second connect and read timeout. Redirects are followed, and up to 10 requests run in parallel. After 10 seconds in total, any request still unfinished is silently dropped from the lists.

Only Cache-Control is read, and the match is a plain substring test, so no-cache, private is flagged and so is public, max-age=0, must-revalidate. A timeout, a refused connection and an address the request library rejects, such as a data: URI, all count as failed requests.

Pass, warning and fail thresholds

The result depends on the share of checked images that are flagged. Exactly 20 percent is a fail. The share is divided by the number of image URLs collected, including any whose request did not finish in time.

Images checkedPassWarningFail
No img tagsAlwaysNot possibleNot possible
1 to 50 flaggedNot possible1 or more flagged
6 to 100 flagged1 flagged2 or more flagged
11 to 150 flagged1 or 2 flagged3 or more flagged

Warning and failed both show the not using cache headers message, a red How to Fix button and a link to this guide. Only signed in users see the image URLs, and an account is free. This item is informational and does not change the headline score.

What the check does not see

The test covers the first 15 img elements. CSS background images, srcset and picture sources, data-src only images and everything past the fifteenth are outside it. The Expires header is ignored too, so an image that only sends Expires passes, as does one sent with no-store.

False alarms are possible as well. The HEAD request carries a generic script user agent, so a firewall or hotlink rule that blocks or drops it can flag an image that loads fine for real visitors.

  • Pass: No flagged images, including a page with no img tags.
  • Fail: 20 percent or more of the checked images are flagged or unreachable.
  • Not tested: CSS backgrounds, srcset and picture sources, data-src only images, images past the first 15, the Expires header, and how long the lifetime is.

Why image caching matters for speed and SEO

Images are usually the heaviest files on a page, so downloading them again on every visit costs the most time and the most data. A visitor who browses five pages of your shop sees the same logo, header graphic and navigation icons five times. With a sensible cache policy, only the first page pays for them, which is why the benefit shows up on repeat visits and multi page sessions rather than on the very first view.

There is a Core Web Vitals angle as well. When the hero image is already in the browser cache, Largest Contentful Paint on a return visit can drop sharply because there is nothing to wait for. Lighthouse and PageSpeed Insights list static assets with short or missing lifetimes under the audit Serve static assets with an efficient cache policy, and newer reports group them under a cache lifetime insight.

Your server benefits too, with lower bandwidth and steadier behavior in traffic spikes. Caching is not a direct ranking signal. It speeds up pages for returning visitors, especially on mobile connections, and speed feeds page experience. For the wider picture, read what page speed means for SEO.

How to check if your images are cached

  1. Our report. Run the free check, open the Speed section and read the Image Caching block. Sign in to see the list of cached and uncached image URLs.
  2. Chrome DevTools. Open the Network tab, filter by Img, click an image and read Cache-Control under Response Headers.
  3. The terminal. Run curl against one image URL and read the cache-control line of the last response.
  4. Lighthouse or PageSpeed Insights. Their cache lifetime audit lists each static asset with its lifetime.

Browser DevTools

Open the page, press F12 and switch to the Network tab. Leave Disable cache unticked, because it overrides what you are testing. Do a normal reload, not a hard reload, and check the Size column: disk cache or memory cache means the image never touched the network. Click any image and scroll to Response Headers for its Cache-Control value.

curl from a terminal

One command prints the headers for an image. The -s flag hides the progress meter, -I sends a HEAD request like our check does, and -L follows redirects.

Text
curl -sIL https://example.com/images/hero.jpg | grep -i cache-control

If the image redirects, each hop that sends the header prints a line, so read the last one, which is what a visitor ends up with. You want a long max-age there.

How to fix image caching with Cache-Control headers

  1. List the flagged files. Copy the uncached URLs from the report and note which host serves each one: your own server, a CDN or a third party you do not control.
  2. Pick a lifetime. If the file name contains a version or hash, use one year, which is 31536000 seconds. If the name never changes, choose a shorter window such as 30 days, 2592000 seconds, so replacements reach visitors.
  3. Apache. Use the mod_headers block shown below, because it replaces a no-cache value that is already being sent.
  4. Nginx. Add a location block for your image extensions with add_header, and leave out the expires directive, which would produce a second Cache-Control header. If your existing image location sets its own root, alias or proxy_pass, copy that line into the block.
  5. CDN or host. Set a browser TTL rule for your image paths, and purge the edge copy only if the old header keeps appearing.
  6. Re-test. Run curl -sIL on one image URL, then run the report again.

Apache .htaccess

Put this block in the .htaccess file in your site root, or in the virtual host configuration. The (?i) makes the match ignore case, so photo.JPG is covered. In .htaccess it needs AllowOverride FileInfo, which some shared hosts restrict.

Text
<IfModule mod_headers.c>
  <FilesMatch "(?i)\.(jpe?g|png|gif|webp|avif|svg)$">
    Header set Cache-Control "public, max-age=31536000, immutable"
  </FilesMatch>
</IfModule>

If your server sends no Cache-Control at all yet, the Apache mod_expires documentation describes an alternative that writes the header for you.

Text
<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType image/jpeg "access plus 1 year"
  ExpiresByType image/png "access plus 1 year"
  ExpiresByType image/gif "access plus 1 year"
  ExpiresByType image/webp "access plus 1 year"
  ExpiresByType image/avif "access plus 1 year"
  ExpiresByType image/svg+xml "access plus 1 year"
</IfModule>

Use this one only when no Cache-Control is sent yet, because mod_expires stands down once a response carries an Expires header, and a flagged no-cache would survive. In .htaccess the mod_expires directives need AllowOverride Indexes, so on a locked down shared host put them in the virtual host too.

Nginx

Add a location block to the server configuration. A regex location wins over a plain prefix location, so it takes over image requests that a block such as location /images/ used to handle. If that block sets its own root, alias or proxy_pass, copy that line into the new block. If root is set once at server level, the snippet works as it is.

Text
location ~* \.(jpe?g|png|gif|webp|avif|svg)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

If the images come from an upstream such as Apache or a Node app, keep the proxy_pass line and hide the upstream header, because proxy_hide_header only has an effect inside a location that proxies.

Text
location ~* \.(jpe?g|png|gif|webp|avif|svg)$ {
    proxy_pass http://backend;
    proxy_hide_header Cache-Control;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

Two traps catch people here. An add_header inside a location replaces every add_header line inherited from the server block, so repeat any you still need. And if an application already sends no-cache, hide that header first with proxy_hide_header Cache-Control, or fastcgi_hide_header Cache-Control inside a PHP location, otherwise the browser receives both values. Run nginx -t to test the configuration, then reload.

CDN and host settings

A CDN has two lifetimes: how long the edge keeps a copy and how long browsers are told to keep one. For this check the browser lifetime is the one that matters, so set it with a rule scoped to your image paths. In Cloudflare, open Caching, then Configuration, and set Browser Cache TTL to Respect Existing Headers if your origin is already correct, or to a fixed value if it is not. A cache purge clears the edge copy only, never files already stored in visitor browsers.

Image caching on WordPress, Shopify, Wix and CDNs

Most site owners never edit a server file, so the table is the quick map. Our CDN usage test covers the edge side.

PlatformWhere to set itWhat to check afterwards
WordPressA caching plugin's browser cache option, or the Apache block aboveThe cache-control line of a Media Library image URL
Shopify and WixNot editable, the platform manages headers for hosted imagesAny flagged image that comes from a third party widget or an app
CloudflareBrowser Cache TTL setting or a Cache Rule for image pathsThe Cache Rules list, to confirm a rule matches your image path
Object storage and CloudFrontCache-Control metadata on each file, plus a header policy at the CDNThe metadata on one object, then its public URL

WordPress

A caching plugin is the easiest route. WP Rocket applies browser caching rules on its own on Apache hosts, while W3 Total Cache and LiteSpeed Cache expose a browser cache option you should confirm is on. You can also paste the Apache block above the WordPress rules. On Nginx hosts, ask your host.

Shopify and Wix

Both platforms manage the headers for images they host, so you cannot edit them. What you can fix are flagged images from a third party app, an embedded widget or a hotlinked picture: replace them with files uploaded to your own store or site.

Cloudflare and object storage

In Cloudflare, use the Browser Cache TTL setting above, or add a Cache Rule under Rules that matches your image extensions and overrides the browser TTL. With Amazon S3, Cache-Control is metadata stored on each object, so you set it at upload time, for example with the AWS command line:

Text
aws s3 cp ./images s3://your-bucket/images --recursive --cache-control "public, max-age=31536000, immutable"

Amazon CloudFront passes along the Cache-Control your origin or S3 object sends. To set it at the CDN instead, attach a response headers policy with a custom Cache-Control header to the behavior that serves your images. Purge the CDN copy after a change.

Image caching best practices

  • Fingerprint your images for cache busting. Put a hash or version in the file name, such as hero.4f9a2c.webp, and give it public, max-age=31536000, immutable. The immutable flag tells browsers the file never changes so they skip revalidation on reload, but some browsers ignore it, so max-age does the real work. Prefer the file name to a query string, because some CDNs and proxies ignore query strings.
  • Choose public or private on purpose. Use public for shared assets, and keep private for images that belong to one signed in user so a shared cache never stores them.
  • Keep HTML fresh. Serve the page itself with a short lifetime, so visitors find new image URLs quickly. The HTTP cache guide on web.dev explains why versioned URLs and a short HTML lifetime go together.
  • Never send anti-cache values on images. That includes resizer endpoints and plugin thumbnails, which otherwise force a server check on every view.
  • Cover every host. One page often mixes images from your own domain, a CDN subdomain and a plugin generated thumbnail path, and each can send a different header, so run curl against one image from each.

Common image caching mistakes

  • Long lifetime on URLs that change in place. You uploaded a new banner under the old file name, and returning visitors still see last season's version. Rename the file.
  • Plugin added headers. A gallery or resizer plugin may send no-cache on its dynamic endpoints even when your server config is perfect, so curl the thumbnail URL itself.
  • Incomplete extension lists. If your FilesMatch names only jpg and png, the webp and avif files you added later keep their old header and stay flagged, so add every extension you serve.
  • Conflicting values. When curl shows two cache-control lines, usually one from PHP and one from the server config, the browser receives both, so remove or hide one at its source.
  • Relying on Expires alone. Browsers honor it, but our check reads only Cache-Control, so it cannot confirm that lifetime. Send Cache-Control as well.
  • Protected images. Pictures behind a login, firewall or hotlink rule may answer our HEAD request with an error page that carries no-cache.

Lazy load placeholders: plugins that put a data: placeholder in the src attribute cause a flag that no header change can remove. Our check cannot fetch a data URI, so each placeholder counts as flagged. Switch the plugin to a real placeholder file, or to an empty src with the picture held in data-src, or exclude the images above the fold from lazy loading, then run the report again.

How to Fix

The short version, the same steps you see when you click How to Fix in your report.

Why this fails: The report found images sent with Cache-Control no-cache or max-age=0, or images it could not fetch, so the browser must check with the server on every view instead of reusing its copy. Without an ETag or Last-Modified to confirm the image is unchanged, the browser downloads the whole file again.

How to fix it:

Choose the lifetime first. Use one year only when the file name changes whenever the image does. If the name stays the same, pick something shorter, such as 30 days.

  1. Sign in (it is free), open the Image Caching list in the report and copy the uncached URLs. Note whether your server, a CDN or a third party serves each one.
  2. Check for data: placeholders from lazy load plugins. They count as uncached, and headers cannot fix them.
  3. On Apache, add the mod_headers block to .htaccess. Header set replaces an existing no-cache, while mod_expires helps only when no Cache-Control is sent yet.
  4. On Nginx, add a location block for image extensions and if your existing image location sets its own root, alias or proxy_pass, copy that line into the block. If an upstream sends no-cache, add proxy_hide_header Cache-Control, then run nginx -t and reload.
  5. Set a browser TTL rule for your image paths if a CDN serves the files.
Text
# Apache, in .htaccess
<IfModule mod_headers.c>
  <FilesMatch "(?i)\.(jpe?g|png|gif|webp|avif|svg)$">
    Header set Cache-Control "public, max-age=31536000, immutable"
  </FilesMatch>
</IfModule>

# Nginx, inside the server block
location ~* \.(jpe?g|png|gif|webp|avif|svg)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}
  • WordPress: WP Rocket sets browser caching itself on Apache hosts, other caching plugins have an option to switch on.
  • Shopify and Wix: headers for hosted images are fixed, so replace flagged third party or app images.
  • Cloudflare: set Browser Cache TTL or a Cache Rule for image paths.

Re-test: run curl -sIL on one image URL and confirm a long max-age with no no-cache or max-age=0 value. Run our free report again and check that the Image Caching block passes. The MDN HTTP caching guide explains every directive used above.

Re-test my website

Frequently Asked Questions

What is image caching?
Image caching means a browser or CDN keeps a copy of an image after the first download and reuses it until it expires. The server controls this with response headers, mainly Cache-Control. It is different from app image libraries for mobile frameworks, and from the browser setting called cached images and files, which only clears what was stored.
How do I cache images in a browser?
You cache images by having your server, a caching plugin or a CDN send Cache-Control with a long max-age on image responses; the HTML does not change. Pair that with fingerprinted file names so replaced images are fetched fresh. The fix steps section above shows the exact Apache and Nginx settings.
What Cache-Control header should I use for images?
For images with versioned or fingerprinted names, use public, max-age=31536000, immutable. For images that keep the same file name, choose a shorter lifetime, from several days up to a month. Never send no-cache or max-age=0, because our report flags both and they force a check with the server on every view.
How long should images be cached?
One year, which is 31536000 seconds, is the common upper limit and suits fingerprinted files. If the file name never changes, pick a shorter window such as 30 days. A longer lifetime saves more downloads, but a replaced image takes longer to reach visitors who already hold the old copy.
Why are my images not being cached?
In our report, at least one of the first 15 images answered with no-cache or max-age=0, or its request failed or timed out. A data: placeholder from a lazy load plugin counts too. Sign in to open the uncached list, then run curl -sIL on one URL to see which header the server really sends.
How do I update an image that is already cached?
Change the file name or add a version hash so the URL is new, then update the page that references it. Purge the CDN copy if you use one. Reusing the same URL under a long lifetime leaves old copies in visitor browsers, which no purge on your side can reach, so renaming is the dependable route.
Does image caching help SEO?
It is not a direct ranking factor. Caching makes repeat views faster and cuts bandwidth, which supports page experience and Core Web Vitals such as Largest Contentful Paint. PageSpeed Insights also flags short cache lifetimes as a diagnostic, so fixing them removes a common warning from your speed audit.
What are cached images and files in my browser?
They are temporary copies the browser stores on the visitor's device so pages reload faster. Clearing them frees space and forces fresh downloads. Site owners control how long those copies stay fresh through the response headers described on this page.

See how your site scores right now

Paste your address into the free report, then use the How to Fix button on anything that needs work.

Free to use. Results in under a minute for most sites.