For years, device-aware delivery meant User-Agent sniffing, client-side JavaScript resizing, or maintaining endless srcset variants. All three shift cost to the client or the developer. Client Hints invert that model: the browser proactively tells the CDN about its constraints, and the edge delivers an optimized response on the first request after opt-in.
When implemented correctly at the CDN layer, Client Hints reduce image payload by 30–60%, eliminate layout shifts from late image swaps, and improve Largest Contentful Paint (LCP) without changing front-end markup. This guide covers how Accept-CH works, how to configure it safely on a CDN, and how to avoid the caching and privacy pitfalls that break many deployments.
1. What Are Client Hints?
Client Hints are a set of HTTP request headers that expose device and context information with explicit user consent. They are defined in RFC 8942 and the User-Agent Client Hints spec. Unlike the legacy User-Agent string, hints are structured, low-entropy by default, and only sent over secure contexts (HTTPS).
There are two families:
- Network & Device Hints (RFC 8942):
DPR,Viewport-Width,Width,Device-Memory,ECT(Effective Connection Type),RTT,Downlink - User-Agent Client Hints (UA-CH):
Sec-CH-UA,Sec-CH-UA-Mobile,Sec-CH-UA-Platform,Sec-CH-UA-Full-Version-List— the privacy-preserving replacement for User-Agent
The browser does not send hints by default. The server must opt-in via Accept-CH (or Critical-CH for high-priority hints). Once opted in, the browser persists the preference for that origin and includes the requested hints on subsequent navigations and subresource requests.
2. Accept-CH, Critical-CH, and Permissions-Policy
The opt-in flow has three headers you must understand:
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
Accept-CH
Sent in the response to tell the browser which hints to include next time:
HTTP/1.1 200 OK
Accept-CH: Sec-CH-UA-Mobile, Sec-CH-UA-Platform, DPR, Viewport-Width, Width, Device-Memory
Vary: Sec-CH-UA-Mobile, DPR, Viewport-Width, Width
Cache-Control: public, max-age=86400
Permissions-Policy: ch-ua-mobile=(self), ch-dpr=(self "https://img.flyfishnwa.com"), ch-viewport-width=(self)
On the next request, the browser will send:
GET /images/hero.jpg HTTP/2
Sec-CH-UA-Mobile: ?1
DPR: 2
Viewport-Width: 390
Width: 780
Device-Memory: 8
Critical-CH
By default, hints only arrive on the second request. Critical-CH forces a restart if a critical hint is missing, ensuring the first HTML response is already optimized. Use sparingly — it costs an extra round trip.
Critical-CH: Sec-CH-UA-Mobile, DPR
Accept-CH: Sec-CH-UA-Mobile, DPR, Viewport-Width
If the browser did not send DPR initially, it will retry the request with it. Ideal for LCP images where device pixel ratio matters immediately.
Permissions-Policy (Delegation)
For third-party origins (e.g., your image CDN on img.flyfishnwa.com), you must delegate hints via Permissions-Policy or the allow attribute on iframes. Without delegation, hints are stripped from cross-origin requests even if Accept-CH was set.
For a deeper look at edge delegation and header trust, see our CDN architecture overview and CDN security guide on header validation at the edge.
3. How a CDN Handles Client Hints at the Edge
A device-aware CDN does not just forward hints to origin — it acts on them. The typical flow:
Modern CDNs like Cloudflare, Fastly, Akamai, and CloudFront support Client Hints natively:
- Image optimization: Automatically resize to
Width/Viewport-Width×DPRand convert to AVIF/WebP ifAccept: image/avifis present. - Adaptive CSS/JS: Serve lighter bundles when
Device-Memory < 2orECT: 2g. - Logging: Forward hints as structured logs for monitoring and observability without parsing User-Agent.
Configure your CDN to normalize hint values into buckets (e.g., DPR 1, 1.5, 2, 3; Width rounded to 100px) before using them as cache keys. Otherwise, cache fragmentation will destroy your hit ratio.
4. Device-Aware Use Cases That Matter
Responsive Images Without srcset Bloat
Instead of authoring 6 variants per image:
<!-- Traditional -->
<img src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, 50vw" alt="River at dawn">
<!-- With Client Hints -->
<img src="hero.jpg" alt="River at dawn">
<meta http-equiv="Accept-CH" content="DPR, Viewport-Width, Width">
The CDN reads Width: 780 + DPR: 2 and serves a 780px WebP/AVIF from a single canonical URL. HTML stays cacheable and clean.
Network-Aware Delivery
Use ECT and RTT to downgrade gracefully: serve low-quality image placeholders (LQIP), disable autoplay video, or defer non-critical JS when ECT: 2g or RTT > 600ms. This is more reliable than navigator.connection alone because it is available server-side before any JS executes.
Memory-Aware Bundles
Device-Memory (0.25, 0.5, 1, 2, 4, 8 GB) lets you serve a lite bundle to low-end devices. At the edge, route Device-Memory: 1 to /app.lite.js while high-memory devices get full interactivity.
These patterns build on core CDN fundamentals — moving decision logic from client to edge reduces TTFB and CLS.
5. Caching Correctly: Vary, Normalization, and Hit Ratio
This is where most teams fail. If you add Accept-CH but forget Vary, caches (both CDN and browser) will serve the wrong variant to the next user.
# Correct origin/CDN response
HTTP/1.1 200 OK
Content-Type: image/webp
Vary: Accept, DPR, Width, Viewport-Width, Sec-CH-UA-Mobile
Cache-Control: public, max-age=31536000, immutable
Accept-CH: DPR, Width, Viewport-Width
Rules for production:
- Always send Vary for every hint you act on. Include
Acceptif you negotiate image formats. - Normalize hints at the edge. Example: round
Widthto nearest 100, clampDPRto [1,2,3]. Fastly VCL or Cloudflare Workers can do this before cache lookup. - Limit variants. Do not vary on
RTTorDownlink— they change per request and will fragment cache. Use them for dynamic HTML only, not cacheable images. - Set distinct TTLs. Image variants: long TTL (1 year, immutable). HTML that uses
Critical-CH: short TTL (60–300s) orprivate.
| Client Hint | Example Value | CDN Action | Cache Impact |
|---|---|---|---|
DPR | 2 | Serve 2× image, select WebP/AVIF | Vary: DPR Low fragmentation (3 buckets) |
Viewport-Width | 390 | Cap image width to viewport | Vary: Viewport-Width Medium — normalize to 100px |
Width | 780 | Exact resource width (with sizes) | Vary: Width Medium — requires Sizes header |
Sec-CH-UA-Mobile | ?1 | Mobile vs desktop layout | Vary: Sec-CH-UA-Mobile Low (2 values) |
Device-Memory | 4 | Lite vs full JS bundle | High if overused — use for HTML only |
ECT / RTT | 4g / 50 | Adaptive bitrate, defer heavy assets | Do NOT Vary — dynamic only |
6. Privacy, Security, and Fallbacks
Client Hints were designed to improve privacy versus User-Agent, but they still expose fingerprinting surface. Browsers enforce:
- HTTPS only. Hints are never sent over HTTP.
- Opt-in persistence is origin-scoped and expires with the
Accept-CHcache (typically session or until cleared). - High-entropy hints (
Sec-CH-UA-Full-Version-List,Sec-CH-UA-Platform-Version,Device-Memory) require explicit request and may be gated by Permissions-Policy and user settings.
Security checklist:
- Validate hint values at the edge — reject non-numeric
DPRor absurdWidth > 5000to prevent cache poisoning. - Do not log high-entropy hints with IP without consent. Anonymize or bucket them.
- Provide fallback for Safari/Firefox. If hints absent, fall back to
Acceptheader for format negotiation and client-sidesrcset. Never block content.
See CDN security best practices for validating untrusted request headers at the edge.
7. Implementation: Nginx, Varnish, and Edge Workers
Nginx (origin or edge)
# nginx.conf — advertise hints and cache correctly
add_header Accept-CH "Sec-CH-UA-Mobile, DPR, Viewport-Width, Width" always;
add_header Vary "Sec-CH-UA-Mobile, DPR, Viewport-Width, Width, Accept" always;
add_header Permissions-Policy "ch-ua-mobile=(self), ch-dpr=(self), ch-viewport-width=(self), ch-width=(self)" always;
# Normalize Width for cache key (requires lua or njs)
# Example: map Width to nearest 100
map $http_width $normalized_width {
default 800;
~^([0-9]+)$ $1;
}
Varnish VCL
sub vcl_recv {
# Normalize DPR to 1,2,3
if (req.http.DPR) {
if (std.real(req.http.DPR, 1) >= 2.5) { set req.http.X-DPR-Norm = "3"; }
elsif (std.real(req.http.DPR, 1) >= 1.5) { set req.http.X-DPR-Norm = "2"; }
else { set req.http.X-DPR-Norm = "1"; }
}
}
sub vcl_hash {
hash_data(req.http.X-DPR-Norm);
hash_data(req.http.Width);
}
Cloudflare Worker (edge normalization)
export default {
async fetch(req, cf) {
const dpr = parseFloat(req.headers.get("DPR") || "1");
const normDpr = dpr >= 2.5 ? "3" : dpr >= 1.5 ? "2" : "1";
const width = parseInt(req.headers.get("Width") || "800", 10);
const normWidth = Math.min(2000, Math.ceil(width / 100) * 100);
// Use normalized values for cache key and image resizing
const res = await fetch(req, { cf: { cacheKey: `${req.url}|dpr=${normDpr}|w=${normWidth}` }});
const headers = new Headers(res.headers);
headers.set("Vary", "DPR, Width, Viewport-Width, Sec-CH-UA-Mobile, Accept");
headers.set("Accept-CH", "Sec-CH-UA-Mobile, DPR, Viewport-Width, Width");
return new Response(res.body, { ...res, headers });
}
}
These examples integrate with your existing load balancing and caching tiers — normalize before you hash, and always set Vary downstream.
8. Measuring Impact
After rollout, track: cache hit ratio per variant, image bytes saved (compare Content-Length before/after), LCP and CLS via Real User Monitoring, and origin offload. A healthy deployment shows >85% hit ratio on image variants and 25–40% reduction in median image weight. If hit ratio drops below 70%, you are varying on too many hints or not normalizing.
Use Server-Timing to expose edge decisions:
Server-Timing: ch;desc="dpr=2 width=800", img-opt;dur=12, cache;desc="HIT"
This makes debugging variant selection trivial in DevTools and aligns with observability pipelines that ingest edge logs.
Conclusion
Client Hints with Accept-CH let CDNs deliver truly device-aware content without client-side complexity. Opt in explicitly, delegate via Permissions-Policy, normalize hints before caching, and always send Vary. Start with low-entropy hints (DPR, Viewport-Width, Sec-CH-UA-Mobile) for image optimization, then expand to network and memory hints for adaptive HTML. Done right, you serve fewer bytes, faster — on every device.
Frequently Asked Questions
Do Client Hints replace responsive images (srcset/sizes)?
No. Client Hints complement responsive images. Use srcset for art direction and layout, and Client Hints for automated, server-driven optimization like DPR-aware resizing and format negotiation at the CDN edge without markup changes.
How do Client Hints affect CDN cache hit ratio?
Each unique hint combination creates a separate cache variant via the Vary header. To protect hit ratio, only vary on essential hints (e.g., DPR, Viewport-Width, Sec-CH-UA-Mobile), normalize values into buckets, and avoid varying on volatile hints like RTT.
Are Client Hints supported in all browsers?
Chromium browsers (Chrome, Edge, Opera) fully support Client Hints. Safari and Firefox support a limited subset and rely more on User-Agent or Sec-CH-UA. Always implement graceful fallback to User-Agent parsing or client-side JS when hints are absent.
Is Accept-CH a privacy risk?
High-entropy hints like Sec-CH-UA-Full-Version or Device-Memory can aid fingerprinting. Browsers require HTTPS and Permissions-Policy delegation, and only send hints after an Accept-CH opt-in. Avoid requesting unnecessary high-entropy hints and anonymize logs.