The right way to cache images is to put a version or hash in the file name and cache those files for a year with Cache-Control: public, max-age=31536000, immutable, while giving images at stable URLs a short cache time plus ETag or Last-Modified so browsers can check for changes cheaply. Everything else, from CDNs to purges, builds on that one split.

How does browser caching work for images?

When your server sends an image, the Cache-Control header tells the browser and any CDN in between how long they can reuse it. While the copy is "fresh," the browser uses it without asking. Once it's "stale," the browser asks the server whether it changed.

That check uses validators. The server sends an ETag (a fingerprint of the file) or a Last-Modified date. Next time, the browser sends them back in If-None-Match or If-Modified-Since. If nothing changed, the server replies 304 Not Modified with no body. It's a round trip, but a tiny one.

HTTP caching rules live in RFC 9111. The immutable directive is a separate extension defined in RFC 8246. It tells the browser the file will never change at that URL, so it shouldn't even revalidate on a reload. Browser support for immutable has varied over the years, but sending it does no harm.

How long should you cache images?

It depends on one question: will the file at this URL ever change?

Versioned or fingerprinted file names: cache for a year

Most build tools and frameworks can add a content hash to file names. logo.png becomes logo.a81f2e.png. When you edit the logo, the hash changes, your HTML points to the new name, and the old cached copy just stops being used.

These files can safely be cached for a year:

Cache-Control: public, max-age=31536000, immutable

Both MDN and web.dev recommend this pattern for versioned URLs. A query string like ?v=3 works too, as long as your CDN includes the query string in its cache key. Most do by default, but check.

Stable URLs: cache briefly and revalidate

Some images can't change names. Your og:image may already be stored in social platforms' caches. /favicon.ico gets requested by that exact path. A logo URL may be listed in your structured data. For these, pick a short fresh time and let validators do the rest:

Cache-Control: public, max-age=3600
ETag: "5d8c72a5edda8d6a"

An hour to a day is a reasonable range for most of these. If you want every load to check with the server, use no-cache, which (despite the name) still allows storing, just not reusing without revalidation.

If you set a long max-age on /og-image.png and then swap the file, some visitors and CDN nodes keep the old one until the timer runs out. Social platforms also have their own caches. Our post on why your social share image isn't updating covers how to clear those.

What headers should each image type get?

Here's a starting point. Adjust the times to how often you actually change things.

Image typeExample URLCache-ControlValidator
Fingerprinted content images/assets/hero.3f9a2c.avifpublic, max-age=31536000, immutableOptional
CMS uploads (unique names per upload)/uploads/2026/09/team-photo.jpgpublic, max-age=2592000 (30 days)ETag
Social share image at a stable URL/og-image.pngpublic, max-age=3600ETag or Last-Modified
Favicon at a fixed path/favicon.icopublic, max-age=86400ETag or Last-Modified
Logo referenced in structured data/logo.pngpublic, max-age=86400ETag
Private or per-user images/account/avatarprivate, max-age=300 or no-storeETag

CMS uploads sit in the middle because editors sometimes replace a file in place with the same name. If your CMS never does that, treat them like fingerprinted files.

Favicons have a few quirks of their own, including browsers that cache them far longer than your headers say. See favicon cache headers for the details.

Why does Content-Type matter for images?

The Content-Type header tells the browser what the bytes are. Get it wrong and odd things happen. An SVG served as text/plain won't render as an image. An AVIF served as application/octet-stream may work in one browser and fail in another. With X-Content-Type-Options: nosniff set, which you should, the browser trusts the header and won't guess.

The usual values:

  • image/jpeg, image/png, image/gif
  • image/webp, image/avif
  • image/svg+xml
  • image/x-icon or image/vnd.microsoft.icon for .ico

AVIF is the one old server configs most often miss. If your web server's MIME table predates it, add it. Our guide to favicon HTTP headers shows how to check the headers a live file is actually sending.

When do you need Vary: Accept?

Some setups serve different formats from one URL. The browser's Accept header says it can handle image/avif, so the server returns AVIF. Another browser gets JPEG from the same URL. This is called content negotiation.

If you do this, send Vary: Accept. MDN describes Vary as the header that makes caches store separate copies based on the listed request headers. Without it, a CDN might cache the AVIF version and hand it to a browser that can't decode it.

There's a catch. Accept strings differ across browsers and versions, so a naive cache splits into many near-identical copies. That hurts your hit rate. CDNs handle this differently. Some normalize the Accept header into a few buckets, some only honor Vary for certain headers, and some need a setting turned on. Read your CDN's docs before you rely on it.

The simpler option is to skip negotiation and use <picture> with separate URLs per format. Each file then has one URL and one type, and caching stays boring. Our format comparison shows the <picture> markup.

Should you use an image CDN?

An image CDN, or image transform service, fetches your original and returns resized, compressed, and converted versions based on URL parameters or rules. Examples include Cloudflare Images, imgix, Cloudinary, and the built-in image optimizer in Next.js. There are many others, and self-hosted options exist too.

What they're good at:

  • Generating every width you need for srcset without a build step. That pairs well with responsive images using srcset and sizes.
  • Picking AVIF or WebP per browser and handling the Vary problem for you.
  • Serving from edge locations close to your visitors.

What to watch:

  • Transform URLs are usually cached by the full URL, including parameters. Keep parameters consistent, or you'll generate near-duplicates.
  • If the original changes but keeps its name, the transformed copies don't know. You'll need a purge or a new URL.
  • Pricing is often based on transforms, bandwidth, or stored images. A srcset with ten widths across five formats adds up.
  • Check what headers the service sends. Some let you set Cache-Control per path. Others apply their own defaults.

How should you purge cached images?

The best purge is the one you never have to do. Fingerprinted names mean you deploy a new URL instead of clearing an old one. For stable URLs, a short max-age caps how long a stale copy can live.

When you do need to purge, go in this order:

  1. Purge the exact URL at your CDN. Most CDNs support single-URL purges through a dashboard or API.
  2. If an image CDN sits in front, purge the original and its transformed versions. Some services do this together, some don't.
  3. Remember that browsers still hold their own copies. A CDN purge doesn't reach them. They'll update when their max-age ends.
  4. For share images, re-scrape with each platform's debugger tool.

Purge-everything buttons are tempting. They also send a flood of requests to your origin at once. Use them rarely.

Should you block image hotlinking?

Hotlinking is when another site embeds your image directly, so your server pays for the bandwidth. The usual defense checks the Referer header and blocks requests from unknown sites. Many CDNs offer this as a toggle.

Be careful with it. Some browsers and privacy tools strip Referer, so a strict rule can block your own visitors. Allow empty referrers. And never apply hotlink protection to your og:image, favicon, or logo. Social platforms, search engines, and feed readers fetch those from their own servers, and blocking them breaks your previews and icons. If you care about Google Images traffic, see image SEO for Google Images before blocking anything broadly.

Common mistakes

  • A year-long cache on an unversioned URL. You can't take it back for visitors who already have it.
  • no-store on every image. Usually a copy-paste from an HTML rule. It forces a full download every time.
  • Negotiating formats without Vary: Accept. Someone eventually gets a format their browser can't show.
  • Missing validators. Without ETag or Last-Modified, a stale image means a full re-download instead of a 304.
  • Wrong or missing AVIF MIME type. Common on older servers.
  • Hotlink rules that block social crawlers. Broken previews are the usual symptom.

Want to see what your server really sends? The favicon checker requests your live icon files and shows the status and content type for each one, which is a quick way to catch a CDN serving HTML where an image should be.

TL;DR

Put a hash or version in image file names and cache them for a year with immutable. Give images at fixed URLs, like your share image and favicon, a short max-age plus an ETag. Send correct Content-Type values, add Vary: Accept only if you negotiate formats, and plan purges so you rarely need them.

Sources