The Wasm Component Model on Fastly Compute
Caching: Three Caches, One Purge Surface
August 25, 2026, by Kats Omuro (@katsuyukiomuro (opens in a new window) on X/Twitter)
Fastly started out as a CDN, and caching is still core to its identity—purging alongside it. Fastly's own blog is still proud of it (opens in a new window): sub-150ms global purges, offered for over a decade now, a number the post itself says sits close to a hard physical limit—light takes roughly 65ms to cross the planet. A cache you can't invalidate that fast isn't much of a CDN feature at all.
You'd be right to think, then, that Compute gives your application a way to actually touch that cache, not just benefit from it invisibly at the edge. Fastly's own Caching Concepts (opens in a new window) documentation names three ways in: Core Cache, Simple Cache, and the Readthrough Cache—compute.wit itself calls that last one HTTP Cache. They don't all work the same way, but they're each useful in their own niches.
You've actually already used one of the three, without ever calling it by name: every backend call in this series has gone through
send, and Calling a Backend by Hand already flagged thatsendis the readthrough cache's entry point.
What all three do share is a single way to purge what's in them.
That's the arc for the next few posts—maybe even weeks, there's enough here to earn it.
This one is the map: where each of the three lives in compute.wit, or doesn't, and the purge surface all of them answer to.
Core Cache: full control, low-level
/// [Core Cache] API
///
/// [Core Cache]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/#core-cache (opens in a new window)
interface cache {
...
}
That's the entire doc comment—cache is the interface everything else here either builds on or contrasts against. It's a general-purpose key/value cache: a list<u8> key, a body you stream bytes into, and metadata (max age, surrogate keys, user metadata, stale-while-revalidate) attached at write time. Nothing about HTTP semantics, nothing automatic. Whatever ends up in it, guest code put there on purpose. The full walkthrough comes in a later post.
Simple Cache: doesn't exist in compute.wit
You'd reasonably expect a simple-cache interface too, since Fastly documents "Simple Cache" as a real, named feature. Searching compute.wit end to end says otherwise:
$ grep -ci "simple" compute.wit
0
Zero matches turn up, anywhere in the file (turns out "simple" was never compute.wit's problem to solve—that's the SDK's job). Simple Cache isn't a second cache type at the ABI-level—it's a convenience layer the various language SDKs build on top of cache's entry resource: Fastly's own Rust SDK docs (opens in a new window) describe the fastly::cache::simple module as "a non-durable key-value API backed by the same cache platform as the Core Cache API." Same storage, same interface underneath—just a smaller get/get_or_set/get_or_set_with/purge surface stands in for the full entry resource.
In a later post, we'll explore building that same convenience layer by hand, directly against cache.entry, to see exactly which calls it collapses down to.
HTTP Cache: driving the readthrough cache yourself
/// [HTTP Cache] API.
///
/// Overall, this should look very familiar to users of the Core Cache API. The primary differences
/// are:
///
/// - HTTP `request`s and `response`s are used rather than relying on the user to
/// encode headers, status codes, etc in `user-metadata`.
///
/// - Convenience functions specific to HTTP semantics are provided, such as `is-request-cacheable`,
/// `get-suggested-backend-request`, `get-suggested-write-options`, and
/// `transaction-record-not-cacheable`.
///
/// The HTTP-specific behavior of these functions is intended to support applications that match the
/// normative guidance in [RFC 9111]. For example, `is-request-cacheable` returns `false` for `POST`
/// requests. However, this answer along with those of many of these functions explicitly provide
/// *suggestions*; they do not necessarily need to be followed if custom behavior is required, such
/// as caching `POST` responses when the application author knows that to be safe.
///
/// The starting points for this API are `lookup` (no request collapsing) and `transaction-lookup`
/// (request collapsing).
///
/// [HTTP Cache]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/cache-freshness/ (opens in a new window)
/// [RFC 9111]: https://www.rfc-editor.org/rfc/rfc9111.html (opens in a new window)
interface http-cache {
...
}
This is the same readthrough HTTP cache send already puts you through automatically—not a third, unrelated cache, but a second way of participating in the one thing.
send's automatic behavior (referred to as host mode in the SDKs), never touches anything in this interface at all: it's driven entirely by cache-override, a much smaller surface on http-req you already met back in Calling a Backend by Hand. http-cache is the other mode (guest mode) where your own code takes over the job send normally does silently.
The doc comment's opening line says it should "look very familiar to users of the Core Cache API." The main differences from Core Cache are specifically about HTTP: it works in terms of request/response instead of an opaque byte-string key and user-metadata blob, and it adds RFC 9111-aware convenience functions like is-request-cacheable—which the doc comment is careful to call a suggestion, not a rule anything is obligated to follow. The readthrough cache gets its own post later, in both host and guest modes, with a full RFC 9111 primer.
Same storage, asymmetric access
Fastly's interoperability section (opens in a new window) is explicit that these three aren't isolated stores that happen to share a name: Core Cache, Simple Cache, and HTTP Cache all read and write the same underlying storage layer and address space.
What they don't share is equal access to each other's objects. Core Cache can read and overwrite what Simple Cache put there. Simple Cache can read what Core Cache put there, but can't overwrite it. HTTP Cache doesn't interoperate with either at all. Whether any of that asymmetry has a corresponding rule inside compute.wit itself, or whether it's purely enforced at the storage layer below the ABI, is a question for the post that digs into it.
Purging: the one control surface for all three
/// [Cache Purging] API.
///
/// [Cache Purging]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/purging/ (opens in a new window)
interface purge {
use types.{error};
record purge-options {
/// Perform a [soft purge] instead of a hard purge.
///
/// [soft purge]: https://www.fastly.com/documentation/guides/concepts/edge-state/cache/purging/#soft-vs-hard-purging (opens in a new window)
soft-purge: bool,
...
}
...
/// Purge a surrogate key for the current service.
///
/// A surrogate key can be a max of 1024 characters.
/// A surrogate key must contain only printable ASCII characters (those between `0x21` and `0x7E`,
/// inclusive).
purge-surrogate-key: func(
surrogate-keys: string,
purge-options: purge-options,
) -> result<_, error>;
...
}
purge-surrogate-key is keyed on surrogate keys rather than the cache key you looked an object up by—which only works if something attached a surrogate key at write time in the first place. purge-options.soft-purge chooses between a soft purge and a hard purge (opens in a new window) (the default is a hard purge).
Watch the parameter name, though:
surrogate-keysis plural, the same namewrite-optionsandget-surrogate-keysuse for a genuinely space-separated list of keys. This one isn't a list. The doc comment already gives it away on a close read—singular "a surrogate key," not "keys," and no "separated by spaces" language the way the other two fields spell it out—and the character range it specifies,0x21to0x7E, excludes space entirely, so there's no delimiter available even if you wanted to pack more than one key in.Fastly's purging docs (opens in a new window) also describe the same one-key-per-call behavior for surrogate-key purging, for what that's worth as independent confirmation.
So, purging several keys means calling
purge-surrogate-key(orpurge-surrogate-key-verbose) once per key.
One thing worth flagging before you reach for this in a hurry: propagation time. Fastly's purging docs put single-key and surrogate-key purges at roughly 150ms (opens in a new window) to reach every cache server, distributed via a gossip protocol rather than all at once—worth keeping in mind if your code is about to write the same key straight back, since some servers may not have caught up to the purge yet.
This is the one surface shared by all three caches—a purge against a given surrogate key clears it regardless of which of the three interfaces originally wrote it. A hands-on example comes with the first real purge-surrogate-key call, once there's something worth purging.
There is also an alternate version of purge-surrogate-key that returns a status including an ID that identifies the purge, which may be useful in logging or tracing program behavior.
/// Purge a surrogate key for the current service, and return the purge id.
///
/// This is similar to `purge-surrogate-key`, but on success, returns a
/// [JSON purge response] containing an ASCII alphanumeric string identifying
/// a purging.
///
/// [JSON purge response]: https://developer.fastly.com/reference/api/purging/#purge-tag (opens in a new window)
purge-surrogate-key-verbose: func(
surrogate-keys: string,
purge-options: purge-options,
max-len: u64,
) -> result<string, error>;
Beyond the WIT
Here's a design question purge-surrogate-key's shape raises immediately for a "Simple Cache"-style convenience wrapper: if purging only works by surrogate key, and the raw write-options field for surrogate keys is optional, what does purging by that same cache key even do when nothing attached a surrogate key at insert time?
Fastly's own purging docs (opens in a new window) confirm this isn't hypothetical: "the simple cache interface provides an edge-accessible mechanism for purging an object by key, available in each language SDK." How it works isn't documented, though—that only turns up by reading the SDKs' own source. Rust, Go, and JavaScript SDKs' sources all independently compute the same thing: a SHA-256 hash of the cache key, recomputed identically at insert time and purge time, so purging by cache key becomes purging by a surrogate key nobody has to remember, because it's reproducible from the same input every time.
One more detail from those same docs, easy to miss: unlike every other purge type, a Simple Cache purge from edge code defaults to the local POP only—going global is the opt-in, not the other way around. Backwards from what you'd expect out of a CDN.
It's the kind of decision that never shows up in compute.wit at all—the ABI just offers surrogate-key purging and leaves "how do you purge by the key you actually care about" entirely to whoever wraps it.
Next up: Core Cache, full control and low-level, the interface everything else in this arc builds on or contrasts against.