The shim.
A 79-line Worker is now the front door for every LayerKick storefront. A warm page leaves the edge in about 9 milliseconds, and every failure path ends up at Shopify serving your store the way it always did.
Two days ago LayerKick’s edge got its permanent front door: the shim, a Worker deployed onto the
storefront’s own zone. A Worker is just a small piece of code Cloudflare runs at its edge, close
to the shopper. The whole of this one’s worker.ts is 79 lines, and it makes a single
decision per request: serve the cached page, or get out of the way.
Everything heavy (rendering, ingest, cache management) lives behind it in a separate backend Worker that the shim relays to. That split is on purpose. The shim exists so the hot path, the code that runs on every single shopper request, stays small enough that I can hold all of it in my head at once. This entry is me reasoning about it out loud, because a front door on a merchant’s storefront is worth being careful about.
One decision per request
The shim only sits in front of a URL once that URL’s page is actually in the cache. A page has to earn its place on our path by already being servable. This is a subtle but important choice, so it’s worth being clear about what it buys.
An uncached URL isn’t ours: its request never touches our code at all, and Shopify serves it the way Shopify always has. We can’t slow down or break a page we’re not even in front of. The store’s default behavior is the untouched Shopify behavior, and we opt individual pages into our path only after we can serve them.
For the URLs that are ours, the shim splits traffic three ways. Page navigations (a shopper loading a product or collection page) go to the serve path. Static asset requests that apps fetch constantly in the background bypass the cache entirely, because caching those wrongly is how you break a store’s cart. And our own control plane relays to the backend Worker over an authenticated channel. That’s honestly the whole file.
// worker.ts, simplified. The real file is 79 lines.
app.all(CONTROL_PLANE, relayToBackend); // internal ops, authenticated
app.get("*", async (c) => {
const page = await fetchServe(c); // per-colo cache, then global
if (page.status !== 404) return page; // warm serve
return fallback(c); // not cached here: origin serves
});
Two things are worth pulling out of that little block. The control-plane relay is how the shim
stays thin: anything that isn’t a plain page serve gets handed to the heavier backend Worker, so
the front door itself never has to grow. And the 404 coming back from fetchServe isn’t an
error the shopper ever sees; it’s an internal signal meaning “not cached here,” which routes the
request into fallback and, from there, to Shopify. The shim treats a cache miss and a genuine
failure the same way on purpose, because both should end up at the origin.
The warm path
Behind the serve path sit two cache tiers, and it’s worth knowing why there are two rather than one:
| Tier | Scope | Rough latency |
|---|---|---|
| Cache API | Per-colo (the shopper’s city) | a few ms |
| KV | Global | tens of ms nearby, more from farther out |
| Shopify origin | Fallback | a full server render |
Those are rough figures, not pinned percentiles. Real latency moves with how far the shopper is from the data and how loaded the path is, which is exactly the variance the two tiers exist to manage.
The Cache API is per-colo. A colo is one of Cloudflare’s data centers, so a Cache API hit serves from the shopper’s own city, which is why it’s so fast. The trade-off is that it’s local: a page cached in one colo isn’t automatically in the others.
KV is Cloudflare’s globally distributed key-value store. A KV hit serves from anywhere, but it’s slower than the local tier and it slows down the farther a shopper sits from the region holding the data, so we use it as the tier below the per-colo cache. When a KV hit serves a shopper, it also backfills that colo’s Cache API in the background, so the next shopper in that same city gets the fast tier instead. The two tiers together mean the first visitor in a city pays the KV cost and everyone after them gets the local speed.
End to end, a warm page leaves the edge in about 9 milliseconds. Internally we can watch a page graduate from origin to the global tier to the per-colo cache across three visits, which is a nice property when something looks slow and we need to know why.
Nine milliseconds is the warm number, and it’s worth being precise about what it beats, because Shopify isn’t slow and it isn’t one far-off origin. Shopify serves from its own edge and it caches too. On a page that’s been hit a few times from the same place, or on a faster plan like Shopify Plus, their serve gets quick on its own. So the honest comparison isn’t our fast page against a naive slow one. It’s warm against warm, and there nine milliseconds is in a different class.
And warm isn’t a lucky state you have to hope for. Every real page view warms that page into our cache, so a page climbs to the fast tier as soon as shoppers start visiting it, and the more it’s hit from a given city the more solidly it stays warm. Both we and Shopify get faster with repeat traffic. The difference is the floor each of us settles at, and the more a store gets used, the further ours pulls ahead. The shim’s job is to make the common case, a page someone has already viewed recently, effectively free to serve again, without the shopper’s browser needing to know any of it happened underneath.
Keys are market-aware, and this part matters more than it sounds. Country and language ride the cache key, so a US visitor gets the USD page and a Thai visitor gets the THB page, each cached separately under its own key. A theme change can invalidate the whole site at once through a single key dimension, without having to hunt down every individual entry.
The cache warms itself
There’s no crawler here, and no scheduled jobs. Warming is demand-driven, which I think is the more honest design: the cache should reflect where people actually go, not where I guessed they’d go.
Here’s how it works. After a real page view, the storefront warms that same page into the cache anonymously, with credentials omitted and the market taken from localization. So the cache ends up being a map of where shoppers actually went, weighted by how often they went there, and it stays about as fresh as the traffic is recent. Popular pages stay warm because they’re popular; forgotten pages fall out because nobody’s asking for them.
Ingest is also where the writes get cheap. A re-ingested page that hasn’t actually changed skips the KV write entirely. There’s no point paying to write bytes that didn’t change. In practice that’s roughly 90% of refreshes, since most page views don’t actually change the page. Stale entries get re-warmed in the background, stale-while-revalidate style, meaning we serve the slightly-old version instantly and refresh it behind the scenes, so freshness costs the shopper nothing.
Failure is the default path
The shim was designed backwards from its failure modes, and it turns out every one of them ends at Shopify. That wasn’t an accident; it was the first requirement, before speed.
Walk the paths. An uncached URL has no route, so we’re not in the request at all, and Shopify serves it. A miss on a routed URL falls through to the origin while the page re-warms in the background. And if the shim itself ever throws, that unhandled exception falls through to the origin too, instead of surfacing an error page. So even a bug that throws ends at Shopify rather than at a broken storefront.
The kill switch is just as boring, and boring is the compliment here. Delete the routes and the store is exactly as it was, byte for byte, as if LayerKick had never existed. There’s no migration to unwind and no state to reconcile, because we never took ownership of anything we couldn’t hand straight back.
That’s one of the properties that makes me comfortable running this on a live store, and it isn’t the only one. The shim only ever sits in front of the browsing pages: the home page, product and collection pages, blog and content pages, the journey a shopper takes on the way to buying. The cart and the checkout, the parts that actually move money, are never ours. They pass straight through to Shopify exactly as they always have, so the shim isn’t anywhere near the transaction to begin with.
On the pages it does sit in front of, Shopify is the terminal state of every failure path, so almost every mistake it could make is designed out: it can’t serve the wrong page, it can’t lose the store to an error. What’s left is mild and self-correcting, a page that on a cold path can come back slower than Shopify would have before it warms. That’s the whole of the downside, bounded and recoverable, a slower page at worst rather than a broken storefront, which is the kind of thing I can actually reason about.
What’s next in the log
The shim is deliberately thin, which just moves the scrutiny one hop back. The backend Worker behind it is heavier than I want anything near the hot path to be, and it’s next on the bench. After that the interesting question stops being how fast one page serves, and becomes how fast the next page can already be there when the shopper clicks.
LayerKick layers onto your existing Shopify theme and serves it from Cloudflare's edge. If anything goes wrong, traffic passes through to Shopify like we were never there. The fastest way to understand it is to watch it run on your own storefront, and the waitlist is the way in.