When Workers Cache is enabled, every cacheable request to your Worker hits Cloudflare's cache first. If there's a fresh cached response, Cloudflare returns it directly — your Worker doesn't run, and you don't pay CPU time for it. On a miss, your Worker runs, and if your response is cacheable, Cloudflare stores it for the next request. The next request from anywhere on Earth can be served straight from cache.
Incredible! This is why I shoehorn all my server side usecases on to the Workers Platform. Cloudflare, since 2020 when I first went all-in, has consistently shipped features that reduce bills significantly (except for 2023 Workers usage model changes). In one case, when they shipped free Snippets (Workers but 32kb code size & 5s CPU time) for Pro accounts ($200/yr), our bills went from £15k+ to £0.
I know about the infamous "Enterprise plan" (especially, when your bandwidth is as high as ours in 100s of TBs) and know of at least one other tech shop that was required to pay for it ... but we haven't got that sales call, yet.
Amazing, exactly what Workers lacked, I was quite annoyed that a worker would spin up when 99% of my requests then return a cache. Waste of 3ms, times millions.
Huge props to just sticking with the HTTP spec on this one with `Cache-Control` headers with `stale-while-revalidate` support. It's amazing how many other providers mess that up.
On top of that the cache tags are a slick way to do invalidation. This looks like a great product.
Reinventing the web, one service at a time. Is it really that hard to build things like this on a per-case base, small scale, all on a bare bones VPS? Do we need a global gateway to help us not screw up the basics? Service looks nice but makes me sad somehow.
As a DevOps engineer, if you learn to implement CF Workers with Claude Code's Cloudflare skill, you will no longer be burdened by this question. I'm running a Docker swarm on my homelab, a Docker swarm on a EC2 instance, but there's no longer a single web service in either of them. Workers is insanely good.
> This is the caching API we've always wanted Workers to have. Here's why it took us this long
I was looking forward to the "why it took us this long" explanation but it wasn't explicitly spelled out. Any Cloudflare staff here able to expand on that?
(The article does a good job of showing how many different smart design decisions went into this, but given caching is core to what a Cloudflare does I'm still a little surprised it took 9 years to get here!)
We implemented the "standard" Cache API back in the early days because it was a standard, one which was intended to be used together with the Service Workers API, which we were also building around back then.
But it was never a good fit. The get/put API was designed for a local browser cache, not a distributed cache like Cloudflare's. We probably should have realized this before implementing it, but it really became obvious over the years of actual use.
But given we had something that mostly worked for most use cases, it was hard to prioritize redoing it against all the other things on our plate. So we deferred.
More recently, architectural changes we've been making in Workers for other reasons happened to make it significantly easier to finally implement this the way we wanted, and we were able to find some engineering time to get it over the line.
Gory details for the curious: We've been improving the infrastructure around the notion of workers having multiple "entrypoints", with the ability to parameterize those entrypoints. ctx.props[0] and ctx.exports[1] are part of this. A lot of this was motivated by Dynamic Workers sandboxing, but the concept also presents a clean way to inject a cache between two parts of the same worker, by applying it to the entrypoint and having the worker call itself using ctx.exports.
Moreover, the introduction of "channel tokens" made a big difference[2][3]. Essentially I created a way to encode a token (bytes) representing an arbitrary entrypoint to a Worker, complete with its serialized parameters. I did this to enable these entrypoint stubs to be passed over RPC, which is again useful for sandboxing use cases, but it also created a convenient, encapsulated way to pass information through our cache infrastructure about what worker should run at the other end.
It's not a huge breakthrough or anything, but I think it made the architecture clearer in everyone's mind to the point that we got excited about using it to implement caching properly, finally.
That is, workers run "in front of" cache. That's usually what you want. Workers run on the edge, so putting them in front of cache doesn't cost anything in terms of latency, and there's a lot of useful stuff you can do only if they run in front, e.g. serving pages from cache but customizing them for specific users.
Putting workers behind cache is an architectural change. All of the logic around routing to them lives in front of the cache. And it makes Workers a lot less useful.
We weren't that excited about it until we had a clearer story for how to run custom logic on both sides of the cache, but it was pretty unclear how to do that in a nice way until the recent developments around ctx.props, ctx.exports, channel tokens, etc.
Does that new architecture make it more expensive to serve worker’s static assets and do worker-to-worker invocations? Or just harder to measure?
This change in billing makes enabling caching not such a straightforward decision, and encourages separating cached and non-cached parts of your workers into two separate workers, which is a bit annoying.
I wasn't personally involved in pricing here so I can't really say exactly why it is the way it is.
However, I'd note that, in terms of our costs, it's often cheaper for us to run your Worker than to serve from cache. Consider that Workers run locally in whatever edge location the request landed in. We are not saving any bandwidth costs by putting the cache in front of Workers rather than behind, and we've built an architecture such that running a Worker is extremely cheap, almost free.
In that light, making cache hits free would basically be giving away money. To be honest, that is another reason why implementing cache in front of Workers has been held back so long. I think the team finally decided that the only way we can deliver it without giving away too much money is to charge full price for the requests. Workers pricing is already incredibly generous, and we do need to run a business at the end of the day.
(Of course, a cache hit still saves you the CPU pricing. CPU time pricing more directly translates to costs for us, so that makes sense.)
I admit the side effect on static assets is a little weird. But no other provider offers unlimited free static asset serving to start with -- so it's only weird relative to our own unreasonable standards. :) I think the thought here is that if you need caching in front of Workers, that implies you are doing complex work in Workers and getting a lot of value out of them. So a bit of "price segmentation" kicks in here. That said, you can work around it by serving static assets from another hostname, etc.
No objection towards charging for cache hits, when the worker would have ran. That (except in worker-to-worker invocations) is always a strict upgrade, when it comes to pricing.
Worker-to-worker invocations also now participate in the cache, so despite the pricing increase, at least we’re getting some added value.
But for static assets in particular, I don’t see what value the cache is adding, so it feels like pure price segmentation.
Or, to put it another way, it makes no sense to use the Workers Static Assets feature now instead of just having the worker read the file from disk and respond with appropriate cache headers to cache indefinitely, since they cost approximately the same.
The previous Worker in-front of the cache never made sense to my old school proxy in-front of the appserver mindset. Already using this to speed up a tool. Nice work.
It makes sense in a world where Cloudflare is something that runs in front of your app. You need to run some arbitrary code before their logic, including their cache. If you wanted something that ran after the cache, you could put it in your app, you didn’t need the worker. That was Cloudflare when Workers launched.
But now the platform has evolved to where you’re running the entire app from Workers.
I was a bit confused what this adds other than just standard CDN-Cache-Control page caching that we do now. Some quirks I've found;
- You still get billed per request, whether the request hits cache or not (but don't get billed for CPU time)
- You now get billed for static asset requests! This makes no sense to me. "One thing to watch: when caching is enabled, requests that are normally free — static asset requests and worker-to-worker invocations through service bindings or ctx.exports — are billed at the standard request rate, because each one now consults the cache in front of your Worker." Yeah that sounds like a bug that just happens to generate them more money.
- The cache key automatically has the worker deployment version, so even gradual deployments populate their own cache which is nice
- It seems like you can set a totally custom cache key? But that was previously Enterprise only, can't see if that's still the case here.
> I was a bit confused what this adds other than just standard CDN-Cache-Control page caching that we do now.
Until now these cache headers didn't work if you set them in a Worker, because Workers were always run in front of the cache. You could use them if you're running Cloudflare cache in front of another origin, but not if you were e.g. rendering a site in Workers. This changes that.
> Yeah that sounds like a bug that just happens to generate them more money.
Yeah. It sounds like they moved the component that counts requests earlier in the pipeline so that it can count cache hits, and now it also counts these.
Time to migrate rarely-updating websites that still need a CMS to Cloudflare.
> One thing to watch: when caching is enabled, requests that are normally free — static asset requests and worker-to-worker invocations through service bindings or ctx.exports — are billed at the standard request rate, because each one now consults the cache in front of your Worker.
That’s a very weird limitation from a product standpoint. You might end up spending more money by enabling caching.
I wonder if static assets and worker-to-worker invocations are really more expensive to them with caching enabled, or if this is just a metering problem.
I would have made this free in the interest of making billing simpler and of making the decision to enable caching more straightforward.
If you’re min-maxing costs, this encourages awkward setups where you split workers into two, one with caching enabled and one with caching disabled.
37 comments
[ 3.0 ms ] story [ 37.4 ms ] threadI know about the infamous "Enterprise plan" (especially, when your bandwidth is as high as ours in 100s of TBs) and know of at least one other tech shop that was required to pay for it ... but we haven't got that sales call, yet.
I am assuming it is a bunch of manual work.
A big worry was always "why does workers sit in front of my cache? that's a waste of an invocation if i'm returning a cached result"
On top of that the cache tags are a slick way to do invalidation. This looks like a great product.
I was looking forward to the "why it took us this long" explanation but it wasn't explicitly spelled out. Any Cloudflare staff here able to expand on that?
(The article does a good job of showing how many different smart design decisions went into this, but given caching is core to what a Cloudflare does I'm still a little surprised it took 9 years to get here!)
We implemented the "standard" Cache API back in the early days because it was a standard, one which was intended to be used together with the Service Workers API, which we were also building around back then.
But it was never a good fit. The get/put API was designed for a local browser cache, not a distributed cache like Cloudflare's. We probably should have realized this before implementing it, but it really became obvious over the years of actual use.
But given we had something that mostly worked for most use cases, it was hard to prioritize redoing it against all the other things on our plate. So we deferred.
More recently, architectural changes we've been making in Workers for other reasons happened to make it significantly easier to finally implement this the way we wanted, and we were able to find some engineering time to get it over the line.
Gory details for the curious: We've been improving the infrastructure around the notion of workers having multiple "entrypoints", with the ability to parameterize those entrypoints. ctx.props[0] and ctx.exports[1] are part of this. A lot of this was motivated by Dynamic Workers sandboxing, but the concept also presents a clean way to inject a cache between two parts of the same worker, by applying it to the entrypoint and having the worker call itself using ctx.exports.
Moreover, the introduction of "channel tokens" made a big difference[2][3]. Essentially I created a way to encode a token (bytes) representing an arbitrary entrypoint to a Worker, complete with its serialized parameters. I did this to enable these entrypoint stubs to be passed over RPC, which is again useful for sandboxing use cases, but it also created a convenient, encapsulated way to pass information through our cache infrastructure about what worker should run at the other end.
It's not a huge breakthrough or anything, but I think it made the architecture clearer in everyone's mind to the point that we got excited about using it to implement caching properly, finally.
[0] https://developers.cloudflare.com/workers/runtime-apis/conte...
[1] https://developers.cloudflare.com/workers/runtime-apis/conte...
[2] https://github.com/cloudflare/workerd/blob/main/src/workerd/...
[3] https://github.com/cloudflare/workerd/blob/main/src/workerd/...
Our architecture is something like:
ingress -> routing -> security -> workers -> cache -> origin
That is, workers run "in front of" cache. That's usually what you want. Workers run on the edge, so putting them in front of cache doesn't cost anything in terms of latency, and there's a lot of useful stuff you can do only if they run in front, e.g. serving pages from cache but customizing them for specific users.
Putting workers behind cache is an architectural change. All of the logic around routing to them lives in front of the cache. And it makes Workers a lot less useful.
We weren't that excited about it until we had a clearer story for how to run custom logic on both sides of the cache, but it was pretty unclear how to do that in a nice way until the recent developments around ctx.props, ctx.exports, channel tokens, etc.
This change in billing makes enabling caching not such a straightforward decision, and encourages separating cached and non-cached parts of your workers into two separate workers, which is a bit annoying.
However, I'd note that, in terms of our costs, it's often cheaper for us to run your Worker than to serve from cache. Consider that Workers run locally in whatever edge location the request landed in. We are not saving any bandwidth costs by putting the cache in front of Workers rather than behind, and we've built an architecture such that running a Worker is extremely cheap, almost free.
In that light, making cache hits free would basically be giving away money. To be honest, that is another reason why implementing cache in front of Workers has been held back so long. I think the team finally decided that the only way we can deliver it without giving away too much money is to charge full price for the requests. Workers pricing is already incredibly generous, and we do need to run a business at the end of the day.
(Of course, a cache hit still saves you the CPU pricing. CPU time pricing more directly translates to costs for us, so that makes sense.)
I admit the side effect on static assets is a little weird. But no other provider offers unlimited free static asset serving to start with -- so it's only weird relative to our own unreasonable standards. :) I think the thought here is that if you need caching in front of Workers, that implies you are doing complex work in Workers and getting a lot of value out of them. So a bit of "price segmentation" kicks in here. That said, you can work around it by serving static assets from another hostname, etc.
Worker-to-worker invocations also now participate in the cache, so despite the pricing increase, at least we’re getting some added value.
But for static assets in particular, I don’t see what value the cache is adding, so it feels like pure price segmentation.
Or, to put it another way, it makes no sense to use the Workers Static Assets feature now instead of just having the worker read the file from disk and respond with appropriate cache headers to cache indefinitely, since they cost approximately the same.
1. What headers does content that will revalidate use on client. Eg can a react hook trigger if a cms json revalidates?
2. If edge worker can call database worker: if database worker sends stale while revalidate, does that revalidate whole worker chain?
btw overall very cool, esp fine grained cache for authed/etc data
But now the platform has evolved to where you’re running the entire app from Workers.
I just don’t understand why undermine your own announcements by delegating comms to the machine. It’s disrespectful to the reader.
I found it funny to just read through.
Thanks CF.
- You still get billed per request, whether the request hits cache or not (but don't get billed for CPU time)
- You now get billed for static asset requests! This makes no sense to me. "One thing to watch: when caching is enabled, requests that are normally free — static asset requests and worker-to-worker invocations through service bindings or ctx.exports — are billed at the standard request rate, because each one now consults the cache in front of your Worker." Yeah that sounds like a bug that just happens to generate them more money.
- The cache key automatically has the worker deployment version, so even gradual deployments populate their own cache which is nice
- It seems like you can set a totally custom cache key? But that was previously Enterprise only, can't see if that's still the case here.
Until now these cache headers didn't work if you set them in a Worker, because Workers were always run in front of the cache. You could use them if you're running Cloudflare cache in front of another origin, but not if you were e.g. rendering a site in Workers. This changes that.
Yeah. It sounds like they moved the component that counts requests earlier in the pipeline so that it can count cache hits, and now it also counts these.
Time to migrate rarely-updating websites that still need a CMS to Cloudflare.
> One thing to watch: when caching is enabled, requests that are normally free — static asset requests and worker-to-worker invocations through service bindings or ctx.exports — are billed at the standard request rate, because each one now consults the cache in front of your Worker.
That’s a very weird limitation from a product standpoint. You might end up spending more money by enabling caching.
I wonder if static assets and worker-to-worker invocations are really more expensive to them with caching enabled, or if this is just a metering problem.
I would have made this free in the interest of making billing simpler and of making the decision to enable caching more straightforward.
If you’re min-maxing costs, this encourages awkward setups where you split workers into two, one with caching enabled and one with caching disabled.