The State of Caching and Interoperability Between Application Types
Table of Contents
1. Summary
The state of caching and interoperability between application types as of 2016. Three patterns dominate asset naming on CDNs: version-pinned paths, content-hash suffixes, and rolling build identifiers.
1.1. Naming conventions
Observable from HTTP Archive trends data:
1.1.1. CDN: version-pinned path
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
The version (3.3.5) is part of the path. Cache-friendly: the URL
never changes for a given version. Drawback: no automatic invalidation
when a patch is released.
1.1.2. Yahoo: content-hash suffix
css/bundle.c60a6d54.css
The hash (c60a6d54) is derived from the file contents. Any change to
the CSS produces a new hash, a new URL, and a guaranteed cache miss.
1.1.3. Microsoft: rolling build identifier
https://assets.onestore.ms/cdnfiles/onestorerolling-1607-15000/shell/v3/scss/shell.min.css
The 1607-15000 encodes the build (Windows 10 Anniversary Update era).
Hybrid: the build number changes with each release cycle, but the path
is not content-addressed.
2. Tools (2016 baseline)
2.1. Rails Asset Pipeline (Sprockets)
Rails Asset Pipeline Guide — Sprockets compiles, concatenates, and
fingerprints assets. The application-<hash>.css naming convention
introduced fingerprinting to the Rails ecosystem.
2.2. Webpack
Webpack Caching Guide — content-hash output filenames
([name].[contenthash].js). Rails integration arrived later, via the
webpacker gem (Rails 5.1, 2017), and has since been retired.
3. Since then
The content hash won. Every mainstream build tool and framework emits
name.<hash>.ext by default, and the version-pinned public CDN lost
its main argument when browsers partitioned their caches. What held up
from 2016 is the underlying rule: put the version in the URL, cache the
URL forever, and spend revalidation only on the HTML that points at it.
What changed is everything around that rule — the HTTP spec was
reissued, the shared cross-site cache disappeared, AppCache gave way to
service workers, CDNs became programmable, and a new cache layer
appeared in front of LLM inference.
3.1. HTTP caching: RFC 9111
RFC 9111 (STD 98, June 2022) obsoletes RFC 7234; Appendix B obsoletes Warning, Section 5.4 deprecates Pragma, adds must-understand
RFC 9111 (June 2022, STD 98) replaced RFC 7234 as the definition of
HTTP caching. The model is the same — freshness from max-age /
s-maxage / Expires, validation with ETag and Last-Modified —
but the cleanup is real: the Warning header is obsoleted (caches
never set it in practice), Pragma is deprecated, the must-understand
directive is new, and duplicate or conflicting directives now have
defined handling.
The directives that matter for fingerprinted assets came from separate RFCs:
| Header / directive | Source | Use |
|---|---|---|
Cache-Control: immutable |
RFC 8246 (Sept 2017) | Fingerprinted assets: skip revalidation even on reload. |
stale-while-revalidate, stale-if-error |
RFC 5861 (May 2010) | Serve stale while a background refresh runs; serve stale when the origin errors. |
CDN-Cache-Control |
RFC 9213 (June 2022) | CDN-targeted TTL. A CDN that honours it ignores Cache-Control and Expires for its own storage. |
Vary: Accept-Encoding |
RFC 9110 / 9111 | Adds the request encoding to the cache key; CDNs normalise the value so gzip/br variants don't fragment the cache. |
The state machine these directives drive — fresh, stale, revalidation, the SWR side branch — is drawn in HTML5 AppCache Expiration Rules.
3.2. Browser cache partitioning
Chrome 86 key is (top-level site, current-frame site, URL) per developer.chrome.com; dropped the unsupported "ITP 2.1 / Safari 12.1" attribution
Safari partitioned its HTTP cache first (around 2013). Chrome 86
(October 2020) shipped cache partitioning, keying each entry on the
top-level site, the current-frame site, and the resource URL; Firefox
85 (January 2021) followed with network partitioning. A library loaded
from cdnjs.cloudflare.com on site A now occupies a different cache
slot than the same URL on site B.
That ended the shared-CDN argument from 2016. The extra DNS lookup and TLS handshake to a third-party origin no longer buys a cross-site cache hit. For fonts, self-hosting is now strictly better (Wicki, Nov 2020); for large libraries it depends on CDN geography, but "everyone already has it cached" is gone.
3.3. Service workers replaced AppCache
In 2016 the offline cache was still AppCache, already deprecated and widely disliked. Browsers removed it between Chrome 85 (2020) and Safari 15.4 (2022). Service workers plus the Cache API took over, with the strategies — cache-first, network-first, stale-while-revalidate — written in JavaScript rather than declared in a manifest. The details, and the AppCache flaws that forced the change, are in HTML5 AppCache Expiration Rules. The same fresh/stale/revalidate vocabulary later moved into client data libraries (SWR, TanStack Query).
3.4. This site's own headers
wal.sh is a small worked example of the 2016 problem persisting. The
host defaulted to long max-age values (30 days on some assets, 48 h on
search-index.json), which hid every ClojureScript bundle deploy from
returning visitors. The site's .htaccess template now sets
Cache-Control: max-age=86400, must-revalidate on .js and .json. The
same audit found the ETag inert: mod_deflate appends -gzip on the
way out and then fails to match If-None-Match on the way back, so a
conditional GET returns 200, not 304. Last-Modified is the only
working validator. Content-hashed filenames would make all of this
moot; the bundles are not fingerprinted yet.
3.5. Rails asset pipeline lineage
| Years | Era | Tool | Pattern |
|---|---|---|---|
| 2011–2022 | Rails 3.1–6.1 | Sprockets | application-<digest>.css |
| 2017–2022 | Rails 5.1–7.0 | Webpacker (retired) | Webpack bundles |
| 2021-- | Rails 7+ | importmap-rails | No bundler; native ES modules |
| 2022-- | Rails 7+ | Propshaft | Simpler fingerprinting, no compilation |
| 2024-- | Rails 8 | Solid Cache | Database-backed fragment caching |
Turbo Drive (Hotwire) replaced Turbolinks: navigation fetches HTML and
swaps the <body> rather than doing a full page load, and Turbo 8 added
DOM morphing for page refreshes. Either way, ETag / 304 Not Modified
on the HTML matters more for navigation than asset fingerprinting does.
3.6. Edge computing
CDN edge functions move computation to the cache layer. The cache is no longer passive storage — it can transform responses, personalize content, and enforce access control without a round-trip to the origin.
3.6.1. Major platforms
Workers paid CPU is 30 s default / 5 min max; CloudFront Functions is ES 5.1 plus ES6-12 features (runtime 2.0); Lambda@Edge 30 s timeout matches AWS quotas page; "six platforms" was seven
Seven platforms define the landscape. They split on runtime: V8 isolates (near-zero cold start, JavaScript/Wasm only) versus containers or Lambda processes (flexible runtime, second-scale cold start).
| Platform | Announced / GA | Runtime | CPU limit | Memory |
|---|---|---|---|---|
| Cloudflare Workers | Sep 2017 / Mar 2018 | V8 isolates | 10 ms free; paid 30 s default, 5 min max | 128 MB per isolate |
| Vercel Edge Functions | Dec 2022 GA | V8 isolates | 50 ms CPU / invocation | not published |
| Netlify Edge Functions | 2022 | Deno (V8 + Rust) | 50 ms CPU | 512 MB per deploy |
| AWS CloudFront Functions | 2021 | JS: ES 5.1 plus selected ES6–12 (runtime 2.0) | submillisecond | 2 MB |
| AWS Lambda@Edge | 2017 | Node.js / Python | 30 s | Lambda limits (128 MB–10 GB) |
| Fastly Compute (was Compute@Edge) | 2019 beta | WebAssembly (WASI) | billed per vCPU-ms | 128 MB min |
| Deno Deploy | 2021 | V8 isolates (Deno runtime) | not published | 512 MB |
Cold-start comparison: CloudFront Functions and Cloudflare Workers start in under a millisecond because isolates share a warm V8 process. Fastly Compute instantiates a fresh Wasm module per request in tens of microseconds. Lambda@Edge requires container provisioning and can take seconds on a cold path.
Cloudflare Workers pricing: free tier 100,000 req/day, 10 ms CPU. Paid ($5/month minimum): 10 M req/month included then $0.30/million; 30 M CPU-ms included then $0.02/million. CloudFront Functions: $0.10/million invocations.
3.6.2. Computation at the edge changes the caching model
Traditional CDN caching is passive: store the response, serve the stored copy, expire on TTL. Edge functions make caching active.
A/B testing. The worker reads a cookie to determine the user's cohort,
rewrites the request path to /test or /control, and fetches from
the corresponding cached variant. No origin round-trip for the routing
decision. New visitors are randomly assigned and the cohort cookie is
set on the response.
Geolocation-based content. Cloudflare injects CF-IPCountry and
Vercel injects X-Vercel-IP-Country into every request. A worker can
serve a different cached variant per country using
Vary: X-Vercel-IP-Country. Vercel's CDN respects this and stores
separate cache entries per country value.
Authentication at the edge. JWT validation (HMAC or RS256 via
SubtleCrypto) runs in the worker before the request reaches the
origin. CloudFront Functions can inspect Authorization headers and
return a 403 in submillisecond time; the origin is never hit for
unauthorized requests.
Response transformation. Workers rewrite HTML (inject banners, swap
canonical URLs, modify <head>), inject security headers
(Content-Security-Policy, Strict-Transport-Security), or transform
JSON before it is placed in the CDN cache. The cache stores the
transformed response, not the raw origin response.
Stale-while-revalidate in the worker. On a stale hit, the worker
calls waitUntil() to revalidate the origin in the background after
returning the stale copy. Vercel natively honours
s-maxage=N, stale-while-revalidate=Z in Cache-Control headers from
functions. The Vercel-CDN-Cache-Control header allows different TTLs
for the Vercel edge, downstream CDNs, and the browser independently —
a vendor-specific instance of the RFC 9213 pattern.
3.6.3. Key technical constraints
These constraints are common across V8-isolate platforms and shape what edge functions can and cannot do.
| Constraint | Detail |
|---|---|
| No filesystem | No server filesystem access. Assets must be bundled or fetched from KV/R2/D1. |
| No native modules | Wasm available; Node.js .node addons are not. |
| CPU time cap | Tens of milliseconds on free tiers covers routing, header manipulation, and SubtleCrypto operations. Not enough for image transcoding or ML inference without Wasm. |
| Memory cap | 128–512 MB. In-memory state is per-isolate and does not persist reliably across requests. |
| Startup budget | On Cloudflare Workers, global scope must complete within 1 second. |
Date.now() does not advance |
On Cloudflare, Date.now() returns the time of the last I/O event. Timing-sensitive logic must account for this. |
No eval() |
Dynamic code evaluation blocked on all V8-isolate runtimes. |
| Request body access | CloudFront Functions cannot read the request body. Lambda@Edge can. |
These constraints explain the CloudFront split: CloudFront Functions for header and URL manipulation, Lambda@Edge for network calls, file access, or heavier compute.
3.6.4. The edge database trend
D1 free tier is 5 M rows read/day and 100 K written/day; 25 B/50 M per month are the paid inclusions. libSQL is a C fork of SQLite, not a Rust rewrite
Edge functions became substantially more capable when database primitives moved to the same network tier.
| Product | Type | Launched | Notes |
|---|---|---|---|
| Cloudflare KV | Key-value | 2018 beta | Eventually consistent; global replication. Free: 100K reads/day, 1K writes/day, 1 GB. Paid: 10 M reads/month, $0.50/million additional. |
| Cloudflare D1 | SQLite SQL | Sep 2023 open beta | Strong consistency per database. Free: 5 M rows read/day, 100 K rows written/day, 5 GB. Paid includes 25 B rows read and 50 M rows written per month. |
| Cloudflare R2 | Object storage | 2022 | S3-compatible; no egress fees. $0.015/GB-month. |
| Vercel KV | Redis-compatible | 2023; discontinued Dec 2024 | Migrated to Upstash. New projects use Marketplace Redis integrations. |
| Turso (libSQL) | SQLite fork | 2023 | libSQL is an open-contribution fork of SQLite's C code base, adding replication and per-tenant databases. Turso's separate Rust rewrite of SQLite is a later project. |
| Neon | serverless Postgres | 2022 | Scale-to-zero compute, built-in pooling. Database branching, point-in-time restore. |
The KV / SQL split reflects two use cases. KV (Cloudflare KV, Upstash Redis) stores session tokens, feature flags, and rate-limit counters — small values, high read volume, eventual consistency acceptable. SQL at the edge (D1, Turso, Neon) stores relational data close to compute.
Cloudflare KV is eventually consistent: writes propagate to all edge nodes within roughly 60 seconds. D1 offers strong consistency within a single database; global read replicas trade recency for read throughput.
3.6.5. Solid Cache and the Rails 8 connection
Rails 8 (2024) ships Solid Cache as the default fragment cache store,
replacing the Redis/Memcached pattern dominant since Rails 4. Solid
Cache stores fragments in a SQLite3 database on SSD
(storage/production_cache.sqlite3 by default), using FIFO eviction.
Configuration: max_age, max_size (e.g., 256 MB), encrypt,
databases (sharding across multiple SQLite files).
| Solid Cache (Rails 8) | Cloudflare KV | Cloudflare D1 | |
|---|---|---|---|
| Storage medium | SQLite on SSD | Distributed key-value store | SQLite (edge-replicated) |
| Consistency | Strong (single DB) | Eventual (~60 s propagation) | Strong per DB; eventual for replicas |
| Location | Origin server | 300+ edge PoPs globally | 300+ edge PoPs globally |
| Query model | SQL (ActiveRecord) | key.get / key.put | Full SQL |
| Eviction | FIFO, configurable max_size |
TTL-based | Manual or TTL |
| Encryption | Optional (encrypt: true) |
Not native | Not native |
| Rails integration | Native (ActiveSupport::Cache) |
Via HTTP API or gem | Via HTTP API or gem |
Solid Cache is appropriate when the cache lives on the same machine as the Rails process — a local SQLite read is sub-millisecond. Cloudflare KV or D1 is appropriate when the cache must be served from an edge location decoupled from any origin process. The patterns are complementary: a Rails app can use Solid Cache for server-side fragment caching and Cloudflare KV for session tokens served at the edge.
3.7. Prompt caching: the newest layer
Anthropic docs: prefix-based (tools, system, messages), 5 min default / 1 h TTL, writes 1.25x / 2x, reads 0.1x base (0.05x on Opus 5.5), up to 4 breakpoints, 512-4096 token minimum by model
LLM providers now cache the attention state of a prompt prefix and bill reuse at a fraction of the input price. The interop problem from 2016 reappears in a new medium: the cache key is the exact byte sequence of the prefix, so anything volatile placed early — a timestamp, an unsorted JSON blob, a reordered tool list — is the equivalent of a cache-busting query string appended to every request.
Anthropic's prompt caching is explicit: up to four cache_control
breakpoints, the cached prefix rendered in the order tools → system →
messages, a 5-minute TTL refreshed on each hit (or a 1-hour option).
Writes cost 1.25x base input (2x for the 1-hour TTL); reads cost 0.1x
on most models and less on the newest. Prefixes below a model-dependent
minimum (512–4,096 tokens) are not cached at all. OpenAI's version
is on by default, also prefix-based, with discounted cached-token reads.
The design rule is the asset-pipeline rule restated: stable content
first and immutable, volatile content last. For agents the stable
prefix is the instruction hierarchy (CLAUDE.md, tool schemas); see
Agentic Workflow 2026 for how that hierarchy maps onto cache layers,
and LLM Cost Monitoring for cache-read versus cache-write pricing in
practice.