guide 2 of 3

Accept-CH & Critical-CH

Everything beyond the three default hints is negotiated. Two response headers run that negotiation, and the corner cases (first visits, caches, third parties) are where real deployments succeed or quietly fail.

Accept-CH: the grant

To receive optional hints, name them in an Accept-CH header on any response to a top-level navigation:

HTTP/1.1 200 OK
Accept-CH: Sec-CH-UA-Arch, Sec-CH-UA-Platform-Version, Sec-CH-Prefers-Color-Scheme
BROWSER SERVER GET / HTTP/1.1 Sec-CH-UA · UA-Mobile · UA-Platform HTTP/1.1 200 OK Accept-CH: Sec-CH-UA-Arch, … grant stored for this origin GET /styles.css HTTP/1.1 + Sec-CH-UA-Arch: "arm"
One response header, one stored grant, and every later request arrives pre-hinted.

Four properties of the grant do most of the explaining:

  • Per origin. The browser stores "this origin may get these hints" keyed to the exact scheme+host+port. A grant for example.com says nothing about its subdomains.
  • Persistent until replaced. The stored set lasts across visits, until the user clears site data or a later response sends a different Accept-CH. Each response's list replaces the stored set entirely: sending Accept-CH: Sec-CH-UA-Arch today and an empty Accept-CH: tomorrow revokes everything. There is no additive merge.
  • HTTPS only. Grants are ignored on plain HTTP.
  • Top-level navigations set it. An Accept-CH on a subresource (an image, an XHR) does not update the stored set, so put it on your HTML responses.

This site, for instance, sends its Accept-CH on every response via a static _headers file. That is why the reference tables can show your high-entropy values without any JavaScript negotiation.

The first-request gap

The grant mechanism has an inherent hole: the browser's very first request to your origin happens before it has ever seen your Accept-CH. That request carries only the default hints. Three ways to live with it:

  1. Accept it. Fine for analytics and any hint that improves rather than determines the response. Second page view onward is fully hinted.
  2. Recover in JavaScript. navigator.userAgentData.getHighEntropyValues() returns the same data on the first page, client-side.
  3. Force a retry with Critical-CH. Below.

Critical-CH: the retry

Critical-CH marks a subset of your Accept-CH list as render-critical:

HTTP/1.1 200 OK
Accept-CH: Sec-CH-Prefers-Color-Scheme, Sec-CH-UA-Arch
Critical-CH: Sec-CH-Prefers-Color-Scheme
Vary: Sec-CH-Prefers-Color-Scheme
BROWSER SERVER GET / HTTP/1.1 first visit · default hints only 200 OK · Accept-CH + Critical-CH: Sec-CH-Prefers-Color-Scheme a critical hint is missing, so the browser discards the response GET / HTTP/1.1 (automatic retry) + Sec-CH-Prefers-Color-Scheme: dark 200 OK dark-themed HTML, first paint correct
The Critical-CH retry: one extra round trip on the first visit, and even the first paint is themed correctly.

When a browser receives this and realizes it would have sent Sec-CH-Prefers-Color-Scheme had it known, it discards the response and retries the request with the hint attached, once, before rendering anything. Cost: one round trip, on the first visit only. Benefit: even the first response can be server-rendered in the right theme. The classic user is a dark-mode-aware server renderer (see Sec-CH-Prefers-Color-Scheme).

Every Critical-CH entry must also appear in Accept-CH; keep the critical list to genuinely render-blocking hints, because you are charging every new visitor a round trip for it.

Caching: Vary or suffer

The moment a response body depends on a hint, shared caches must key on it:

Vary: Sec-CH-UA-Mobile, Sec-CH-Prefers-Color-Scheme

Rules of thumb: booleans and small enums (Mobile, color scheme, ECT) vary cleanly, yielding two to four variants. Continuous values (Viewport-Width, RTT) explode cache cardinality; bucket them in application logic before letting them near a cache key.

Delegating hints to third parties

By default, granted hints go to your origin only. Cross-origin subresources (your image CDN, say) receive them only if you delegate explicitly with Permissions-Policy:

Permissions-Policy: ch-dpr=("https://img.example"), ch-viewport-width=("https://img.example")

Each hint has a policy token (ch-ua-arch, ch-device-memory, …). Delegation is a privacy decision, not plumbing: you are extending your user's data to another party, so delegate named hints to named origins and resist wildcards. In HTML, <meta http-equiv="delegate-ch"> covers the no-server-config case.

Deployment checklist

  • Serve Accept-CH on every HTML response, over HTTPS, listing only hints you consume.
  • Remember the list replaces the stored grant; deploys that drop the header revoke it.
  • Add Vary for every hint that changes a cacheable response.
  • Reserve Critical-CH for render-critical hints; it costs first-visit latency.
  • Delegate to third-party origins explicitly via Permissions-Policy.
  • Keep a no-hints fallback path, because Firefox, Safari and every bot will use it.

Next: User-Agent reduction: what the old string no longer tells you.