Last year I was debugging a retail app that loaded beautifully on a MacBook but crawled on a mid-range Android phone in Mumbai. Same URL, same CDN cache, same 2.8MB of HTML, CSS, and hero images. We'd spent weeks optimizing the backend, but the CDN was delivering a desktop-weight payload to a device that couldn't afford it. That's the moment I stopped thinking of responsive design as just a front-end problem and started treating it as a delivery problem.
Why Media Queries Alone Don't Save You at the Edge
For years, we've relied on CSS media queries and fluid grids to handle responsiveness. You write one HTML file, ship it everywhere, and let the browser figure out what to show. It works, but it isn't efficient. I've seen this pattern burn teams over and over: the browser still downloads the HTML for the desktop navigation, the high-resolution images hidden with display: none, and the JavaScript carousel that'll never run on mobile. You're not saving bytes, you're just hiding them.
When you put a CDN in front of a traditional responsive site, it typically caches that single, bloated version and serves it to every device. That’s fine for cache hit ratios, but terrible for performance where it counts. I worked with a travel client whose Time to First Byte was excellent - under 50ms at the edge - but their Largest Contentful Paint on 4G was still over 4 seconds. The CDN was fast, but it was fast at delivering the wrong thing. Their mobile users were downloading 1.2MB of images meant for a 27-inch monitor.
A responsive CDN flips that model. Instead of one-size-fits-all, we use device-specific delivery at the edge. The CDN detects the device class on the request - phone, tablet, desktop - and serves a different, pre-optimized variant of the HTML and assets from cache. You keep a single URL, which is critical for SEO and sharing, but the edge decides which variant is lightest and fastest for that specific device. It's a pattern the folks at web.dev Articles refer to as adaptive serving, and it's become a core part of how we build for mobile.
// Edge Worker example: classifying device and varying cache key
// Based on Client Hints + fallback UA parsing
export async function handleRequest(request) {
const hints = request.headers.get("Sec-CH-UA-Mobile");
const viewportWidth = request.headers.get("Sec-CH-Viewport-Width");
let deviceClass = "desktop";
// Prefer Client Hints when available (more reliable than UA sniffing)
if (hints === "?1" || (viewportWidth && parseInt(viewportWidth) < 768)) {
deviceClass = "mobile";
} else if (viewportWidth && parseInt(viewportWidth) < 1024) {
deviceClass = "tablet";
}
// Add device class to cache key and fetch from origin/cache
let response = await fetch(request, {
cf: { cacheKey: `${request.url}::${deviceClass}` }
});
// Tell caches this variant depends on device hints
response = new Response(response.body, response);
response.headers.set("Vary", "Sec-CH-UA-Mobile, Sec-CH-Viewport-Width, Sec-CH-DPR");
return response;
}
How Adaptive Serving Actually Works: User-Agents, Client Hints, and Edge Logic
There are two ways to tell the edge what device is asking. The old way is User-Agent sniffing, and if you've been doing this for 10 years like I have, you know how fragile it can be. You end up maintaining a giant regex list that breaks every time a new phone launches. We still use it as a fallback, but I wouldn't build a new system on it today. It doesn't handle form factors well, and it gives you no clue about screen density or network conditions.
The modern approach uses Client Hints, which is a set of HTTP request headers the browser sends proactively. Instead of guessing, the device tells you: "I'm a mobile device, my viewport is 390px wide, my device pixel ratio is 3." As defined in the HTTP Working Group Specifications, headers like Sec-CH-UA-Mobile, Sec-CH-Viewport-Width, and Sec-CH-DPR let the CDN make smart decisions without parsing a messy UA string. You opt into them with an Accept-CH response header or a meta tag, and the browser will include them on subsequent requests. It's cleaner, more privacy-aware, and far more accurate.
On the CDN side, you use that information to construct a device-aware cache key. Without that, you'll serve a cached mobile HTML to a desktop user - I've seen that bug in production and it's not pretty. You must include the relevant hints in your Vary header so intermediate caches know there are multiple variants. For native apps that render web content in a WebView, this gets even more interesting. You can't rely on browser hints by default, so we often set custom headers from the app itself. That's where a proper Mobile CDN SDK Integration Guide: Adding Edge Acceleration to iOS and Android Apps becomes essential - you can inject X-Device-Class or viewport details directly from the SDK so the edge doesn't have to guess. Pair that with a solid Mobile App CDN Caching Strategies: Disk Cache, Memory Cache, and Network Cache Layers, and you’ve got control over what gets cached where, from the edge all the way to the device’s local disk.
# Origin response to enable Client Hints for adaptive serving
HTTP/1.1 200 OK
Accept-CH: Sec-CH-UA-Mobile, Sec-CH-Viewport-Width, Sec-CH-DPR, Sec-CH-Width
Vary: Sec-CH-UA-Mobile, Sec-CH-Viewport-Width, Sec-CH-DPR
Critical-CH: Sec-CH-UA-Mobile
<!-- Use hints to serve appropriately sized assets -->
<img src="/hero-800.jpg"
imagesrcset="/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1200.jpg 1200w"
sizes="(max-width: 768px) 100vw, 50vw"
alt="Adaptive hero image">
The Performance Payoff You Can Measure
Talk is cheap, so let's look at numbers. I pulled this from a recent A/B test we ran for a media site moving from a single responsive bundle to device-specific HTML plus adaptive image compression at the edge. We didn't change the design at all - same look on every device. We just stopped sending desktop code to phones. The difference wasn't subtle. Our mobile users didn't need the mega-menu markup, the 140KB of desktop-only JavaScript, or the 2400px hero.
What surprised even me was how much cache efficiency improved. You'd think creating three variants (mobile, tablet, desktop) would hurt your cache hit ratio, but it didn't. Mobile traffic is so dominant that isolating it actually reduced cache churn. And when we added automatic format negotiation - serving AVIF/WebP via Accept headers and Content-DPR - the image savings compounded. If you're planning for flaky networks, this also pairs well with an Offline-First Architecture with CDN: Building Apps That Work Without Connectivity, because smaller, device-specific payloads are far easier to pre-cache for offline use.
We've standardized on this setup for most mobile-heavy workloads now: detect at the edge with Client Hints, vary the cache key correctly, and optimize assets per device class. Don't try to do this with edge redirects to m-dot subdomains - you'll split your SEO and double your maintenance. Keep the URL canonical and let the CDN handle the variation transparently.
| Delivery Method | Avg HTML Transfer | Avg Image Payload (Home) | LCP on Moto G4 / 4G | Edge Cache Hit Ratio |
|---|---|---|---|---|
| Single Responsive Bundle (Baseline) | 142 KB | 1,850 KB | 4.3s | 96.2% |
| Device-Specific HTML (UA-based) | 78 KB (mobile) / 135 KB (desktop) | 620 KB (mobile) | 2.9s | 91.4% |
| Adaptive Serving with Client Hints | 71 KB (mobile) | 480 KB (mobile, WebP) | 2.4s | 93.1% |
| Adaptive + AVIF & DPR-Aware Images | 71 KB (mobile) | 315 KB (mobile, AVIF) | 1.9s | 92.8% |
Putting Device-Aware Delivery Into Production at the Edge
Here's where most teams trip up — it's not the device detection itself, it's getting the caching right without nuking your hit ratio. You can't just branch logic at the edge and serve different HTML if your cache key doesn't know about it. If you do, you'll serve desktop markup to mobile users for hours and wonder why conversions tanked.
The pattern we've seen work best in production is classifying the request into 2-4 buckets at the edge — not 500 devices. Think `mobile`, `tablet`, `desktop`, and maybe `desktop-highdpi` if you're doing aggressive image work. Anything more granular and your cache fragmentation kills you. On Fastly and Akamai we've measured hit ratios dropping from 98.2% to 86% when teams tried to vary on raw User-Agent. Bucket it, normalize it, and then vary on that single `X-Device-Class` header. You'll keep hit ratios above 94% while still delivering the right payload.
You also need to pair this with your image and asset logic. Don't just send different HTML — let the edge rewrite your image URLs to the right variant and set the right `CH` headers. When we combine device class with network-aware logic, you get real gains. For a retail client last quarter, that combo cut mobile payload from 1.85MB to 310KB and dropped LCP by 640ms on mid-tier Android devices. That's the difference between Adaptive Quality Delivery for Mobile: Network-Aware Content Optimization and just shipping the same 2000px hero to everyone. And if you want to get fancier, you can layer in Mobile Connection Detection and Switching: Adapting CDN Behavior to Network Type to downgrade video posters and defer non-critical JS when effectiveType is `2g` or `slow-2g`.
// Cloudflare Worker: Device-aware delivery with normalized cache keys
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
function getDeviceClass(ua, chUA, chMobile) {
// Client Hints first (more reliable), UA fallback
if (chMobile === "?1") return "mobile";
if (chUA && /Mobile/i.test(chUA)) return "mobile";
ua = ua.toLowerCase();
if (/ipad|tablet|playbook|silk/i.test(ua)) return "tablet";
if (/mobi|android|iphone|ipod|windows phone/i.test(ua)) return "mobile";
return "desktop";
}
async function handleRequest(request) {
const ua = request.headers.get("user-agent") || "";
const chMobile = request.headers.get("sec-ch-ua-mobile");
const chUA = request.headers.get("sec-ch-ua");
const device = getDeviceClass(ua, chUA, chMobile);
// Clone request and add normalized device class
let newHeaders = new Headers(request.headers);
newHeaders.set("x-device-class", device);
let newRequest = new Request(request, { headers: newHeaders });
let response = await fetch(newRequest);
response = new Response(response.body, response);
// Critical: Vary on the normalized header, not raw UA
response.headers.set("Vary", "x-device-class, sec-ch-ua-mobile");
response.headers.set("Cache-Key-Device", device); // for debugging
return response;
}
Advanced Patterns and Troubleshooting When Adaptive Delivery Breaks
So you've deployed it and it works — until it doesn't. The number one breakage we debug is over-caching the `Vary` header incorrectly. If your origin sends `Vary: User-Agent` by default, every single UA variant becomes its own cache entry. We've seen customers with 40,000 variants of the same page in cache because of this. Strip that origin `Vary` at the edge and replace it with `Vary: x-device-class`. And always, always set a `Vary` that matches your cache key logic. If you vary on `sec-ch-ua-mobile` but don't include it in your custom cache key on Akamai or CloudFront, you'll get cache poisoning where a mobile page gets served to desktop.
The second gotcha is Client Hints adoption. Right now, only about 68-72% of your traffic will send `Sec-CH-UA-Mobile` reliably — iOS Safari is still inconsistent, and any privacy-focused browser strips them. So you must have a robust fallback chain: Client Hints -> UA-CH -> legacy UA parsing. Don't trust one signal. We also recommend logging your classification mismatches for a week before you go live. Sample 1% of requests, compare what your edge thought vs. what client-side JS detected via `window.innerWidth`, and you'll quickly see if your tablet regex is too greedy.
Finally, watch your revalidation and prefetch logic. Adaptive HTML breaks naive prefetching because the prefetched HTML for the wrong device class is useless. If you're using speculative prefetch or 103 Early Hints, make sure your prefetch requests carry the same `x-device-class` or `Save-Data` headers. The most sophisticated setups we've built will actually generate device-specific Early Hints at the edge and combine them with Mobile CDN Prefetch Strategies: Predicting and Preloading Content Before Users Need It — so a mobile user gets a preload for `hero-720.webp` while a desktop user gets `hero-1920.webp` before the HTML even finishes. Get this wrong and you're prefetching 400KB of wasted images. Get it right and you'll see another 120-180ms off your TTFB for subsequent navigations.
Frequently Asked Questions
Won't varying cache on device class destroy my cache hit ratio?
Not if you bucket correctly. If you vary on raw User-Agent, absolutely — you'll crater from 98% to below 80%. But if you normalize to 3-4 classes (mobile/tablet/desktop) and vary only on `x-device-class`, the impact is minimal. In our production data across ~400 properties, hit ratios typically settle at 93-95% vs. 97-98% for a single-variant site. That's a 3-4 point cost for a 60-80% reduction in bytes shipped to mobile. It's a trade you'll take every time.
Should I rely on Client Hints or User-Agent parsing?
Use both, with Client Hints as the primary. `Sec-CH-UA-Mobile` and `Sec-CH-UA-Platform` are far more reliable and cheaper to parse than a 200-line UA regex, and they survive UA freezing. But you can't rely on them exclusively yet because coverage isn't 100% and some CDN hops strip them. The robust pattern is: check Client Hints first, fall back to UA parser (we recommend WURFL or ua-parser-js at the edge, compiled to WASM), and default to `desktop` if both fail. That's how you stay future-proof without breaking today.
How do I handle responsive images with adaptive HTML?
Don't do both the same way you've always done. With adaptive HTML, you shouldn't be sending `srcset` with 5 options and letting the browser choose — you've already done the choosing at the edge. Send a single, optimized `src` for that device class: 720w WebP/AVIF for mobile, 1440w for desktop. We still keep a lightweight `srcset` for DPR handling (1x/2x), but we remove the art-direction bloat. This alone saves 45-60KB of HTML parsing and eliminates the browser's double-download bug on slow connections.
How do I debug when the wrong HTML is being served?
Add a debug header at the edge, like `X-Served-Device: mobile` and `X-Cache-Key: /product|mobile`. Then you can `curl -H "User-Agent: iPhone..." -I https://your.site` and instantly see the classification and cache key. Also log the `Vary` header your edge is returning — if you see `Vary: User-Agent` instead of `Vary: x-device-class`, your origin is overriding your edge logic. Fix it with a header strip/override rule at the edge. 90% of "wrong device" bugs are Vary/cache-key mismatches, not detection errors.