Skip to content
Back to skills

Cache Invalidation

ASecurity

Cache invalidation — TTL, event-driven, cache-aside, write-through, SWR, tag purge

  • 2 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added October 26, 2026
developmenttypescriptgonextjsapidatabasebackendperformance

Works with

  • api

Security analysis

A100/100

Scanned October 26, 2026

npx -y skills add nhan03bit/ultrathink --skill cache-invalidation --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Cache Invalidation?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Cache Invalidation
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nhan03bit-cache-invalidation/badge)](https://www.skillsdirectory.com/skills/nhan03bit-cache-invalidation)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: cache-invalidation
description: Cache invalidation — TTL, event-driven, cache-aside, write-through, SWR, tag purge
layer: domain
category: backend
triggers:
  - cache invalidation
  - stale cache
  - cache busting
  - cache aside
  - write through
  - stale while revalidate
  - cache purge
  - tag-based invalidation
linksTo:
  - caching
  - redis
  - api-caching
linkedFrom:
  - caching
  - redis
riskLevel: medium
---

# Cache Invalidation

## Overview

Cache invalidation is the process of removing or updating stale cached data when the underlying source of truth changes. It is famously one of the two hard problems in computer science. The goal is to balance freshness (serving current data) against performance (avoiding unnecessary origin fetches).

## When to Use

- Any system with a caching layer (Redis, CDN, in-memory, browser)
- APIs where data freshness requirements vary by endpoint
- E-commerce catalogs, CMS content, user profiles — data that changes but not on every request
- Multi-tier caches (browser -> CDN -> app cache -> database)

## Key Patterns

### Cache-Aside (Lazy Loading)

Application checks cache first; on miss, reads from DB and populates cache.

```typescript
async function getProduct(id: string): Promise<Product> {
  const cacheKey = `product:${id}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const product = await db.query('SELECT * FROM products WHERE id = $1', [id]);
  await redis.setex(cacheKey, 3600, JSON.stringify(product)); // 1h TTL
  return product;
}

async function updateProduct(id: string, data: Partial<Product>) {
  await db.query('UPDATE products SET ... WHERE id = $1', [id]);
  await redis.del(`product:${id}`); // invalidate — next read repopulates
}
```

### Write-Through

Write to cache and DB simultaneously. Cache is always fresh but writes are slower.

```typescript
async function updateProduct(id: string, data: Partial<Product>) {
  const updated = await db.query(
    'UPDATE products SET ... WHERE id = $1 RETURNING *', [id]
  );
  await redis.setex(`product:${id}`, 3600, JSON.stringify(updated));
  return updated;
}
```

### Event-Driven Invalidation

Decouple invalidation from the write path using events.

```typescript
// Publisher (in the write service)
await db.query('UPDATE products SET price = $1 WHERE id = $2', [newPrice, id]);
await eventBus.publish('product.updated', { id, fields: ['price'] });

// Subscriber (cache invalidation worker)
eventBus.subscribe('product.updated', async (event) => {
  await redis.del(`product:${event.id}`);
  await cdn.purgeTag(`product-${event.id}`);
});
```

### Tag-Based Purging (CDN / Vercel)

```typescript
// Set tags when caching
// Next.js revalidateTag example
import { revalidateTag } from 'next/cache';

// In a fetch call
fetch('https://api.example.com/products', {
  next: { tags: ['products', `category-${categoryId}`] },
});

// On mutation — purge all caches with this tag
export async function updateCategory(id: string) {
  await db.updateCategory(id);
  revalidateTag(`category-${id}`);
}
```

### Stale-While-Revalidate (SWR)

Serve stale data immediately while refreshing in the background.

```typescript
// HTTP header approach
// Cache-Control: public, max-age=60, stale-while-revalidate=300

// Application-level SWR
async function getWithSWR<T>(key: string, fetcher: () => Promise<T>, ttl: number, swrWindow: number) {
  const entry = await redis.hgetall(`swr:${key}`);

  if (entry.data) {
    const age = Date.now() - Number(entry.timestamp);
    if (age < ttl * 1000) return JSON.parse(entry.data); // fresh

    if (age < (ttl + swrWindow) * 1000) {
      // stale but within SWR window — return stale, refresh async
      refreshInBackground(key, fetcher, ttl);
      return JSON.parse(entry.data);
    }
  }

  // Cache miss or expired beyond SWR window
  const fresh = await fetcher();
  await redis.hset(`swr:${key}`, { data: JSON.stringify(fresh), timestamp: Date.now() });
  return fresh;
}
```

## Pitfalls

- **Delete vs. update race**: Two concurrent writes can leave stale data. Prefer delete-on-write (cache-aside) over set-on-write unless you use versioning.
- **Thundering herd**: When a popular key expires, hundreds of requests hit the DB simultaneously. Use request coalescing or a lock to let one request repopulate.
- **Multi-layer inconsistency**: Browser cache, CDN, and app cache can all hold different versions. Set consistent TTLs and use `Vary` headers correctly.
- **Forgetting related keys**: Updating a product may require invalidating the product key, the category listing, search results, and recommendation caches. Use tag-based invalidation to group related entries.
- **TTL = 0 is not invalidation**: Zero TTL means "always revalidate," not "never cache." Understand the difference between no-cache and no-store.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.