Skip to content
All postsImage requests grouped into bounded AVIF, WebP, and JPEG cache variants

Image CDN cache keys: tame Vary: Accept

Design image cache keys that keep AVIF, WebP, and JPEG correct without multiplying variants for every raw Accept header or query permutation.

cachingimage-formatsoperations

An image CDN cache key should identify the bytes that can change, not every spelling of the request that produced them. Put the source identity and effective transform parameters in the key. When one URL negotiates AVIF, WebP, or JPEG, add one normalized output-format dimension—or use an explicit fmt value in the URL. Do not blindly key on the entire raw Accept string.

That answer has two parts because there are two caches. Keenpix builds an internal transform key after it has resolved the output format and Client Hints. A CDN in front of Keenpix sees the public URL and request headers. Both layers must agree about representation identity, but they do not have to serialize the key the same way.

The minimum safe identity

For a deterministic image transform, start with this model:

source identity
  + tenant or project boundary
  + effective width, height, DPR, fit, crop, quality, format, and effects
  + configuration that changes pixels, such as a watermark
= one reusable representation

The request method and target URI are the minimum HTTP cache key under RFC 9111. A stored response carrying Vary: Accept may be reused only when the selecting request field matches as defined by the RFC. Vary protects correctness; it does not promise that a particular CDN will store the response, normalize equivalent values, or keep the number of variants small.

Key choiceCorrectnessCardinality riskOperational use
Full path and normalized query + explicit fmtStrong and easy to inspectBounded by the URL variants you emitPreferred when the CDN cannot prove image negotiation
Full path and query + normalized AVIF/WebP/JPEG bucketStrong if edge and origin use the same negotiation ruleThree format buckets per transformUseful at a programmable edge
Full path and query + raw AcceptUsually correct when Vary is honoredMany syntactically different headers can select the same bytesDiagnose before accepting the storage cost
URL only while origin uses fmt=autoUnsafeLooks efficient because variants collideCan serve AVIF bytes to a browser that needs JPEG
URL with query ignoredUnsafe for transformsWidths, qualities, crops, and signatures collideNever use for /img/*

Observation: what Keenpix keys today

Verified against the public repository on August 23, 2026: Keenpix parses the request first, resolves fmt=auto to avif, webp, or jpeg, rounds automatic Client Hints to bounded ladders, then hashes this object with SHA-256:

projectId + source URL + full effective transform options

The effective options include the resolved output format, dimensions, DPR, quality, crop and effects, metadata policy, and project watermark configuration. A signature is checked before the internal cache lookup but is not part of this transform key. Two valid signed URLs for the same effective transform can therefore share internal bytes.

The current automatic ladders contain 12 widths (320 through 5000) and seven DPR values (1 through 3). Before other options, that creates a theoretical ceiling of 252 header-derived width/DPR/format combinations for one source: 12 × 7 × 3. A project maximum width and the browser population usually reduce that set. The number is a code-derived upper-bound model, not a measured Keenpix production count.

Every successful transform response currently sends:

Cache-Control: public, max-age=31536000, immutable
Accept-CH: Sec-CH-DPR, Sec-CH-Width, Sec-CH-Viewport-Width
Vary: Accept, Sec-CH-DPR, Sec-CH-Width, Sec-CH-Viewport-Width, DPR, Width, Viewport-Width

Those headers describe every request field that can affect the response. They do not configure your outer CDN for you. Read the Keenpix caching reference and parameter bounds before changing the edge key.

Why raw Accept can multiply storage

These request headers all select AVIF in the current Keenpix parser:

Accept: image/avif,image/webp,image/*
Accept: image/avif,image/webp,*/*;q=0.8
Accept: image/webp,image/avif,image/png,*/*

Keenpix collapses them internally because it stores the resolved avif format in the transform options. An intermediary that retains a separate object for each raw header value may store three copies of identical bytes. Browser versions, embedded webviews, crawlers, and application fetches create more spellings over time.

There is another important limit: the current Keenpix negotiation code checks whether the header contains image/avif, then image/webp; it does not rank media-type quality values. A custom edge normalizer for Keenpix must mirror that exact rule until the origin parser changes. A general-purpose media server that honors q=0 or quality ordering needs a real Accept parser instead. Never deploy a cache normalizer whose selection logic differs from the origin.

Recommendation: make the format explicit when possible

An explicit URL is the easiest cache contract to operate:

<picture>
  <source
    srcset="https://images.example.com/img/https://assets.example.com/hero.jpg?project=store&w=1200&fmt=avif"
    type="image/avif"
  />
  <source
    srcset="https://images.example.com/img/https://assets.example.com/hero.jpg?project=store&w=1200&fmt=webp"
    type="image/webp"
  />
  <img
    alt="Product team reviewing an image delivery dashboard"
    height="675"
    src="https://images.example.com/img/https://assets.example.com/hero.jpg?project=store&w=1200&fmt=jpeg"
    width="1200"
  />
</picture>

The browser chooses a supported <source>, and each resulting URL has one format. Keep the width ladder finite and generated from actual layout slots. Do not create an arbitrary integer width endpoint merely because the transform service accepts one.

Use fmt=auto when a single URL matters and the CDN has documented, tested image-variant support. Cloudflare's official Vary for Images feature parses Accept into image variants, but it requires a configured variants rule and a compatible image extension in the path. Cloudflare also documents that custom request-header cache keys are Enterprise-only. Returning Vary: Accept by itself is not equivalent to enabling either feature.

A bounded Nginx cache example

The safest Nginx example uses explicit fmt URLs, so no request-header normalization is needed:

proxy_cache_path /var/cache/nginx/keenpix levels=1:2 keys_zone=keenpix:20m
                 max_size=20g inactive=30d use_temp_path=off;

server {
    listen 443 ssl;
    server_name images.example.com;

    location /img/ {
        proxy_pass http://keenpix_transform;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_cache keenpix;
        proxy_cache_methods GET HEAD;
        proxy_cache_convert_head on;
        proxy_cache_key "$scheme$host$request_uri";
        proxy_cache_valid 200 30d; # Fallback when the origin sends no cache lifetime.
        add_header X-Cache-Status $upstream_cache_status always;
    }
}

Nginx enables proxy_cache_convert_head by default, converting HEAD to GET for caching. Omitting the request method from this key deliberately lets HEAD reuse the GET representation. If you disable that conversion, follow the official directive guidance and include $request_method so responses from different methods cannot collide.

$request_uri retains the complete path and query. Do not switch to $uri alone, do not enable an ignore-query-string mode, and do not exclude sig from an outer key unless the edge verifies that signature before cache lookup. The signed URL guide explains why an unverified cache hit can bypass origin enforcement.

proxy_cache_valid 200 30d is only a fallback lifetime. Nginx gives origin X-Accel-Expires or, when it is absent, Expires and Cache-Control response headers higher priority than that directive. Keenpix's current Cache-Control: public, max-age=31536000, immutable therefore takes precedence unless an operator explicitly disables header processing. Do not add proxy_ignore_headers Cache-Control Expires Vary to this example.

Nginx 1.7.7 and later do not cache Vary: *; for other Vary values, Nginx caches the response while taking the named request fields into account. This example deliberately retains that behavior. Even an explicit fmt URL can retain conservative header variants because Keenpix currently advertises Vary on every successful transform. Measure that cardinality before overriding it, and never ignore Vary unless every named header has been proven unable to change the bytes.

For fmt=auto, verify cold and warm requests for AVIF, WebP, and fallback clients on the exact Nginx version and configuration you operate. If you instead add an explicit format bucket to proxy_cache_key, make the bucket logic identical to the origin and test fallback requests with no Accept header. A copied regex that looks only for webp can collapse AVIF and WebP into the wrong entry.

Canonicalize what is semantically unordered

Query order can fragment an outer cache even when the application parser treats these URLs identically:

?project=store&w=1200&q=80&fmt=webp
?fmt=webp&q=80&w=1200&project=store

Generate one canonical order in your application. If the CDN offers query sorting, enable it only after confirming that no endpoint under the rule assigns meaning to duplicate-parameter order. Cloudflare documents Query String Sort as a way to increase hit rate, but the cache rule must remain scoped to the image endpoint.

Do not remove “unknown” parameters at the CDN to improve hit rate when URLs are signed. Keenpix signs every query parameter except sig; changing the public query after signing makes the request invalid. Prefer a strict URL builder that never emits tracking or cache-buster parameters on transform URLs.

Production verification matrix

Use one source and one transform URL. Purge that object or use a new versioned source path before the test. Then run each row twice from the same edge region:

RequestExpected MIME typeExpected second-request evidence
Accept: image/avif,image/webp,image/*image/avifOuter cache hit for AVIF bucket
Accept: image/webp,image/*image/webpOuter cache hit for WebP bucket
Accept: image/jpeg,image/*image/jpegOuter cache hit for fallback bucket
Explicit fmt=webp, any Acceptimage/webpSame URL remains WebP
Same params in a different query orderSame bytesSame hit only if sorting is intentionally enabled
Change w=1200 to w=640New dimensionsSeparate object

Example probe:

curl --fail --silent --show-error \
  --dump-header - \
  --output /dev/null \
  --header 'Accept: image/webp,image/*' \
  'https://images.example.com/img/https://assets.example.com/hero.jpg?project=store&w=1200&fmt=auto'

Check Content-Type, Vary, Cache-Control, Age, the CDN's cache-status header, and Keenpix's X-Keenpix-Cache. CF-Cache-Status: HIT means the request did not reach Keenpix; X-Keenpix-Cache: HIT describes the internal layer only when the request did reach the origin. They are different observations.

Failure signals and likely causes

SignalLikely cache-key failureFirst check
WebP browser receives AVIF bytesAccept omitted or normalized incorrectlyCDN key and Vary/variant feature
Correct MIME type but repeated outer MISSURL/query fragmentation or object not eligibleExact URL, query order, cache rule, response cookies
Many objects with the same byte hashRaw Accept or unnecessary headers in keyVariant inventory by normalized format
Origin MISS rises after adding Client HintsUnbounded hint values or changed ladderEffective width/DPR buckets
Signed URLs return 403 after canonicalizationQuery changed after signingURL builder and edge rewrite order
Cloudflare returns BYPASS for auto formatVary for Images rule or extension contract not metCloudflare variant configuration

Do not diagnose a low hit ratio from one layer's metric. Edge hits never contact the origin, so an origin-only dashboard cannot count them. The self-hosting CDN guide describes that measurement boundary.

Production checklist

  • Cache only /img/*; exclude dashboard, auth, API, and health routes.
  • Include scheme, host, complete path, and the full normalized transform query.
  • Use explicit fmt or prove a bounded Accept variant mechanism with three cold/warm tests.
  • Keep automatic widths and DPRs on a finite ladder; do not key on arbitrary raw hint strings.
  • Ensure watermark and other project-level pixel changes reach the internal key.
  • Preserve sig in the outer key unless the edge verifies it before lookup.
  • Canonicalize query order before signing and cache lookup.
  • Record edge and Keenpix-origin cache statuses separately.
  • Alert on wrong Content-Type, repeated misses, variant-count growth, and origin-transform growth.
  • Test purge behavior for every format variant before relying on emergency invalidation.

Explicit limits

This guide does not claim that three formats mean only three total cached images. Width, DPR, quality, crop, source version, watermark configuration, and other transforms remain legitimate dimensions. It also does not claim a universal CDN implementation for Vary; verify the provider and plan you operate.

The current Keenpix internal format set for automatic negotiation is exactly AVIF, WebP, or JPEG. Explicit format parameters add PNG, GIF, HEIF, TIFF, and SVG paths. Automatic Client Hint bucketing is bounded in repository code, but explicit numeric parameters can still create many valid variants. Use signed URLs when callers should not be able to manufacture new parameter combinations.

Continue the operations cluster

Next, read how to defend image pipelines against SSRF and image bombs and how to prevent cache stampedes and size transform capacity. For codec-specific testing, use AVIF vs WebP in production. To activate Keenpix, follow the managed cloud quickstart or the self-hosting guide.

Sources and verification

Verified August 23, 2026 against RFC 9111 HTTP Caching, RFC 9110 Vary semantics, Cloudflare Vary for Images, Cloudflare cache keys, Cloudflare Query String Sort, and the official Nginx proxy cache directives. Keenpix observations were checked against the current public parameter parser, cache coordinator, transform runtime, and their tests. Recommendations are operating patterns, not measured Keenpix performance claims.

Optimized images, minus the surprise bill.

One published price on managed image delivery. Or self-host the open-source engine, free.