Experience Edge Caching - Advanced Strategies for Sub-Second Response Times

Experience Edge is Sitecore's global CDN, but caching strategy makes the difference between millisecond response times and multi-second waits. Many implementations treat caching as an afterthought, resulting in stale content, cache invalidation issues, and poor Core Web Vitals.

This guide covers production-grade caching patterns that deliver sub-second responses while maintaining content freshness.


⚠️ The Caching Challenge

Common Problems:

  1. Stale Content: Users see outdated information after publishing
  2. Cache Invalidation: Correct invalidation is harder than it seems
  3. Unpredictable Performance: Sometimes fast, sometimes slow
  4. High Latency Spikes: Traffic causes cache misses

Real-World Impact:

  • First Contentful Paint (FCP): 3-5 seconds (should be less than 1.8s)
  • Largest Contentful Paint (LCP): 5-8 seconds (should be less than 2.5s)
  • Bounce rate: 30-40% (should be less than 10%)

🏗️ Caching Layers Architecture

┌────────────────────────────────────────────────────┐
│              User Browser                          │
│          (Browser Cache: 1 hour)                   │
└────────────┬─────────────────────────────────────┘
             │ Cache Miss
             ↓
┌────────────────────────────────────────────────────┐
│          CDN Edge (Akamai/Cloudflare)             │
│          (CDN Cache: 5-60 minutes)                 │
└────────────┬─────────────────────────────────────┘
             │ Cache Miss
             ↓
┌────────────────────────────────────────────────────┐
│          Experience Edge (Sitecore)               │
│          (Origin Cache: 10 minutes)               │
└────────────┬─────────────────────────────────────┘
             │ Cache Miss
             ↓
┌────────────────────────────────────────────────────┐
│          Next.js Application                       │
│          (ISR Cache + Application Logic)          │
└────────────┬─────────────────────────────────────┘
             │ Cache Miss
             ↓
┌────────────────────────────────────────────────────┐
│          Sitecore XM Cloud (GraphQL)              │
│          (Layout Service, Item Resolution)        │
└────────────────────────────────────────────────────┘

🌐 Part 1: CDN Caching Configuration

Setting Correct Cache Headers

// next.config.js
module.exports = {
  headers: async () => {
    return [
      // Static pages: Long cache
      {
        source: '/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, s-maxage=604800, stale-while-revalidate=1209600'
            // CDN: 7 days, Stale-while-revalidate: 14 days
          }
        ]
      },
      // Dynamic content: Short cache with revalidation
      {
        source: '/news/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, s-maxage=300, stale-while-revalidate=600'
            // CDN: 5 minutes, SWR: 10 minutes
          },
          {
            key: 'Vary',
            value: 'Accept-Encoding, User-Agent'
          }
        ]
      },
      // Personalized content: No cache
      {
        source: '/account/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'private, no-cache, no-store, must-revalidate'
          }
        ]
      }
    ];
  }
};

Cache Headers Reference

HeaderPurposeExample
s-maxageCDN cache duration (seconds)s-maxage=300 (5 min)
stale-while-revalidateServe stale while refreshingstale-while-revalidate=600
VaryCache key (vary by header)Vary: Accept-Encoding
ETagContent version identifierETag: "abc123"
Last-ModifiedLast update timestampFor conditional requests

⚡ Part 2: Next.js ISR Configuration

ISR (Incremental Static Regeneration)

ISR combines static generation with dynamic updates.

// pages/blog/[slug].tsx
export default function BlogPost({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <time>{post.publishedDate}</time>
    </article>
  );
}

export async function getStaticProps({ params }) {
  const post = await getPostFromXmCloud(params.slug);

  return {
    props: { post },
    revalidate: 60  // ISR: revalidate every 60 seconds
  };
}

export async function getStaticPaths() {
  // Generate paths for popular posts at build time
  const paths = await getPopularPosts();

  return {
    paths: paths.map(post => ({ params: { slug: post.slug } })),
    fallback: 'blocking'  // Generate unknown paths on-demand
  };
}

ISR Per Content Type

Different content types need different revalidation rates:

interface RevalidationConfig {
  contentType: string;
  revalidateSeconds: number;
  reason: string;
}

const revalidationConfig: RevalidationConfig[] = [
  {
    contentType: 'News Article',
    revalidateSeconds: 60,
    reason: 'Frequent updates, time-sensitive'
  },
  {
    contentType: 'Blog Post',
    revalidateSeconds: 300,
    reason: 'Updated occasionally'
  },
  {
    contentType: 'Product Details',
    revalidateSeconds: 1800,
    reason: 'Infrequently changed'
  },
  {
    contentType: 'Static Page',
    revalidateSeconds: 86400,
    reason: 'Annual updates'
  }
];

function getRevalidateTime(template: string): number {
  const config = revalidationConfig.find(c => c.contentType === template);
  return config?.revalidateSeconds ?? 300;
}

On-Demand Revalidation

Trigger revalidation immediately after publishing:

// pages/api/revalidate.ts
export default async function handler(req, res) {
  // Verify webhook is from Sitecore
  const signature = req.headers['x-sitecore-signature'];
  if (!verifySignature(req.body, signature)) {
    return res.status(401).json({ message: 'Unauthorized' });
  }

  const { itemId, path } = req.body;

  try {
    // Revalidate specific paths
    await res.revalidate(path);
    
    // Also revalidate related pages
    const relatedPaths = await getRelatedPages(itemId);
    for (const relatedPath of relatedPaths) {
      await res.revalidate(relatedPath);
    }

    return res.json({
      revalidated: true,
      paths: [path, ...relatedPaths]
    });
  } catch (error) {
    console.error('Revalidation failed:', error);
    return res.status(500).json({ message: 'Revalidation failed' });
  }
}

// Sitecore webhook configuration
// POST /api/revalidate
// Trigger on: Publish Complete
// Include: Item path, template name

🔍 Part 3: GraphQL Query Caching

Query-Level Cache Configuration

// Apollo Client cache configuration
import { InMemoryCache, HttpLink } from '@apollo/client';
import { useMemo } from 'react';

const createApolloClient = () => {
  return new ApolloClient({
    ssrMode: typeof window === 'undefined',
    link: new HttpLink({
      uri: '/api/graphql',
      credentials: 'same-origin',
      headers: {
        'sc-apikey': process.env.NEXT_PUBLIC_SITECORE_API_KEY
      }
    }),
    cache: new InMemoryCache({
      typePolicies: {
        Item: {
          keyFields: ['id'],
          fields: {
            // Cache this field for 10 minutes
            children: {
              merge(existing, incoming) {
                return incoming;
              },
              read(existing) {
                return existing;
              }
            }
          }
        },
        Query: {
          fields: {
            // Cache search results
            search: {
              merge(existing = {}, incoming) {
                return { ...existing, ...incoming };
              }
            }
          }
        }
      }
    })
  });
};

Query Complexity Optimization

# INEFFICIENT: Over-fetching nested data
query GetPageWithAllData {
  item(path: "/home") {
    id
    name
    fields {
      name
      value
    }
    children {
      id
      name
      children {
        id
        name
        children {  # Too deep!
          id
          name
        }
      }
    }
  }
}

# EFFICIENT: Fetch only needed data
query GetPageWithOptimizedFields {
  item(path: "/home") {
    id
    name
    title: field(name: "Title") {
      value
    }
    description: field(name: "Description") {
      value
    }
  }
}

# EFFICIENT: Separate queries for different concerns
fragment PageFields on Item {
  id
  name
  title: field(name: "Title") { value }
}

fragment ChildrenSummary on Item {
  id
  name
  url
}

query GetPage {
  item(path: "/home") {
    ...PageFields
    children(first: 10) {
      ...ChildrenSummary
    }
  }
}

🔄 Part 4: Cache Invalidation Patterns

Pattern 1: Time-Based Invalidation

// Simple: Set TTL and let cache expire
const config = {
  news: { ttl: 5 * 60 },        // 5 minutes
  blog: { ttl: 30 * 60 },       // 30 minutes
  product: { ttl: 2 * 60 * 60 } // 2 hours
};

Pattern 2: Event-Based Invalidation

// Listen to XM Cloud publish events
app.post('/webhooks/item-published', async (req, res) => {
  const { itemId, language, version } = req.body;

  // 1. Get item details
  const item = await xmCloud.getItem(itemId, language);

  // 2. Determine affected URLs
  const urls = await getUrlsForItem(item);

  // 3. Invalidate caches
  for (const url of urls) {
    // Invalidate CDN
    await cdn.purge(url);
    
    // Trigger ISR
    await fetch(`${process.env.SITE_URL}/api/revalidate`, {
      method: 'POST',
      body: JSON.stringify({ path: url, itemId })
    });

    // Invalidate Apollo cache
    await invalidateApolloCache(itemId);
  }

  res.status(200).json({ invalidated: urls.length });
});

Pattern 3: Smart Cache Busting

// Include version hash in URLs
const getAssetUrl = (path: string, version: string) => {
  return `${path}?v=${version}`;
};

// Sitecore item hash
const itemHash = createHash('sha256')
  .update(JSON.stringify(item))
  .digest('hex')
  .substring(0, 8);

// URLs include hash
const urls = {
  css: `/styles/main.css?v=${itemHash}`,
  js: `/scripts/app.js?v=${itemHash}`,
  json: `/api/item/${itemId}.json?v=${itemHash}`
};

// When item changes, hash changes, URLs change, browser fetches new version

🐛 Part 5: Debugging Stale Cache Issues

Identifying Cache Problems

// Middleware to detect cache issues
export async function middleware(req, res, next) {
  const cacheKey = req.url;
  const cachedAt = req.headers['x-cache-date'];
  const now = Date.now();
  
  // Check if content is stale
  if (cachedAt && (now - parseInt(cachedAt)) > 300000) {
    // More than 5 minutes old
    console.warn(`Stale content served: ${cacheKey}`);
  }

  // Add diagnostic headers
  res.setHeader('X-Cache-Date', now.toString());
  res.setHeader('X-Cache-Status', 'HIT/MISS');
  res.setHeader('X-Cache-Age', (now - parseInt(cachedAt || 0)).toString());

  next();
}

Browser DevTools Debugging

// Log cache status in browser console
fetch('/api/page', {
  headers: {
    'Accept-Encoding': 'gzip',
    'User-Agent': navigator.userAgent
  }
}).then(response => {
  console.log('Cache Headers:', {
    cacheControl: response.headers.get('cache-control'),
    etag: response.headers.get('etag'),
    lastModified: response.headers.get('last-modified'),
    age: response.headers.get('age'),  // CDN age
    via: response.headers.get('via')   // CDN provider
  });
});

Troubleshooting Matrix

SymptomCauseSolution
Content always fresh but slowCache disabledCheck Cache-Control headers
Stale content shownTTL too longReduce s-maxage, set revalidate
"Always different" responsesCache key issuesCheck Vary headers
High latency spikesCache miss stormsEnable stale-while-revalidate
Users see different contentVary header missingAdd Vary: Accept-Encoding

🎯 Conclusion

Sub-second response times require coordinated caching across multiple layers. Start with ISR and CDN cache headers, then add sophisticated invalidation as needed. Monitor continuously and adjust TTLs based on real user data.


Related Posts

Leveraging Sitecore Search SDK Starter Kit into Your Sitecore Next.js Solution [Part 2]

In this blog post, I'll walk you through the process of smoothly leveraging the Search SDK Starter Kit into a Sitecore Next.js solution. ## Prerequisites Before proceeding, ensure the following prer

Read More

Sitecore Search SDK Integration [Part 1]

In today's web development landscape, delivering efficient and intuitive search functionality is crucial to provide an enhanced user experience for your visitors. With the advent of Sitecore Search,

Read More

Experience Edge Caching - Advanced Strategies for Sub-Second Response Times

Experience Edge is Sitecore's global CDN, but caching strategy makes the difference between millisecond response times and multi-second waits. Many implementations treat caching as an afterthought, r

Read More