DEV Community

Cover image for Building a Zero-Downtime Cache Invalidation Pipeline with Webhooks
Alok Deep
Alok Deep

Posted on

Building a Zero-Downtime Cache Invalidation Pipeline with Webhooks

Cache invalidation is notoriously difficult to get right in high-throughput systems.

If your cache TTL is too short (e.g. 10 seconds), your database gets overwhelmed by stampedes. If your TTL is too long (e.g. 24 hours), your application serves stale data after updates.

The standard solution is event-driven cache invalidation. Whenever a mutation occurs in your CMS, e-commerce admin, or custom backend, you trigger an invalidation webhook that purges matching cache entries across all edge points of presence.

Here is how to design a resilient, zero-downtime cache invalidation pipeline with surrogate key tags, fanout workers, and retry guarantees.


1. Surrogate Key Tags (The Tagging Hierarchy)

URL-based invalidation (PURGE /api/products/linen-shirt) is brittle because a single product update might affect dozens of related endpoints:

  • The product details page (/api/products/linen-shirt)
  • The category listing (/api/collections/summer-apparel)
  • The brand page (/api/brands/linen-co)
  • The search index and homepage featured carousel

Instead of tracking every possible URL, attach surrogate key tags to your HTTP responses:

HTTP/2 200 OK
Content-Type: application/json
X-ApexCache-Tags: product:1029, collection:summer, brand:42, catalog
Enter fullscreen mode Exit fullscreen mode

The edge proxy indexes these tags in an in-memory inverted index. When a purge request arrives for product:1029, the proxy instantly evicts every cached response containing that tag across the cluster.


2. The Invalidation Webhook Architecture

Admin / CMS Mutation ──► Database Commit ──► Event Emitter (Webhook Dispatcher)
                                                   │
                                                   ▼
                                      ApexCache Invalidation API
                                                   │
                                    (Parallel Distributed Fanout)
                                                   ├── Edge PoP (US-East): Purged in 6ms
                                                   ├── Edge PoP (EU-West): Purged in 8ms
                                                   └── Edge PoP (AP-South): Purged in 9ms
Enter fullscreen mode Exit fullscreen mode

3. Implementation in Node.js / TypeScript

Here is a resilient webhook dispatcher implementing non-blocking dispatch with exponential backoff retries:

// services/cache-invalidator.ts
interface InvalidateOptions {
  tags?: string[];
  urls?: string[];
  maxRetries?: number;
}

export async function invalidateEdgeCache(options: InvalidateOptions): Promise<void> {
  const { tags = [], urls = [], maxRetries = 3 } = options;
  const apiKey = process.env.APEXCACHE_API_KEY;
  const endpoint = 'https://api.getapexcache.com/api/v1/cache/invalidate';

  const payload = JSON.stringify({ tags, urls });

  let attempt = 0;
  while (attempt < maxRetries) {
    try {
      const res = await fetch(endpoint, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json'
        },
        body: payload
      });

      if (res.ok) {
        return; // Purge successful
      }

      console.warn(`[ApexCache] Invalidation returned status ${res.status}. Retrying...`);
    } catch (err) {
      console.error(`[ApexCache] Network error during purge attempt ${attempt + 1}:`, err);
    }

    attempt++;
    // Exponential backoff: 50ms, 100ms, 200ms
    await new Promise((r) => setTimeout(r, 50 * Math.pow(2, attempt)));
  }

  console.error('[ApexCache] Failed to invalidate cache after max retries:', { tags, urls });
}
Enter fullscreen mode Exit fullscreen mode

4. Hooking Into ORMs (Prisma / Drizzle / TypeORM)

Integrate the invalidation dispatcher directly into your database middleware or lifecycle hooks:

// Prisma Middleware Example
prisma.$use(async (params, next) => {
  const result = await next(params);

  // Check if the action was a mutation
  if (['create', 'update', 'delete', 'updateMany'].includes(params.action)) {
    if (params.model === 'Product') {
      const productId = params.args.where?.id || result?.id;
      // Fire-and-forget background eviction
      invalidateEdgeCache({
        tags: [`product:${productId}`, 'catalog']
      }).catch(console.error);
    }
  }

  return result;
});
Enter fullscreen mode Exit fullscreen mode

Latency and Consistency Guarantees

  • Purge Execution Time: Tag evictions complete across global edge nodes in 6ms to 12ms.
  • Zero Downtime: While tags are being evicted, the proxy handles in-flight requests seamlessly without returning 502/504 errors.
  • Thundering Herd Shield: The next incoming request for the evicted tag triggers a single coalesced origin fetch via SingleFlight, preventing database spikes.

Conclusion

Building a zero-downtime cache invalidation pipeline requires separating cache tags from raw URLs and dispatching event-driven purges on database commits.

With tag-based surrogate keys and fast webhook dispatch, you can maintain long cache TTLs (24+ hours) while guaranteeing that updates reflect globally within milliseconds.

Top comments (0)