
The Under-Documented Optimizely Caching API I Somehow Missed
By Dom Reilly · Posted 31 Aug 2026 · 7 min read
I was recently implementing some caching of data using the ISynchronizedObjectInstanceCache following a conversation with Optimizely support, when I came across a method I genuinely didn't know existed:
ReadThrough.
I've worked with Optimizely for years, used ISynchronizedObjectInstanceCache plenty of times and written more variations of Get, check for null, then Insert than I care to remember; yet, I've never stumbled across this method.
Me being me, I ended up doing a bit of a deep dive into the method, scouring the internet and finding limited information.
At first, I thought ReadThrough was just a nicer way of wrapping the usual cache-aside pattern i.e.
Which, to be fair, would still be useful. It's less code to maintain and removes a pattern I've probably manually implemented hundreds of times by now.
But then I spotted the ReadStrategy argument.
The implementation I was looking at was using:
And this is where ReadThrough becomes a little more interesting.
Let's say the cache has expired and two requests hit the application at roughly the same time. With my usual Get and Insert implementation, request one checks the cache and finds nothing. Before it has finished getting the value and putting it back into the cache, request two does exactly the same thing.
Now, both requests have missed the cache and both go off to get the same data - sure, there's locking to prevent this, even double locking, but for this example, we'll ignore that.
Anyway, imagine that we have a busy application, or make GetValue() something expensive like a database query or an API request, and suddenly a cache miss can cause a burst of identical calls - not ideal!
Thankfully, ReadStrategy.Wait handles that scenario for us!
When the first request misses the cache, ReadThrough marks that particular item as currently being loaded and executes the delegate. If another request comes along looking for the same key while that's happening, it doesn't execute the delegate again. It waits for the first request to finish and uses the result.
So instead of:
you essentially get:
It's protection against what's commonly referred to as a cache stampede, and Optimizely has support for it built into an API that, until this point, I didn't even know existed!
Then I found master keys
While looking into ReadThrough, I also ended up going further down the caching rabbit hole and looking at the CacheEvictionPolicy being passed into it.
This is the implementation I ended up with:
The data itself isn't particularly important, but what is important is what happens when somebody changes that page.
Typically, my first thought would normally be to give the cached value an expiry; for example, five minutes.
A fixed time-to-live is fine, but it also means somebody could publish a change in the CMS and potentially have to wait another five minutes before the application starts using it.
To work around this, you could listen for a publish event and manually remove the cache entry, but that feels like I'm creating another bit of code whose sole responsibility is keeping my cache in sync with Optimizely, and then maintaining that - it just feels overkill for a five minute cache.
The good news is, that Optimizely already knows the content has changed, and it turns out that we can use that to our advantage!
The eviction policy for this cache looks like this:
There are two lines in here that I hadn't really paid much attention to before.
The first:
And then:
IContentCacheKeyCreator allows us to get the cache key Optimizely uses to represent the content.
Rather than creating my own cache invalidation based on the content ID, I can use Optimizely's existing one.
The masterKeys property on CacheEvictionPolicy then allows my cache entry to depend on that key.
This was probably the bit where the penny dropped for me, my custom cache entry isn't just cached for five minutes anymore; it's now tied to the Optimizely content that the data came from.
If the page doesn't change, the absolute expiration still applies and eventually the cached value is removed as normal, but if somebody changes and publishes the page first, Optimizely invalidates the associated content cache and my dependent cache entry is invalidated along with it.
This means:
1. No custom publish event.
2. No manual call to Remove.
3. No waiting around wondering why the change you've just published hasn't done anything.
Why CreateCommonCacheKey matters
You could technically create your own master key. In fact, when I first started looking at masterKeys, that's probably what I would have done.
Something along the lines of:
But then something still needs to remove that master key when the content changes for example, via an event listener, and that's the important difference.
By using:
I'm not inventing another cache invalidation mechanism. Instead, I'm attaching my cached value to the one Optimizely is already maintaining.
It's important to flag that I also specifically don't want the versioned ContentReference here.
The data I'm caching isn't tied to version 42 of that particular page, I just care about the page itself. When the next version gets published, the data I've cached from version 42 is immediately something I don't want anymore.
Using the common cache key for the versionless reference gives me that relationship.
There is also a nice side effect of using ISynchronizedObjectInstanceCache here rather than treating this as a normal local memory cache. Optimizely's synchronized cache is designed to propagate cache removals between instances, which becomes particularly important in DXP where the application isn't necessarily running on a single instance.
Without Optimizely implementing propagated cache removals between instances, invalidating the cache on one instance doesn't help much if the next request happens to land on another instance that still has the old value.
It is worth being precise here: ISynchronizedObjectInstanceCache is synchronising cache removals between instances, not turning the cache itself into a shared distributed cache.
Because of the work done by Optimizely, the whole implementation ends up being relatively small:
But, there's a surprising amount hidden behind it.
ReadThroughtakes care of the usual cache-aside plumbing.ReadStrategy.Waitprevents a load of concurrent requests from all doing the same work when that cache is emptymasterKeyslets the cached value depend on something else being cached.- Finally,
IContentCacheKeyCreator.CreateCommonCacheKey()gives us a way of making that dependency the Optimizely content the data actually came from.
One important thing to flag is that this invalidation approach only works with CMS content. After confirming this with Optimizely support, it won't work for something like a DAM asset because publishing or updating an asset in CMP/DAM doesn't trigger the same internal CMS event.
As a result, the cache key we're depending on isn't invalidated, meaning our cached value would remain until its normal expiry.
I think the bit I like most is that none of this requires me to invent another abstraction on top of Optimizely; instead, allowing me to focus on the current feature that I'm working on.
Maybe ReadThrough and master keys are common knowledge and I'm incredibly late to the party, but considering how little I managed to find about them when I went looking, I suspect I'm probably not the only one.
Either way, that's another under-documented Optimizely feature added to the list.
Keep reading
Related articles
19 Years Later: The Undocumented Optimizely Category Inheritance That Caught Me Out
While working on an Optimizely CMS 12 project, I encountered an unexpected issue where categories assigned to a parent page were automatically inherited by newly created child pages. After ruling out custom code and...
Read moreThe Architecture Behind the Blog: From Zero to Production
What started as an all in one Blazor blog gradually evolved into something quite different. From moving the data layer to MySQL and rebuilding the backend in Go, to Dockerising the applications and putting Caddy in...
Read more