Last spring I watched a retail client demo their persistent AR storefront in downtown Seattle. A designer had placed a virtual sculpture on the sidewalk that morning, and by afternoon thirty different phones could still see it — locked in place, occluded correctly behind a real park bench, without re-scanning the street. That illusion didn't come from the app. It came from the delivery layer moving spatial maps, meshes, and anchors to the edge faster than the devices could drift.
Why Static Asset Caching Fails Persistent Spatial Maps
Traditional CDN logic assumes assets are files with URLs that don't change until you purge them. An AR cloud is nothing like that. You're not caching a JPEG or even a 3D model — you're caching a continuously refined understanding of physical space: sparse point clouds, feature descriptors, dense meshes, and the semantic anchors tied to them. That data is volumetric, viewpoint-dependent, and it mutates every time another device walks through the scene and contributes observations. If you've ever tried to slap a 24-hour TTL on a spatial map, you've seen what happens: users start localizing against stale geometry and holograms swim two meters to the left.
I ran into this exact problem back in 2021 when we tried to shoehorn an indoor navigation prototype onto a standard pull-through CDN. We stored world maps as monolithic blobs in S3 and cached them at the edge like video segments. Cache hit rates looked great on the dashboard, but localization success dropped to 46% after a few hours because chairs had moved and lighting changed. We'd optimized for byte delivery, not spatial freshness. What we needed wasn't a bigger cache, it was a cache that understood space. That shift is central to any serious CDN for Spatial Computing: Delivering AR/VR Assets with Sub-Frame Latency architecture, where the cache key isn't just a URL but a 6DoF pose and a confidence score.
The fix is to tile and version space itself. Instead of caching one map per room, you shard the environment into an octree or sparse voxel grid and address each tile independently. Each tile gets its own ETag derived from its geometric hash and observation timestamp, so edge nodes can revalidate a single 10cm cube without evicting the entire floor. You also need to separate immutable data (like learned feature descriptors that rarely change) from highly volatile data (like dense depth updates). The former can live at the edge for hours, the latter might only survive for seconds before a delta patch replaces it. This aligns with modern HTTP caching semantics described in the HTTP Working Group Specifications, particularly around revalidation and stale-while-revalidate.
// Edge worker: spatial-aware cache key for AR cloud tiles
// Runs on Cloudflare Workers / Fastly Compute@Edge
export default {
async fetch(request, env) {
const url = new URL(request.url);
const { x, y, z, lod, anchorId } = parseSpatialParams(url);
// Quantize world coordinates to tile grid (e.g., 0.5m voxels at LOD 3)
const tileKey = `ar-tile:${anchorId}:${lod}:${quantize(x, 0.5)}:${quantize(y, 0.5)}:${quantize(z, 0.5)}`;
const cache = caches.default;
// Check edge cache using geometric hash as validator
let response = await cache.match(tileKey);
if (response) {
const geomHash = response.headers.get('X-Geom-Hash');
// Revalidate with origin if tile is older than its volatility window
const age = Date.now() - parseInt(response.headers.get('X-Observed-At'), 10);
if (age > getTTLForLOD(lod)) {
const reval = await fetch(origin + `?tile=${tileKey}`, {
headers: { 'If-None-Match': `"${geomHash}"` }
});
if (reval.status !== 304) {
response = reval;
response.headers.set('Cache-Control', `public, max-age=${getTTLForLOD(lod)}, stale-while-revalidate=5`);
await cache.put(tileKey, response.clone());
}
}
return response;
}
return fetchAndCacheTile(tileKey, cache);
}
}
function getTTLForLOD(lod) {
// High LOD = fine geometry, more volatile. Low LOD = coarse structure, stable.
return lod >= 4 ? 5 : lod === 3 ? 30 : 300; // seconds
}
Building an Edge Network for Sub-20ms Localization and Anchor Resolution
Persistent AR lives or dies on localization latency. When a user raises their phone, the device extracts visual features and must resolve its pose against the AR cloud in under 20ms, otherwise content pops in late and breaks presence. You can't round-trip to us-east-1 for that. You need edge PoPs that can answer pose queries locally, which means pushing not just cached tiles but lightweight localization services to the edge. We've had the best results co-locating a visual positioning service (VPS) alongside the cache, so the edge can match query features against its resident map tiles without ever hitting origin.
This is where the request flow starts to look different from typical web delivery. The client isn't doing a simple GET for a known URL; it's doing a POST with a compressed feature descriptor frame, and the edge responds with a pose correction plus the set of content anchors visible from that pose. To keep that exchange fast, we're now using HTTP/3 and QUIC for 0-RTT resumption and better loss recovery on mobile networks, and we prioritize these localization requests over bulk asset downloads using priority signaling. The web.dev Articles on fetch priority and early hints were surprisingly useful when tuning this, even though they were written for the web — the same principles apply when you're streaming spatial content. For teams already working with immersive browsers, this pattern extends naturally from WebXR Content Delivery Through CDN: Streaming Immersive Experiences at Low Latency where frame timing is equally strict.
You also have to think about write path fan-out. Every device that localizes successfully becomes a mapper, uploading delta observations that refine the cloud. If you send all those deltas to a central origin, you create a bottleneck and stale reads everywhere else. Instead, we use an edge ingest tier that aggregates deltas, deduplicates observations using geometric hashing, and then gossips only meaningful changes to neighboring PoPs via CRDTs or versioned patches. Clients subscribed to an area get server-sent events or WebTransport streams with tile patches, so persistent content can shift subtly — for instance, when someone moves that park bench — without requiring a full map reload.
# Client: fetching persistent anchors for a localized pose
# Uses HTTP/3, cache-aware headers, and delta patches
POST /v1/arcloud/localize HTTP/3
Host: ar-edge.example.net
Content-Type: application/octet-stream
X-AR-Session: 6f3a9c2e-1b4d-4a8e-9f01-2e7c8a9b0c1d
X-Device-Pose-Hint: 47.6062,-122.3321,15.0
[binary descriptor frame: ~45KB quantized ORB features]
# Edge response: pose + visible anchors + cache directives
HTTP/3 200 OK
Content-Type: application/json
Cache-Control: private, max-age=0 # pose is never cached
X-Localization-Latency: 11ms
X-Served-By: sea-04-vps
{
"pose": { "position": [47.60621, -122.33208, 1.42], "rotation": [...] },
"confidence": 0.92,
"visible_anchors": [
{
"id": "anchor_sculpture_01",
"url": "https://cdn.example.net/ar/anchors/sculpture_01.glb",
"cache_control": "public, max-age=3600, stale-while-revalidate=86400, immutable",
"tile_hash": "a3f9c1e8"
}
],
"tile_patch_subscription": "wss://ar-edge.example.net/patches/sea-tile-9f3a"
}
Balancing Persistence, Bandwidth, and Freshness on an Augmented Reality CDN
There's no single TTL that works for spatial data. The lobby of an office building at 9 AM changes constantly as people walk through, while the structural shell of that same building hasn't changed in years. If you cache too aggressively, holograms float through relocated furniture. If you revalidate too often, you burn through your user's data plan and introduce jitter. The pragmatic approach I've settled on is tiered volatility: coarse LOD tiles (building shape) cache for minutes to hours, medium LOD (furniture, wall fixtures) for 30-60 seconds, and fine LOD depth deltas stream in near-real-time with 2-5 second patch intervals. That stratification lets you hit high cache ratios without sacrificing the persistence illusion.
We also learned to stop treating persistence as binary. For many clients, eventual consistency is fine — it's okay if a new device sees a virtual poster 3 seconds after it was placed — but geometric consistency is not negotiable. A rendering fix that looks great on one device can't appear to intersect a wall on another. That's why we separate the delivery of WebGPU Edge Rendering: Offloading 3D Computation to CDN Infrastructure assets (which can be cached immutably with long-lived hashes) from the spatial anchors that position them (which need short, revalidatable lifetimes). The 3D model might be 12MB and live on the edge for weeks, while its anchor transform is 200 bytes and revalidates every few seconds.
The table below shows numbers from a recent production rollout for a 5,000 sqm retail deployment with about 800 daily active AR users. All traffic was served over HTTP/3 from edge nodes within 30km of users, with tiles at 0.5m resolution. These aren't lab benchmarks — they include real network variance, device thermal throttling, and people actually moving props around the store.
| Caching Strategy | Edge Hit Ratio | Median Pose Latency (p50) | Downlink per Session (5 min) | Anchor Drift after 24h |
|---|---|---|---|---|
| Monolithic Map (no tiling) | 42% | 187 ms | 84.3 MB | 148 cm |
| Tiled + Long TTL (300s) | 91% | 34 ms | 22.1 MB | 18.4 cm |
| LOD-Tiered + Delta Patches | 88% | 14 ms | 11.7 MB | 2.1 cm |
| Tiered + Edge VPS Co-location | 89% | 9 ms | 12.4 MB | 1.8 cm |
Practical Implementation: Building an Anchor-Aware Edge Tier
Let's get out of theory and into what actually ships. You can't just throw your spatial maps and anchor payloads onto a standard CDN origin and hope for the best. We tried that early on — treating a 4MB spatial tile like a JPEG — and watched our p95 localization time blow out to 180ms because every request had to go back to us-east-1 to resolve the anchor graph. For persistent AR, your edge needs to be spatially aware, not just URL-aware.
The pattern that works is a two-tier cache keyed on geohash + anchor_id + map_version. Your client doesn't request /assets/anchor-123.bin; it requests /ar/v1/localize?geohash=u4pruyd&anchor_set=3f9c... and the edge worker does the fan-out. You’ll want to store dense point cloud tiles (typically 3-8MB after Draco compression) at the edge for 24-48 hours, but keep the lightweight anchor descriptors — that JSON with pose, covariance, and feature hashes — on a much shorter TTL of 60-90 seconds. That way you get fast hits for the heavy geometry but don't serve stale poses when another user just moved that persistent object. We've seen this cut origin fetch by 82% while keeping pose freshness under 2 seconds.
This is a lot like how we handle Digital Twin Data Distribution Through CDN: Real-Time Synchronization of Physical Models — you're not just caching a file, you're synchronizing state between physical space and the edge. Set your origin to push invalidations via surrogate keys when an anchor is updated, don't rely on TTL alone. If a user in a mall moves a virtual sculpture, you need to purge surrogate-key: geohash-u4pruyd globally in under 500ms, not wait for it to expire.
// Cloudflare Worker / Fastly Compute@Edge - AR Anchor Resolver
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const geohash = url.searchParams.get('geohash').substring(0, 7); // ~150m tile
const anchorVersion = request.headers.get('X-AR-Map-Version') || 'latest';
// 1. Try edge KV/cache for descriptor (short TTL)
const cacheKey = `ar:${geohash}:${anchorVersion}`;
let descriptor = await env.AR_CACHE.get(cacheKey, { type: 'json' });
if (!descriptor) {
// 2. Stale-while-revalidate to origin anchor service
descriptor = await fetch(`https://origin.ar-cloud.internal/resolve?geohash=${geohash}`, {
cf: { cacheTtl: 60, cacheEverything: false }
}).then(r => r.json());
// Cache descriptor 60s, tile geometry 24h via surrogate keys
ctx.waitUntil(env.AR_CACHE.put(cacheKey, JSON.stringify(descriptor), {
expirationTtl: 60,
metadata: { surrogateKeys: [`geo-${geohash}`, `v-${descriptor.version}`] }
}));
}
// 3. Attach edge location and return with 15ms budget
return new Response(JSON.stringify(descriptor), {
headers: {
'Content-Type': 'application/json',
'Cache-Status': descriptor ? 'HIT' : 'MISS',
'X-AR-Edge-Latency': `${Date.now() - start}ms`
}
});
}
}
Advanced Patterns: Drift Correction, Occlusion Meshes, and Cold Starts
Once you're past the basic cache, you'll hit the nasty AR-specific problems that don't show up in web CDN metrics. Number one is drift. Your visual positioning system will accumulate 1-3cm of drift per minute if you're serving a stale mesh, and users *will* notice when their persistent notes float off the wall. Don't cache your localization correction layers the same way you cache static geometry. We run a separate, uncached gRPC/WebSocket path through Argo / Tiered Cache that bypasses cache entirely for the live /correct endpoint, keeping that round-trip between 12-18ms at the 95th percentile. The trick is to let the edge terminate TLS and keep a warm connection to origin, you're not saving bandwidth here, you're saving milliseconds.
Cold starts are the other killer. When a user walks into a spatial tile that hasn't been requested in hours, you don't want them to wait 600ms while you pull 25MB of occlusion meshes from S3. We pre-warm the edge based on movement prediction — if your app knows the user's trajectory, prefetch the next 3 geohashes at low priority using Link: </ar/tiles/u4pruyde.mesh>; rel=prefetch. It costs you maybe 15-20% more egress, but it drops cold-start localization from ~400ms to under 45ms. This predictive distribution model is very similar to what we use for Connected Vehicle Update Delivery: CDN Infrastructure for Automotive OTA, where you can't afford to miss a tile when the car enters a new region.
Finally, watch your bandwidth vs. freshness tradeoff for shared experiences. If you've got 30 users in the same co-located AR session, don't have all 30 polling the edge for anchor updates. Use the edge to fan out via WebSockets or Server-Sent Events. One origin publish, 30 edge pushes. We've also borrowed heavily from IoT Firmware Update Distribution via CDN: Secure OTA Updates for Device Fleets here — signing every anchor payload with Ed25519 and validating at the edge worker before caching. If you cache a poisoned mesh, you've broken persistence for everyone in that geohash until the TTL expires. Don't do it.
Frequently Asked Questions
How is AR Cloud delivery different from standard static asset caching?
Static assets are immutable and URL-addressable. AR Cloud data is spatially-addressable, mutable, and has two freshness requirements: the heavy geometry (meshes, point clouds) can be cached for hours, but the pose graph and anchor locks need 60-90 second freshness. Standard LRU caching with long TTLs will serve stale poses, causing drift and visual jitter. You need surrogate-key purging and a split TTL strategy.
What latency should I target for persistent localization?
For it to feel truly persistent, you need sub-20ms edge latency for the descriptor fetch and under 50ms end-to-end for a full 6DoF relocalization. That means your edge PoP must be within ~400km of the user. If your p50 is 25ms but your p95 is 120ms, users will feel anchors "swim" on relocalization. Monitor p95 and p99 by geohash, not just globally.
How large are spatial tiles and how do I control bandwidth costs?
A dense indoor tile (10x10m) with feature points and a compressed occlusion mesh is 3-8MB; an outdoor city tile can be 15-30MB. Don't serve full tiles on every request. Use delta updates — send only the changed anchors since the client's last map_version — and apply Brotli or Draco compression. Most clients only need a 1-2KB descriptor update 90% of the time, which keeps average session bandwidth under 40MB per hour.
How do you handle persistence when the physical space changes?
Persistence isn't forever. You need a confidence decay. When our mapping service detects construction or layout changes (via low feature match scores <0.3), we automatically deprecate anchors in that geohash and purge the surrogate key. The client then falls back to creating a new anchor rather than snapping to a stale one. It's better to lose persistence than to render content on the wrong wall.