The Wasm Component Model on Fastly Compute
HTTP Cache: Options and Purging
September 10, 2026, by Kats Omuro (@katsuyukiomuro (opens in a new window) on X/Twitter)
EDIT (2026-09-10): added a working example. It now runs against a real service, and the output is in "Reading it directly" below.
Both cache interfaces have a record called write-options. Both have one called lookup-options. Neither pair shares a definition, and http-cache imports plenty of other types from cache without importing these.
Setting them side by side is worth doing for its own sake. It also answers two questions the Core Cache posts had to leave open.
Two write-options
The Core Cache's version, from the write-options post, has ten fields and leaves five of them without a doc comment. The HTTP Cache's version has eight, and documents every single one.
Seven fields appear in both: max-age-ns, vary-rule, initial-age-ns, stale-while-revalidate-ns, surrogate-keys, length, and sensitive-data. Most of those are the ABI's spelling of things Fastly documents under cache freshness (opens in a new window), including how Age accumulates and why an object's remaining lifetime is not simply its max-age.
Three appear only in the Core Cache's: request-headers, user-metadata, and edge-max-age-ns. The first two make sense as omissions. request-headers exists there so a non-transactional insert can supply headers for vary matching, and http-cache has no non-transactional insert; the headers came in with the request at lookup time. user-metadata is an arbitrary byte blob for a cache that stores arbitrary bytes, and an HTTP cache stores responses, which already have headers to put things in.
edge-max-age-ns is the interesting absence, because it was also the field the Core Cache never documented.
And one appears only in the HTTP Cache's:
/// The maximum duration after `max-age` during which the response may be delivered stale
/// if synchronous revalidation produces an error.
///
/// If this field is not set, the default value is zero.
stale-if-error-ns: option<duration-ns>,
That's the field transaction-choose-stale operates on, which the stale content post put to work. It has no Core Cache equivalent because serving stale on error is an HTTP caching concept, and the Core Cache has no idea what an error response even looks like.
The question the Core Cache left open
Here is the Core Cache's sensitive-data, quoted in full:
sensitive-data: bool,
That's the entire declaration. The write-options post could only report what it observed: setting it changed nothing visible from inside the guest that wrote the object.
Here is the same field in http-cache:
/// Enable or disable PCI/HIPAA-compliant non-volatile caching.
///
/// See the [Fastly PCI-Compliant Caching and Delivery documentation] for details.
///
/// [Fastly PCI-Compliant Caching and Delivery documentation]: https://docs.fastly.com/products/pci-compliant-caching-and-delivery (opens in a new window)
sensitive-data: bool,
There it is. The flag controls whether the object is stored under Fastly's PCI/HIPAA-compliant caching rules, which is about where and how the platform is permitted to persist it. Nothing to do with guest visibility, which is exactly why nothing changed when the guest looked.
The same trick works on length. The Core Cache declares it bare; here it comes with a reason to bother setting it:
/// The length of the response body.
///
/// If this field is not set, the length of the body is treated as unknown.
///
/// When possible, this field should be set so that other clients waiting to retrieve the
/// body have enough information to synthesize a `content-length` even before the complete
/// body is inserted to the cache.
length: option<object-length>,
Declaring the length isn't about the writer at all. It's about the readers collapsed behind you, who can start sending a content-length downstream before your body has finished arriving.
Two fields, undocumented in one interface and fully documented in the other, describing the same underlying storage layer. If you're reading compute.wit to understand a field and come up empty, check whether its sibling interface has more to say.
What a purge can reach
The surrogate keys field is documented here too, and it confirms in writing what the purging post worked out by experiment:
/// A list of surrogate keys that may be used to purge this response.
///
/// The format is a string containing [valid surrogate keys] separated by spaces.
///
/// If this field is not set, no surrogate keys will be associated with the response. This
/// means that the response cannot be purged except via a purge-all operation.
///
/// [valid surrogate keys]: https://www.fastly.com/documentation/reference/http/http-headers/Surrogate-Key/ (opens in a new window)
surrogate-keys: option<string>,
"The response cannot be purged except via a purge-all operation." An untagged object is unreachable, and the only remaining lever is the blunt one.
Which raises a question the purge interface doesn't answer: there is no purge-all function in compute.wit. The interface has purge-surrogate-key and purge-surrogate-key-verbose, and that is all of it. A doc comment in one interface refers to an operation another interface doesn't expose, so purge-all is real at the platform level, reachable from Fastly's API or UI, and simply not something edge code can do.
That's worth knowing before you rely on it as a fallback, because from inside a Compute service it isn't one.
There's no purge function in http-cache either. Both caches share the same purge surface, which was the whole point of the post that opened this arc: three caches, one purge surface. Purging an HTTP-cached response means tagging it with surrogate-keys at write time and calling purge-surrogate-key later, exactly as the Core Cache does.
Two lookup-options
The lookup records share nothing at all.
The Core Cache's carries request-headers and always-use-requested-range. The HTTP Cache's carries override-key and backend. Zero overlap, which makes sense once you see what each is for: the Core Cache's lookup needs to be told about headers and ranges because it only ever got a byte-string key, while the HTTP Cache's lookup already has the whole request and instead needs a way to override what it derives from it.
The pattern holds across both interfaces. cache is a key/value store that HTTP concepts have to be handed to explicitly. http-cache is an HTTP cache that already knows, and gives you escape hatches for when it knows wrong.
Reading it directly
Full working code: full example on GitHub (opens in a new window).
None of this runs under Viceroy, which still answers Error::Unsupported for every http-cache function, so the example was deployed to a real service. It is the guest pipeline from the previous posts with two directives bolted on: sk= sets the surrogate keys in write-options instead of accepting the suggested ones, and purge= skips the pipeline entirely and calls purge-surrogate-key.
Store two objects under different keys:
storage-action: StorageAction::Insert
surrogate-keys written: Some("tag-a7677")
discharged with: transaction-insert (5 bytes)
surrogate-keys written: Some("tag-b9687")
discharged with: transaction-insert (4 bytes)
Ask for both again and both are hits:
alpha: LookupState(FOUND | USABLE)
beta: LookupState(FOUND | USABLE)
Now purge one of the two keys, and only that one:
purge-surrogate-key: "tag-a7677" -> Ok(())
alpha: LookupState(MUST_INSERT_OR_UPDATE)
beta: LookupState(FOUND | USABLE)
Three things at once, in those last two lines. The surrogate keys this guest wrote through http-cache's write-options were real, and the cache honored them. purge-surrogate-key, which lives in a different interface entirely and has no idea http-cache exists, reached an object that http-cache stored. And the purge was scoped to the key rather than blunt: beta never moved.
That is the claim the post that opened this arc made from reading the WIT, running. Three caches, one purge surface, and the surface really is shared rather than merely adjacent.
The example also declines the suggestion on its way past. get-suggested-write-options had a value for surrogate-keys and the code overrides it with its own, which is the separation the after-the-send post spends its closing section on, exercised in one field.
What is still unexercised is the Core Cache comparison this post is built around. Those examples run locally and always have; what could not be shown until now was the HTTP Cache side writing keys that the shared purge surface could find.
Beyond the WIT
The two write-options records are the same idea specialized twice, and the specialization is uneven in a way that matters to anyone building on top of both.
An SDK exposing both caches has to decide whether its users see one options type or two. One type is tempting: seven of the fields are shared, the names are identical, and a single CacheWriteOptions looks like an obvious deduplication. It also means every user of the Core Cache gets a stale_if_error field that does nothing, and every user of the HTTP Cache gets a user_metadata field that does nothing, with no compile-time signal either way. The ABI's own doc comments are the only thing that would have told them, and they've just been merged away.
Two types is more code and more honest, and it's what the ABI is telling you to do by declining to share the definition itself. http-cache imports lookup-state, object-length, duration-ns, and cache-hit-count from cache without hesitation. It could have imported write-options too. It didn't, and the fields that differ are exactly the fields that would be wrong.
The broader habit worth taking from this arc: when an ABI repeats a name instead of sharing a definition, that's usually deliberate, and it's usually load-bearing. Two types with the same name and seven identical fields look like duplication that a good binding should clean up. Here it's the opposite. The duplication is the information, and flattening it hands your users a type whose fields are silently inert depending on which function they pass it to. That's precisely the failure mode write-options already has within a single interface, where four functions share one record and the rules live in prose. Reproducing it across two interfaces would be choosing to make it worse.
That closes out caching. Next: we leave storage behind entirely and look at what Fastly gives you for talking about your service while it runs, starting with real-time logging.