بنقرة واحدة
api-caching-strategies
Application-level caching strategies, HTTP caching, cache invalidation, and stampede prevention
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Application-level caching strategies, HTTP caching, cache invalidation, and stampede prevention
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
GraphQL API server with Apollo Server — schema, resolvers, context, error handling, data sources, plugins
GraphQL server for Fastify with Mercurius — loaders, subscriptions, federation, JIT compilation
GraphQL Yoga v5 server, Envelop plugins, subscriptions, error masking
Webhook patterns — receiving, sending, signature verification, and retry logic
| name | api-caching-strategies |
| description | Application-level caching strategies, HTTP caching, cache invalidation, and stampede prevention |
Quick Guide: Choose the right caching strategy for your use case: cache-aside for read-heavy data, write-through for consistency, write-behind for write-heavy workloads. Always set TTL to prevent stale data and memory exhaustion. Use HTTP caching headers (Cache-Control, ETag, Last-Modified) for API responses. Prevent cache stampedes with locking or request coalescing. Measure cache hit rates before and after -- caching without metrics is guessing.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST set TTL on ALL cached data -- cache without TTL leads to memory exhaustion and infinitely stale data)
(You MUST use namespaced cache keys with a consistent prefix -- generic keys cause collisions across data types)
(You MUST invalidate or update cache entries on writes -- serving stale data after mutation breaks user trust)
(You MUST implement stampede prevention (locking or coalescing) for high-traffic cache keys -- concurrent misses can overwhelm your data source)
</critical_requirements>
Auto-detection: caching, cache-aside, write-through, write-behind, cache invalidation, TTL, Cache-Control, ETag, Last-Modified, stale-while-revalidate, s-maxage, cache stampede, thundering herd, in-memory cache, LRU cache, distributed cache, cache key, cache miss, cache hit, CDN caching, HTTP caching, conditional request, 304 Not Modified
When to use:
When NOT to use:
Key patterns covered:
Caching trades freshness for speed. Every caching decision is a consistency vs performance trade-off -- understand where your use case falls on that spectrum before choosing a strategy.
The three questions before adding caching:
Caching is a layered system. HTTP caching (browser and CDN) reduces requests to your server. Application-level caching (in-memory or distributed) reduces requests to your database. Apply caching at the right layer for the problem.
When to use caching:
When NOT to use caching:
The most common application-level caching pattern. The application checks the cache first, fetches from the data source on miss, and stores the result with a TTL.
const CACHE_TTL_SECONDS = 300;
const CACHE_PREFIX = "app:user";
async function getUserById(userId: string): Promise<User | null> {
const cacheKey = `${CACHE_PREFIX}:${userId}`;
const cached = await cacheStore.get(cacheKey);
if (cached) return JSON.parse(cached) as User;
const user = await db.query.users.findFirst({ where: eq(users.id, userId) });
if (!user) return null;
await cacheStore.set(cacheKey, JSON.stringify(user), { ttl: CACHE_TTL_SECONDS });
return user;
}
Why good: TTL prevents stale data, namespaced keys prevent collisions, early return on cache hit, cache is populated lazily (only data that is actually requested gets cached)
// BAD: No TTL, generic key
async function getUser(id: string) {
const cached = await cacheStore.get(id); // No prefix -- collides with other data types
if (cached) return JSON.parse(cached);
const user = await db.query.users.findFirst({ where: eq(users.id, id) });
await cacheStore.set(id, JSON.stringify(user)); // No TTL -- never expires
return user;
}
Why bad: No TTL means infinite staleness and eventual memory exhaustion, generic key collides with other entity types using the same ID format
When to use: Read-heavy endpoints where data changes infrequently relative to reads.
See examples/core.md for generic cache wrapper and error handling patterns.
Update both cache and data source on every write. Guarantees read-after-write consistency without waiting for TTL expiration.
const CACHE_TTL_SECONDS = 300;
async function updateUser(userId: string, updates: Partial<User>): Promise<User> {
const updatedUser = await db.update(users)
.set({ ...updates, updatedAt: new Date() })
.where(eq(users.id, userId))
.returning();
const cacheKey = `${CACHE_PREFIX}:${userId}`;
await cacheStore.set(cacheKey, JSON.stringify(updatedUser[0]), {
ttl: CACHE_TTL_SECONDS,
});
return updatedUser[0];
}
Why good: Cache always reflects latest database state, no stale reads after updates
When to use: Data that is read frequently after writes and must be consistent. When not to use: Write-heavy workloads where the overhead of updating cache on every write is too expensive.
See examples/core.md for write-through with delete invalidation.
Set appropriate Cache-Control, ETag, and Last-Modified headers on API responses to leverage browser and CDN caching. This reduces requests to your server entirely.
// API endpoint setting caching headers
function setCacheHeaders(
res: Response,
body: unknown,
options: { maxAge: number; isPublic: boolean },
) {
const etag = generateETag(body);
const scope = options.isPublic ? "public" : "private";
res.setHeader("Cache-Control", `${scope}, max-age=${options.maxAge}, must-revalidate`);
res.setHeader("ETag", etag);
res.setHeader("Last-Modified", new Date().toUTCString());
}
Key header combinations for APIs:
| Use Case | Cache-Control | Why |
|---|---|---|
| Public list data | public, max-age=60, s-maxage=300 | CDN caches longer than browser |
| User-specific data | private, max-age=0, must-revalidate | No shared cache, always revalidate |
| Sensitive data | no-store, private | Never cache anywhere |
| Immutable assets | public, max-age=31536000, immutable | Version in URL, cache forever |
See examples/core.md for conditional request handling (If-None-Match / 304) and CDN patterns.
For single-process hot data that does not need to be shared across instances. Faster than a distributed cache (no network hop) but lost on process restart and not shared between workers.
import { LRUCache } from "lru-cache";
const MAX_ITEMS = 500;
const TTL_MS = 300_000; // 5 minutes
const configCache = new LRUCache<string, AppConfig>({
max: MAX_ITEMS,
ttl: TTL_MS,
});
function getConfig(key: string): AppConfig | undefined {
return configCache.get(key);
}
function setConfig(key: string, value: AppConfig): void {
configCache.set(key, value);
}
Why good: No serialization overhead (stores objects directly), automatic eviction of least-recently-used items, TTL prevents staleness
When to use: Configuration, feature flags, frequently accessed reference data in single-process applications. When not to use: Data that must be shared across multiple server instances (use a distributed cache instead).
See examples/core.md for LRU cache with size tracking and fetch method.
Consistent, namespaced cache keys prevent collisions and enable pattern-based invalidation.
const CACHE_PREFIX = "myapp";
// Entity keys: prefix:type:id
const userKey = (id: string) => `${CACHE_PREFIX}:user:${id}`;
const productKey = (id: string) => `${CACHE_PREFIX}:product:${id}`;
// Query result keys: prefix:type:list:hash
const productListKey = (filters: ProductFilters) => {
const normalized = JSON.stringify({
category: filters.category ?? "all",
page: filters.page ?? 1,
sort: filters.sort ?? "created",
});
const hash = createHash("md5").update(normalized).digest("hex");
return `${CACHE_PREFIX}:products:list:${hash}`;
};
// User-scoped keys
const userCartKey = (userId: string) => `${CACHE_PREFIX}:cart:${userId}`;
Why good: Hierarchical structure prevents collisions, hashing complex queries keeps keys short, pattern prefix enables bulk invalidation
See examples/core.md for cache key generation patterns.
Invalidation is the hardest part of caching. Choose the simplest strategy that meets your consistency requirements.
Direct invalidation -- delete the exact key on mutation:
async function updateProduct(id: string, data: ProductUpdate) {
await db.update(products).set(data).where(eq(products.id, id));
await cacheStore.del(productKey(id));
}
Tag-based invalidation -- group related keys by tag for bulk invalidation:
// Invalidate all electronics products when category changes
await invalidateByTag("category:electronics");
TTL-based expiration -- let the cache expire naturally when eventual consistency is acceptable:
const SHORT_TTL = 60; // 1 minute for frequently changing data
const MEDIUM_TTL = 300; // 5 minutes for user data
const LONG_TTL = 3600; // 1 hour for reference data
See examples/advanced.md for tag-based invalidation implementation and pattern-based key deletion.
When a popular cache key expires, many concurrent requests may all miss the cache simultaneously and flood the data source. This is a cache stampede (thundering herd).
Locking -- only one request regenerates the cache; others wait or get stale data:
async function getWithLock(key: string, fetchFn: () => Promise<string>): Promise<string> {
const cached = await cacheStore.get(key);
if (cached) return cached;
const lockKey = `lock:${key}`;
const acquired = await cacheStore.setNX(lockKey, "1", { ttl: LOCK_TTL_SECONDS });
if (!acquired) {
await delay(RETRY_DELAY_MS);
return getWithLock(key, fetchFn); // Retry after brief wait
}
try {
const value = await fetchFn();
await cacheStore.set(key, value, { ttl: CACHE_TTL_SECONDS });
return value;
} finally {
await cacheStore.del(lockKey);
}
}
Why good: Only one request hits the data source, others wait briefly, lock auto-expires if holder crashes
See examples/advanced.md for request coalescing (singleflight) and probabilistic early recomputation.
Detailed Resources:
<decision_framework>
Is data read much more than written?
+-- YES --> Can you tolerate staleness?
| +-- YES --> Cache-aside with TTL
| +-- NO --> Write-through (consistent reads)
+-- NO --> Is write latency critical?
+-- YES --> Write-behind (async persist)
+-- NO --> Write-through or skip caching
Is the response public (same for all users)?
+-- YES --> HTTP caching (Cache-Control: public, s-maxage)
| CDN handles it, your server may never see the request
+-- NO --> Is it shared across server instances?
+-- YES --> Distributed cache (external store)
+-- NO --> Is it hot data in a single process?
+-- YES --> In-memory LRU cache
+-- NO --> Cache-aside with distributed store
| Data Type | Recommended TTL | Rationale |
|---|---|---|
| Static config / feature flags | 3600s+ (1h+) | Rarely changes |
| Product catalog / listings | 300-3600s (5m-1h) | Infrequent updates |
| User profile | 60-300s (1-5m) | Changes occasionally |
| Search results | 30-60s (30s-1m) | Balance freshness vs performance |
| Real-time data (prices, stock) | 5-30s | Must be near-fresh |
| Session data | 86400s (24h) | Long-lived by design |
</decision_framework>
<red_flags>
High Priority Issues:
Medium Priority Issues:
private directive -- leaks personal data to other usersno-cache confused with no-store -- no-cache still stores but revalidates; no-store prevents any storageGotchas & Edge Cases:
Cache-Control: no-cache does NOT prevent caching -- it forces revalidation before every use. Use no-store to prevent storage entirely.s-maxage overrides max-age for shared caches (CDNs) only -- browsers ignore itstale-while-revalidate lets CDNs serve expired content while refreshing in the background -- great for availability, but means users briefly see stale dataJSON.stringify for cache serialization drops undefined values and converts Date objects to strings -- use a structured serializer if neededJSON.stringify varies across engines; sort keys or use a canonical serializer</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST set TTL on ALL cached data -- cache without TTL leads to memory exhaustion and infinitely stale data)
(You MUST use namespaced cache keys with a consistent prefix -- generic keys cause collisions across data types)
(You MUST invalidate or update cache entries on writes -- serving stale data after mutation breaks user trust)
(You MUST implement stampede prevention (locking or coalescing) for high-traffic cache keys -- concurrent misses can overwhelm your data source)
Failure to follow these rules will cause stale data bugs, cache key collisions, memory exhaustion, and data source overload during traffic spikes.
</critical_reminders>