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 representationThe 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 choice | Correctness | Cardinality risk | Operational use |
|---|---|---|---|
Full path and normalized query + explicit fmt | Strong and easy to inspect | Bounded by the URL variants you emit | Preferred when the CDN cannot prove image negotiation |
| Full path and query + normalized AVIF/WebP/JPEG bucket | Strong if edge and origin use the same negotiation rule | Three format buckets per transform | Useful at a programmable edge |
Full path and query + raw Accept | Usually correct when Vary is honored | Many syntactically different headers can select the same bytes | Diagnose before accepting the storage cost |
URL only while origin uses fmt=auto | Unsafe | Looks efficient because variants collide | Can serve AVIF bytes to a browser that needs JPEG |
| URL with query ignored | Unsafe for transforms | Widths, qualities, crops, and signatures collide | Never 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 optionsThe 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-WidthThose 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=storeGenerate 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:
| Request | Expected MIME type | Expected second-request evidence |
|---|---|---|
Accept: image/avif,image/webp,image/* | image/avif | Outer cache hit for AVIF bucket |
Accept: image/webp,image/* | image/webp | Outer cache hit for WebP bucket |
Accept: image/jpeg,image/* | image/jpeg | Outer cache hit for fallback bucket |
Explicit fmt=webp, any Accept | image/webp | Same URL remains WebP |
| Same params in a different query order | Same bytes | Same hit only if sorting is intentionally enabled |
Change w=1200 to w=640 | New dimensions | Separate 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
| Signal | Likely cache-key failure | First check |
|---|---|---|
| WebP browser receives AVIF bytes | Accept omitted or normalized incorrectly | CDN key and Vary/variant feature |
Correct MIME type but repeated outer MISS | URL/query fragmentation or object not eligible | Exact URL, query order, cache rule, response cookies |
| Many objects with the same byte hash | Raw Accept or unnecessary headers in key | Variant inventory by normalized format |
Origin MISS rises after adding Client Hints | Unbounded hint values or changed ladder | Effective width/DPR buckets |
Signed URLs return 403 after canonicalization | Query changed after signing | URL builder and edge rewrite order |
Cloudflare returns BYPASS for auto format | Vary for Images rule or extension contract not met | Cloudflare 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
fmtor prove a boundedAcceptvariant 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
sigin 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.
