Home › Protocols › CDN Client Hints

CDN Client Hints: Device-Aware Content Delivery with Accept-CH

Protocols By Alex Petrov, DevOps & Infrastructure Engineer Sep 15, 2026 9 min read

Client Hints let your CDN serve the right bytes for every device without bloating HTML or relying on fragile User-Agent parsing. With Accept-CH, the edge can adapt images, CSS, and even JS bundles to viewport, DPR, network, and memory — automatically.

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:

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.

Key concept: Think of Client Hints as server-driven content negotiation. Instead of the client guessing which asset to request, the server declares what it can adapt on, and the client supplies facts. This is ideal for CDNs that perform image optimization, format negotiation, and adaptive delivery at the edge.

2. Accept-CH, Critical-CH, and Permissions-Policy

The opt-in flow has three headers you must understand:

Performance Comparison: Edge vs Origin Processing
Metric Edge Processing Origin Processing Improvement
Latency (p50)8–15 ms120–300 ms15–20x faster
Cache Hit Ratio85–95%N/AReduces origin load
TLS Handshake1–5 ms (nearby PoP)50–150 ms10–30x faster
Bandwidth Cost$0.01–0.04/GB$0.05–0.12/GB3–5x cheaper
Availability99.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:

Client Hints Flow via CDN Edge Browser requests page, CDN responds with Accept-CH, browser resends with hints, CDN serves optimized variant. BROWSER Chrome / Edge CDN EDGE Cache + Image Opt ORIGIN Static / API GET / (no hints) 200 + Accept-CH: DPR, Viewport-Width GET /hero.jpg + DPR:2 Width:780 cache miss → origin 780w WebP (Vary: DPR, Width)
Figure 1: Accept-CH negotiation. The first response opts in; subsequent requests carry hints and receive device-specific variants from edge cache.

Modern CDNs like Cloudflare, Fastly, Akamai, and CloudFront support Client Hints natively:

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:

  1. Always send Vary for every hint you act on. Include Accept if you negotiate image formats.
  2. Normalize hints at the edge. Example: round Width to nearest 100, clamp DPR to [1,2,3]. Fastly VCL or Cloudflare Workers can do this before cache lookup.
  3. Limit variants. Do not vary on RTT or Downlink — they change per request and will fragment cache. Use them for dynamic HTML only, not cacheable images.
  4. Set distinct TTLs. Image variants: long TTL (1 year, immutable). HTML that uses Critical-CH: short TTL (60–300s) or private.
Client HintExample ValueCDN ActionCache Impact
DPR2Serve 2× image, select WebP/AVIFVary: DPR Low fragmentation (3 buckets)
Viewport-Width390Cap image width to viewportVary: Viewport-Width Medium — normalize to 100px
Width780Exact resource width (with sizes)Vary: Width Medium — requires Sizes header
Sec-CH-UA-Mobile?1Mobile vs desktop layoutVary: Sec-CH-UA-Mobile Low (2 values)
Device-Memory4Lite vs full JS bundleHigh if overused — use for HTML only
ECT / RTT4g / 50Adaptive bitrate, defer heavy assetsDo 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:

Security checklist:

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.

References & Standards: W3C Web Performance · OWASP CDN Security