Design application and edge caches: key design, TTLs, invalidation, stampede protection, and consistency. Use when caching, cache invalidation, 缓存, Redis/Memcached, CDN cache keys, TTL, thundering herd, or cache-aside patterns. Not for web cache deception/poisoning assessment (see web-cache-deception).
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Design application and edge caches: key design, TTLs, invalidation, stampede protection, and consistency. Use when caching, cache invalidation, 缓存, Redis/Memcached, CDN cache keys, TTL, thundering herd, or cache-aside patterns. Not for web cache deception/poisoning assessment (see web-cache-deception).
Caching Strategies
Engineering design for correct, operable caches: what is stored, under which
key, for how long, how it is invalidated, and how concurrent misses stay safe.
Covers in-process, distributed (Redis/Memcached), and HTTP/CDN layers when you
own the cache policy. Prefer the repo’s existing cache libraries and key
conventions over inventing a second scheme.
Use When
Adding or changing application caches (memory, Redis, Memcached, local L1/L2)
Designing cache keys, namespaces, versioning, and multi-tenant isolation
Choosing TTLs, soft/hard expiry, stale-while-revalidate, or refresh-ahead
Multi-layer layout: L1 process → L2 Redis → L3 CDN; who owns what and
whether write paths touch every layer
Auth and privacy: never cache personalized or secret responses under
public/shared keys; prefer private / no-store where required
Observability: existing metrics for hit rate, latency, eviction, errors;
dashboards and alerts to extend rather than replace
Neighboring code: copy 2–3 mature services’ get-or-load, stampede, and
invalidation patterns before inventing new abstractions
Precedence: If repo rules conflict with defaults below, follow the repo.
Surface conflicts that risk cross-tenant data, unbounded memory growth, or
indefinite stale reads after writes.
Workflow
State the freshness and correctness contract.
What may be stale, and for how long (business max lag)
What must never be served stale (authz decisions, balances, one-time tokens)
Single-key vs multi-object consistency requirements
Failure mode when cache is down: fail open (hit origin) vs fail closed
Choose a pattern that matches write/read ratio.
Pattern
When
Notes
Cache-aside (lazy load)
Read-heavy, origin is source of truth
App loads on miss; invalidate or short TTL on write
Read-through
Library/proxy loads for you
Same correctness rules; centralize loader
Write-through
Must keep cache warm on write
Higher write latency; simpler read path
Write-behind
High write volume, can tolerate lag
Needs durability queue; careful crash semantics
Refresh-ahead / SWR
Predictable hot keys
Serve stale briefly while one revalidation runs
Design keys deliberately (see Key Design).
Pick TTLs and hard bounds (see TTL Design).
Plan invalidation for every write path that changes cached data.
Stampede-protect hot keys (singleflight, locking, probabilistic early expiry).
Bound memory and cardinality — no unbounded key growth from user input.
Observe, test, and document hit rates, stale windows, and purge procedures.
Key Design
Rule
Practice
Stable identity
Key from canonical ids, not display names or unordered maps
Namespace
svc:env:domain:… or repo-standard prefix; avoid collisions across services
Version / generation
Include schema or generation token so deploy can wipe logical space
Tenant isolation
Always include tenant/org id when data is tenant-scoped
No secrets in keys
Tokens, passwords, raw PII must not appear in key strings or Redis KEYS scans
Normalize inputs
Sorted query parts, lowercased where case-insensitive, explicit defaults
Vary correctly (HTTP)
Everything that changes the body must be in the cache key or Vary
Avoid huge keys
Hash long natural keys; keep debug mapping out of hot path if needed
Good key sketch:orders:v2:t{tenantId}:order:{orderId} Bad key sketch:order-cache: + raw user JSON dump or unvalidated path segments
TTL Design
Concern
Guidance
Upper bound
TTL ≤ business-acceptable staleness for that resource class
Negative caching
Cache “not found” briefly only; shorter TTL; never cache auth failures long
Jitter
Add small random jitter to aligned TTLs so many keys do not expire together
Soft vs hard
Soft-expire: serve stale + revalidate; hard-expire: must reload before serve
Zero / infinite
Avoid infinite TTL unless invalidation is proven complete for every write
Clock skew
Prefer relative TTL from cache server; document absolute-expiry edge cases
Config
Prefer configurable TTLs per resource type over magic numbers in deep call sites
Invalidation
Prefer explicit delete or version bump on write over “wait for TTL only”
when users can observe their own writes.
Invalidate the right set: primary key + secondary indexes + list/aggregate
keys that embed the entity (or use tags/generations to bulk-invalidate).
Order of operations (cache-aside):
Write origin first, then invalidate cache (not update-then-write-origin
with stale fill race unaddressed).
On race (stale fill after write): short TTL, version checks, or
compare-and-set with content version from origin.
Multi-layer: purge L3 CDN and L2 Redis when both can serve the path;
document who triggers which purge.
Never rely on KEYS * in production for purge; use tracked sets, tags,
or generation counters.
When a hot key expires or is invalidated, many concurrent requests may hit origin.
Technique
Mechanism
Singleflight / request coalescing
One in-flight load per key; others wait for the same future
Distributed lock
Short lock around reload; waiters read through or serve stale
Probabilistic early recompute
XFetch-style: revalidate before hard expiry with probability rising near TTL
Stale-while-revalidate
Serve last value while background refresh runs (if stale is allowed)
TTL jitter
Reduce synchronized expiry across the keyspace
Always bound waiters, honor cancellation, and fall back if the loader fails
(see async-concurrency-patterns for structured wait and cancel).
Consistency And Safety
Authz: Cache after authorization, or include principal/role in the key
when responses are principal-specific. Prefer not caching per-user secrets.
PII / secrets: Encrypt at rest if required; restrict who can GET keys;
never log full cache values with secrets.
Poisoning defense (defensive eng): Do not key only on untrusted Host/path
without normalization; set explicit Cache-Control on sensitive responses.
Null/poison values: Do not cache thrown errors as immortal empty hits.
Partial failure: If origin succeeds but cache set fails, prefer serving the
fresh origin result and metric the set failure — do not fail the user unless
policy requires cache durability.
Good / Bad Examples
Cache-aside with invalidation
Good
// Sketch: load from origin on miss; invalidate on writeasyncfunctiongetOrder(tenantId: string, orderId: string): Promise<Order> {
const key = `orders:v2:t${tenantId}:order:${orderId}`;
const hit = await cache.get(key);
if (hit) returndecode(hit);
const order = await db.orders.find(tenantId, orderId);
await cache.set(key, encode(order), { ttlSeconds: 60 + jitter(10) });
return order;
}
asyncfunctionupdateOrder(tenantId: string, orderId: string, patch: Patch) {
await db.orders.update(tenantId, orderId, patch); // origin firstawait cache.del(`orders:v2:t${tenantId}:order:${orderId}`);
// also invalidate list/aggregate keys or bump generation
}
Bad — write cache then DB; or never invalidate:
await cache.set(key, newValue, { ttlSeconds: 86400 });
await db.orders.update(...); // if this fails, cache lies// readers may see old DB on later miss after wrong ordering
order:ord_123 # missing tenant → cross-tenant risk if ids collide or leak
profile:Alice # display name, not stable id
Stampede protection (singleflight)
Good
// One load per key; concurrent callers share the resultvar group singleflight.Group
funcGetUser(ctx context.Context, id string) (*User, error) {
v, err, _ := group.Do("user:"+id, func() (interface{}, error) {
return loadUserFromDB(ctx, id)
})
if err != nil {
returnnil, err
}
return v.(*User), nil
}
Bad — every concurrent miss hits DB:
if val, ok := cache.Get(key); ok {
return val, nil
}
return loadUserFromDB(ctx, id) // N parallel loads for one hot key
HTTP / CDN cacheability
Good
HTTP/1.1 200 OK
Cache-Control: public, max-age=60, stale-while-revalidate=30
Vary: Accept-Encoding
# Body is identical for all users; no Set-Cookie; no Authorization variance
Bad
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
Set-Cookie: session=…
# Personalized body cached under a shared URL key
Negative caching
Good — short TTL on 404 for known-stable missing ids; re-check soon.
Bad — cache “user not found” for 24h after a race where the user was just created.
Anti-Patterns
Unbounded keys from raw user input (path, query, body hash without limits)
Omitting tenant or principal from keys for non-public data
Infinite TTL with incomplete invalidation coverage
Synchronized expiry of millions of keys at the same second (no jitter)
Updating cache value on write without versioning while concurrent loaders
can re-fill stale origin snapshots
Caching errors, empty bodies, or auth challenges as long-lived hits
Using production FLUSHALL / broad KEYS as the normal invalidation strategy
Treating CDN “static rule by extension” as safe for HTML account pages
(engineering: fix policy; assessment: web-cache-deception)
L1 process cache without generation bump on deploy when shape changes
Silent cache-aside that hides origin outages without metrics/alerts