3 Signals Your Server Is Missing: A Technical Guide to Structured Client Hints
Stop relying on brittle User-Agent strings and start using Sec-CH headers to deliver precision-optimized assets and layouts based on real-time hardware data.
The User-Agent string is a historical accident that grew into a 150-character catastrophe. For decades, we’ve been parsing this bloated, lying string of text with increasingly complex regular expressions just to figure out if a user is on a phone or if they can handle a modern image format.
But the industry is finally moving away from this "guesswork-as-a-service" model. Browsers are freezing the UA string, stripping it of its useful bits to prevent fingerprinting, and replacing it with something much more surgical: Structured Client Hints. If your server is still just reading navigator.userAgent, you are flying blind while wearing a blindfold designed in 2005.
Client Hints allow a server to proactively ask the browser for the specific hardware and network data it needs. No more guessing. No more 50KB regex libraries. Just clean, structured headers that let you deliver a high-performance experience.
Here are the three critical signals your server is currently missing and how to start using them.
---
1. The Memory Signal: Device-Memory
We’ve all been there: you build a beautiful, interactive React or Vue dashboard that runs like butter on your M2 MacBook, only to have it crawl to a halt on a $100 Android device. The problem isn't necessarily the CPU; it's the RAM.
When a device runs out of memory, the browser starts killing tabs or aggressive garbage collection kicks in, causing massive frame drops. Traditionally, we had no way of knowing a device's memory capacity on the server side until the JavaScript had already loaded. By then, it’s too late—you’ve already sent a 2MB bundle of JS.
The Power of Device-Memory
The Device-Memory hint tells your server exactly how much RAM the device has, rounded to the nearest power of two (to prevent fingerprinting). You’ll get values like 0.25, 0.5, 1, 2, 4, or 8.
If you know a user has only 1GB of RAM, why are you sending the "Heavy Data Visualization" package? You should be sending the "Lite" version.
Practical Implementation
To get this data, your server must first "opt-in" by telling the browser it wants the hint via the Accept-CH header.
The Initial Response from Server:
HTTP/1.1 200 OK
Content-Type: text/html
Accept-CH: Device-Memory
Vary: Device-MemoryOnce the browser sees this, subsequent requests to your domain will include the hint.
The Subsequent Request from Client:
GET /assets/main-bundle.js HTTP/1.1
Host: example.com
Device-Memory: 2Server-Side Logic (Node.js/Express example):
I’ve found that the best way to handle this is at the middleware level. Instead of just serving one index.html, you can change your asset paths dynamically.
app.get('/', (req, res) => {
const ram = parseFloat(req.get('Device-Memory')) || 8; // Default to 8 if unknown
let scriptPath = '/js/standard-app.js';
if (ram <= 1) {
// Low-end device logic: Send a version with fewer dependencies
scriptPath = '/js/lite-app.js';
} else if (ram >= 8) {
// High-end device logic: Preload heavy assets or 3D models
scriptPath = '/js/ultra-app.js';
}
res.send(`
<html>
<body>
<div id="root"></div>
<script src="${scriptPath}"></script>
</body>
</html>
`);
});The Vary: Device-Memory header is crucial here. It tells CDNs and caches that the response they store for a 2GB device should not be served to an 8GB device.
---
2. The Network Reality: ECT (Effective Connection Type)
We often talk about "mobile vs. desktop," but that's a false dichotomy. A desktop on a tethered 3G connection in a rural area is a "slow" device. A flagship phone on 5G mmWave is a "fast" device.
The ECT (Effective Connection Type) hint doesn't tell you the raw speed of the radio; it tells you the *measured* latency and bandwidth characteristics of the connection. It returns values like slow-2g, 2g, 3g, or 4g.
Why You Need This
If a user is on 3g, sending them a 4K hero image is a performance crime. You’re hogging their bandwidth and delaying the Time to Interactive (TTI).
Instead of using complex client-side "Network Information API" listeners that execute after the page loads, ECT lets you make the decision on the server, before the first byte of the image is even sent.
Practical Implementation
First, add ECT and Save-Data to your Accept-CH handshake.
Accept-CH: ECT, Save-DataThe Save-Data header is a simple on/off signal indicating if the user has enabled "Data Saver" mode in their browser. If that's on, you should probably ignore everything else and send the smallest assets possible.
Handling Image Optimization (Nginx Example):
You can use Nginx to intercept image requests and serve different qualities based on the network speed.
map $http_ect $image_suffix {
default "";
"3g" "_lowres";
"2g" "_lowres";
"slow-2g" "_lowres";
}
server {
location ~* \.(jpg|jpeg|png)$ {
# Check if a low-res version exists and the network is slow
try_files $uri$image_suffix $uri =404;
}
}In this setup, if a user requests hero.jpg and their ECT header is 3g, Nginx will check if hero_lowres.jpg exists and serve that instead. This happens at the infrastructure layer—zero impact on your application code.
---
3. The Specifics of the Silicon: Sec-CH-UA-Arch and Sec-CH-UA-Model
The standard User-Agent string might tell you "Windows NT 10.0; Win64; x64". But that doesn't tell you much about the actual capabilities of the hardware. Is it a high-performance workstation or a low-power ARM-based laptop?
With the rise of Apple Silicon (M1/M2/M3) and the increasing prevalence of ARM on Windows, architecture matters. Certain WASM modules or high-performance video codecs are optimized specifically for these architectures.
Precision Targeting
The Sec-CH-UA-Arch and Sec-CH-UA-Platform-Version headers provide the precision that the frozen UA string lacks.
For instance, did you know that Chrome on Windows 11 still reports "Windows NT 10.0" in the UA string for compatibility reasons? If you need to know if the user is actually on Windows 11 (perhaps to suggest a specific App Store download), you *must* use Client Hints.
Requesting High-Entropy Hints:
These are considered "high-entropy" because they could potentially be used to fingerprint a user. Therefore, the browser won't send them by default. You have to ask.
Accept-CH: Sec-CH-UA-Arch, Sec-CH-UA-Platform-Version, Sec-CH-UA-ModelThe Response:
Sec-CH-UA-Arch: "arm"
Sec-CH-UA-Platform-Version: "13.0.0"
Sec-CH-UA-Model: "MacBook Air"Use Case: Optimized Binary Delivery
If you are serving a heavy web-based tool (like a photo editor or a game), you might want to serve a WebAssembly (WASM) binary compiled specifically for ARM or x86 to squeeze out every bit of performance.
const arch = req.get('Sec-CH-UA-Arch');
if (arch === '"arm"') {
return res.sendFile('engine_arm.wasm');
} else {
return res.sendFile('engine_x86.wasm');
}---
The Catch: Critical Infrastructure and the "First Hit" Problem
There is a significant architectural "gotcha" with Client Hints: The first request never has the hints.
When a user types your URL into the browser and hits enter, the browser doesn't know you want Client Hints yet. It sends the first GET request with only the basic, low-entropy headers (like Sec-CH-UA: "Chromium"; v="122").
You only get the good stuff (like Device-Memory or ECT) on the *second* request and all subsequent sub-resource requests (images, scripts, CSS).
Strategy A: The Critical Restart
If your entire layout depends on knowing the device memory, you can force a "restart" of the request. If you see a request without the required hints, you send a response that tells the browser to try again immediately with the headers attached.
You do this using the Critical-CH header.
HTTP/1.1 200 OK
Accept-CH: Device-Memory
Critical-CH: Device-Memory
Vary: Device-MemoryWhen the browser sees Critical-CH, it realizes it should have sent Device-Memory. It will stop processing the current response and immediately re-request the page with the hint included. Use this sparingly; it adds a round-trip of latency. Only use it if the alternative is serving a broken or unusable page.
Strategy B: The Permissions-Policy
To ensure that sub-resources (like images hosted on a CDN) can also see these hints, you need to grant permission. Hints are not automatically sent to cross-origin requests for privacy reasons.
You can do this in your HTML via the Permissions-Policy header:
Permissions-Policy: ch-device-memory=(self "https://cdn.example.com"), ch-ect=(self "https://cdn.example.com")This tells the browser: "It's okay to send the memory and network hints to my CDN, too."
---
Moving Beyond the "Junk Drawer"
The User-Agent string was the junk drawer of the web—a place where we threw every bit of metadata until we couldn't find anything anymore. Structured Client Hints are the organizational system we’ve desperately needed.
By moving from a passive model (parsing a messy string) to an active model (requesting specific, structured data), we can build applications that are genuinely responsive. Not just responsive to screen size, but responsive to the actual constraints of the user's hardware and environment.
Start by auditing your server headers. Add Accept-CH: ECT, Device-Memory, Save-Data to your main document response. You don't even have to change your logic yet—just start looking at the data in your logs. I guarantee you’ll find that a significant portion of your "performance issues" are actually just cases where you were sending too much data to a device that never stood a chance of processing it.
The data is there. You just have to ask for it.


