Skip to content

Implement Caching Strategies

⏱ 20 minutes advanced

Every content request without caching hits the database, renders Razor templates, and builds the response from scratch. On high-traffic sites, this creates unnecessary load and slower page responses. CMS provides multiple caching layers — output caching, content repository caching, and custom cache dependencies — that work together to serve cached responses until content changes.

  1. Enable output caching on page controllers
  2. Configure content cache settings
  3. Set up cache dependencies for automatic invalidation
  4. Build a custom cache key strategy

Use the [OutputCache] attribute on your page controllers to cache the full HTTP response.

Output caching on a controller
csharp
using Microsoft.AspNetCore.Mvc;
using Optimizely.Cms.Core.Web;

namespace MySite.Controllers;

public class ArticlePageController : PageController<ArticlePage>
{
    [ResponseCache(
        Duration = 300,
        Location = ResponseCacheLocation.Any,
        VaryByQueryKeys = new[] { "page", "category" })]
    public IActionResult Index(ArticlePage currentPage)
    {
        return View(currentPage);
    }
}

Key settings:

  • Duration — Cache lifetime in seconds. Start with 300 (5 minutes) and adjust.
  • Location — Use Any for CDN and browser caching, Client for browser only.
  • VaryByQueryKeys — Create separate cache entries for different query parameters.

Verify: Open browser devtools, navigate to a cached page, and confirm the Cache-Control response header shows your configured max-age. Reload the page and check for a 200 (from disk cache) or 200 (from memory cache) status in the Network tab.

The CMS caches content objects in memory by default. You can tune this behavior in Program.cs.

Content cache configuration
csharp
using Optimizely.Cms.Core;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCms()
    .Configure<ContentCacheOptions>(options =>
    {
        // Maximum items in the content cache
        options.CacheSize = 5000;

        // Time before cache entries expire
        options.SlidingExpiration =
            TimeSpan.FromMinutes(20);

        // Enable cache statistics for monitoring
        options.EnableStatistics = true;
    });

The content cache stores deserialized IContent objects. Increasing CacheSize uses more memory but reduces database queries.

Verify: After configuring content caching, enable EnableStatistics and check the cache hit ratio in Application Insights or your custom telemetry dashboard. A healthy content cache should show a hit ratio above 85%. If the ratio is lower, increase CacheSize or review your cache expiration settings.

Cache dependencies ensure that when content changes, all related cached responses are automatically invalidated.

Cache dependencies with content links
csharp
using Microsoft.AspNetCore.Mvc;
using Optimizely.Cms.Core.Web;
using Optimizely.Cms.Core.Caching;

namespace MySite.Controllers;

public class StartPageController : PageController<StartPage>
{
    private readonly IContentCacheKeyCreator _cacheKeys;

    public StartPageController(
        IContentCacheKeyCreator cacheKeys)
    {
        _cacheKeys = cacheKeys;
    }

    public IActionResult Index(StartPage currentPage)
    {
        // Create cache dependency on the start page
        // When the start page is republished,
        // this cached response is purged automatically
        var dependency = _cacheKeys
            .CreateCommonCacheKey(currentPage.ContentLink);

        HttpContext.Response.Headers.Append(
            "X-Cache-Dependency", dependency);

        return View(currentPage);
    }
}

For pages that vary by user state or custom conditions, implement a cache key strategy.

Custom vary-by logic
csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace MySite.Caching;

public class VaryByUserTypeAttribute : ActionFilterAttribute
{
    public override void OnResultExecuting(
        ResultExecutingContext context)
    {
        var isAuthenticated = context.HttpContext.User
            .Identity?.IsAuthenticated ?? false;

        var cacheProfile = isAuthenticated
            ? "authenticated"
            : "anonymous";

        context.HttpContext.Response.Headers.Append(
            "Vary", "X-User-Type");
        context.HttpContext.Items["CacheProfile"] =
            cacheProfile;

        base.OnResultExecuting(context);
    }
}

Apply the attribute to controllers that serve different content for logged-in and anonymous users.

Note: The VaryByUserTypeAttribute sets a Vary header and stores a cache profile identifier, but it does not invoke the response caching middleware on its own. To make this work, you must also register response caching in Program.cs (builder.Services.AddResponseCaching() and app.UseResponseCaching()) and apply [ResponseCache] on the controller action alongside [VaryByUserType]. Without UseResponseCaching() in the pipeline, the Vary header is sent to downstream caches (CDN, browser) but the server does not cache the response itself.

Verify: After applying the attribute, make a request as an anonymous user and check the Vary response header in devtools — it should include X-User-Type. Then log in and repeat. Confirm the two requests produce different cached entries by comparing response content or checking the X-Cache-Profile value if you expose it.

Never cache responses when editors are previewing content. The CMS sets a context flag you can check.

Skip caching in edit mode
csharp
using Optimizely.Cms.Core.Web;

public IActionResult Index(ArticlePage currentPage)
{
    if (PageEditing.PageIsInEditMode)
    {
        Response.Headers.CacheControl = "no-cache, no-store";
    }

    return View(currentPage);
}

On PaaS hosting, each web application instance maintains its own in-memory content cache. When content is published, the CMS invalidates the cache on the instance that processed the publish event, but other instances may continue serving stale content until their cache entries expire or they receive a cache invalidation message.

Approaches:

  • Accept eventual consistency — If your SlidingExpiration is short (5-20 minutes), stale content resolves itself quickly. This is acceptable for most content sites where a few minutes of delay after publishing is tolerable.
  • Use a distributed cache — Replace the default in-memory cache with a distributed cache provider like Redis. Configure this in Program.cs by registering an IDistributedCache implementation. This ensures all instances share the same cache state, but adds a network hop for every cache read.
  • Enable cache invalidation messaging — Optimizely CMS supports inter-instance cache invalidation through Azure Service Bus or a message broker. When configured, a publish event on one instance sends an invalidation message to all others. This is the recommended approach for multi-instance PaaS deployments where freshness matters.

For single-instance deployments or SaaS, this is not a concern — the platform handles cache consistency automatically.

MetricWhere to checkTarget
Cache hit ratioApplication Insights or custom telemetryAbove 85% for content pages
Memory usageProcess metricsKeep content cache under 500 MB
Cache evictionsEnableStatistics countersIncrease CacheSize if evictions are high
Response timeRequest duration metricsUnder 100 ms for cached pages