Back to Blog
PerformanceJuly 1, 20264 min read

Predictive Cache Invalidation: Using ETags and Service Workers

Master predictive cache invalidation by combining ETag validation with Service Workers. Keep your frontend data fresh and lightning-fast without manual purges.

cache invalidationservice workersETagweb performancefrontend architectureweb developmentPerformanceWeb VitalsFrontend

We’ve all been there: a critical hotfix pushed to production, but the user is still staring at the stale, broken version because their browser is stubbornly holding onto the old bundle. Relying on simple TTLs is often a recipe for either serving stale content or forcing unnecessary network requests.

To solve this, I started moving toward predictive cache invalidation patterns. Instead of guessing how long a resource stays "fresh," I let the server tell the client exactly when to discard it. By combining ETag-based validation with Service Worker background updates, I’ve managed to keep UI state consistent while keeping load times under 300ms for repeat visitors.

The Problem with TTL-only Strategies

Early in my career, I defaulted to Cache-Control: max-age=3600. It was simple, but it was flawed. If I pushed an emergency patch, users were stuck with the old code for an hour. If I set the TTL too low, I wasted bandwidth and kept the main thread busy re-validating resources that hadn't changed.

I tried using Cache-Control: no-cache to force a check every time, but that caused a visible latency spike—about 150ms—on every navigation. I needed a way to check for changes without downloading the entire payload if the data was still valid. That’s when I turned to ETags.

Leveraging ETag Validation

An ETag is essentially a version fingerprint. When a browser sends a request with an If-None-Match header, the server compares the provided ETag with its current version. If they match, the server returns a 304 Not Modified status. The payload is empty, the transfer is tiny, and the browser simply pulls the resource from its local disk cache.

Here is how I structure the request in a Service Worker:

JAVASCRIPT
self.addEventListener(CE9178">'fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      if (cachedResponse) {
        // Perform a background revalidation
        fetch(event.request, {
          headers: { CE9178">'If-None-Match': cachedResponse.headers.get(CE9178">'ETag') }
        }).then((networkResponse) => {
          if (networkResponse.status === 200) {
            const cache = caches.open(CE9178">'v1');
            cache.put(event.request, networkResponse);
          }
        });
        return cachedResponse;
      }
      return fetch(event.request);
    })
  );
});

This approach is a massive win for web performance. The user gets an instant response from the cache, while the Service Worker silently updates the background storage if the ETag has changed.

Integrating Service Workers for Background Updates

The beauty of this pattern is that it separates the "display" from the "sync." When the user revisits, the UI updates instantly. If the background fetch detects a new ETag, the cache is updated for the next visit.

However, be careful. If you're updating core application bundles, a background update isn't enough; you need to notify the user. I usually hook this into a simple event emitter:

JAVASCRIPT
// Inside the Service Worker
if (networkResponse.status === 200) {
  cache.put(event.request, networkResponse);
  self.clients.matchAll().then(clients => {
    clients.forEach(client => client.postMessage({ type: CE9178">'UPDATE_AVAILABLE' }));
  });
}

Comparing Invalidation Patterns

When designing your architecture, you have to choose the right tool for the job. Here is how ETag-based predictive invalidation stacks up against other common patterns:

StrategyLatency ImpactBandwidthComplexity
Simple TTLLowLowVery Low
ETag ValidationMediumVery LowMedium
Purge-TagsLowHighHigh
SW BackgroundNear ZeroLowHigh

If you're dealing with edge-side concerns, you might want to look at how Cache Invalidation Strategies: Syncing Edge Content with Purge-Tags handles invalidation at the CDN level. It's often complementary to what you're doing in the browser.

Trade-offs and Gotchas

This isn't a silver bullet. ETags rely on your server's ability to calculate hashes quickly. If you have a high-traffic API and your server calculates ETags by reading the entire file from disk on every request, you’re just moving the bottleneck from the network to the CPU. Always cache your ETags in memory if possible.

Also, be aware of the Cache Storage API and Resource Timing API: Mastering Resource Lifecycle when debugging. Sometimes the browser's disk cache and the Cache Storage API interact in ways that aren't immediately obvious. I’ve spent more than one afternoon chasing down why a 304 response wasn't updating my Service Worker cache because of conflicting Vary headers.

Final Thoughts

Predictive cache invalidation is about trade-offs. You're trading a bit of background CPU and network activity for a much snappier user experience. Is it overkill for a simple landing page? Probably. But for a complex dashboard or a PWA, it’s often the difference between a "laggy" feel and a "native" feel.

Next time, I'm planning to experiment with stale-while-revalidate directives directly in the Cache-Control header to see if I can offload some of this logic to the browser engine itself, potentially simplifying the Service Worker code even further. For now, the ETag-plus-Service-Worker combo is the most reliable way I've found to keep things fresh.

Similar Posts