vanara 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/bin/vanara.js +66 -2
- package/catalog/agents/api-designer/AGENT.md +140 -0
- package/catalog/agents/api-designer/examples/openapi-snippet.yaml +130 -0
- package/catalog/agents/api-designer/examples/review-notes.md +61 -0
- package/catalog/agents/api-designer/references/contract-and-openapi.md +82 -0
- package/catalog/agents/api-designer/references/design-checklist.md +68 -0
- package/catalog/agents/api-designer/references/versioning-and-evolution.md +63 -0
- package/catalog/agents/api-designer/scripts/lint-openapi.mjs +180 -0
- package/catalog/agents/code-reviewer/AGENT.md +168 -0
- package/catalog/agents/code-reviewer/examples/pr-comment-template.md +48 -0
- package/catalog/agents/code-reviewer/examples/sample-review-output.md +38 -0
- package/catalog/agents/code-reviewer/references/review-checklist.md +46 -0
- package/catalog/agents/code-reviewer/references/security-review.md +62 -0
- package/catalog/agents/code-reviewer/references/severity-rubric.md +63 -0
- package/catalog/agents/code-reviewer/scripts/review-guard.mjs +102 -0
- package/catalog/agents/debugger/AGENT.md +112 -0
- package/catalog/agents/debugger/examples/postmortem-template.md +59 -0
- package/catalog/agents/debugger/examples/repro-template.md +50 -0
- package/catalog/agents/debugger/references/bisection.md +63 -0
- package/catalog/agents/debugger/references/debugging-method.md +57 -0
- package/catalog/agents/debugger/references/observability.md +61 -0
- package/catalog/agents/debugger/scripts/parse-stacktrace.mjs +109 -0
- package/catalog/agents/pr-summarizer/AGENT.md +156 -0
- package/catalog/agents/pr-summarizer/examples/sample-pr-summary.md +46 -0
- package/catalog/agents/pr-summarizer/examples/weak-vs-strong-summary.md +68 -0
- package/catalog/agents/pr-summarizer/references/identifying-risk.md +68 -0
- package/catalog/agents/pr-summarizer/references/reading-the-diff.md +68 -0
- package/catalog/agents/pr-summarizer/references/test-plan-and-missing-tests.md +64 -0
- package/catalog/agents/pr-summarizer/references/writing-the-summary.md +63 -0
- package/catalog/agents/pr-summarizer/scripts/diff-risk.mjs +111 -0
- package/catalog/agents/refactoring-specialist/AGENT.md +185 -0
- package/catalog/agents/refactoring-specialist/examples/before-after.md +91 -0
- package/catalog/agents/refactoring-specialist/examples/refactor-plan.md +97 -0
- package/catalog/agents/refactoring-specialist/references/large-scale-refactor.md +50 -0
- package/catalog/agents/refactoring-specialist/references/safe-workflow.md +75 -0
- package/catalog/agents/refactoring-specialist/references/smell-catalog.md +76 -0
- package/catalog/agents/refactoring-specialist/scripts/detect-smells.mjs +150 -0
- package/catalog/agents/security-auditor/AGENT.md +168 -0
- package/catalog/agents/security-auditor/examples/audit-report.md +68 -0
- package/catalog/agents/security-auditor/examples/finding-template.md +31 -0
- package/catalog/agents/security-auditor/references/audit-workflow.md +116 -0
- package/catalog/agents/security-auditor/references/severity-and-reporting.md +56 -0
- package/catalog/agents/security-auditor/references/vuln-classes.md +129 -0
- package/catalog/agents/security-auditor/scripts/scan-secrets.mjs +86 -0
- package/catalog/agents/technical-writer/AGENT.md +163 -0
- package/catalog/agents/technical-writer/examples/how-to-example.md +76 -0
- package/catalog/agents/technical-writer/examples/reference-example.md +75 -0
- package/catalog/agents/technical-writer/references/clarity-and-style.md +52 -0
- package/catalog/agents/technical-writer/references/diataxis-and-doc-types.md +61 -0
- package/catalog/agents/technical-writer/references/structure-and-maintenance.md +59 -0
- package/catalog/agents/technical-writer/scripts/readability.mjs +103 -0
- package/catalog/agents/test-author/AGENT.md +177 -0
- package/catalog/agents/test-author/examples/test-plan.md +54 -0
- package/catalog/agents/test-author/examples/unit-test-example.ts +55 -0
- package/catalog/agents/test-author/references/aaa-and-naming.md +75 -0
- package/catalog/agents/test-author/references/mocking-and-fixtures.md +94 -0
- package/catalog/agents/test-author/references/test-types-and-pyramid.md +60 -0
- package/catalog/agents/test-author/scripts/check-coverage.mjs +145 -0
- package/catalog/agents/threat-modeler/AGENT.md +163 -0
- package/catalog/agents/threat-modeler/examples/dfd-example.md +55 -0
- package/catalog/agents/threat-modeler/examples/threat-model.md +81 -0
- package/catalog/agents/threat-modeler/references/dfd-and-trust-boundaries.md +76 -0
- package/catalog/agents/threat-modeler/references/mitigation-catalog.md +77 -0
- package/catalog/agents/threat-modeler/references/stride-method.md +68 -0
- package/catalog/agents/threat-modeler/scripts/stride-checklist.mjs +183 -0
- package/catalog/agents/vuln-scanner/AGENT.md +146 -0
- package/catalog/agents/vuln-scanner/examples/finding-template.md +57 -0
- package/catalog/agents/vuln-scanner/examples/scan-report.md +49 -0
- package/catalog/agents/vuln-scanner/references/remediation-and-severity.md +68 -0
- package/catalog/agents/vuln-scanner/references/scan-types-and-tools.md +83 -0
- package/catalog/agents/vuln-scanner/references/triage-and-false-positives.md +73 -0
- package/catalog/agents/vuln-scanner/scripts/parse-scan-results.mjs +133 -0
- package/catalog/packs/code-review-pack/PACK.md +100 -0
- package/catalog/packs/code-review-pack/examples/pr-flow-example.md +50 -0
- package/catalog/packs/code-review-pack/references/rollout-guide.md +40 -0
- package/catalog/packs/security-pack/PACK.md +128 -0
- package/catalog/packs/security-pack/examples/owasp-vuln-triage-and-fix.md +66 -0
- package/catalog/packs/security-pack/examples/threat-model-then-audit.md +77 -0
- package/catalog/packs/security-pack/references/choosing-the-right-tool.md +37 -0
- package/catalog/packs/security-pack/references/threat-model-to-remediation.md +69 -0
- package/catalog/packs/security-pack/references/workflow.md +56 -0
- package/catalog/skills/api-pagination/SKILL.md +83 -0
- package/catalog/skills/api-pagination/examples/keyset-queries.sql +38 -0
- package/catalog/skills/api-pagination/examples/response-shapes.md +51 -0
- package/catalog/skills/api-pagination/references/consuming-pages.md +39 -0
- package/catalog/skills/api-pagination/references/counting-and-totals.md +41 -0
- package/catalog/skills/api-pagination/references/cursor-encoding.md +34 -0
- package/catalog/skills/api-pagination/scripts/check-cursor.mjs +51 -0
- package/catalog/skills/caching-strategies/SKILL.md +180 -0
- package/catalog/skills/caching-strategies/examples/http-cache-headers.md +82 -0
- package/catalog/skills/caching-strategies/examples/redis-cache-aside.js +110 -0
- package/catalog/skills/caching-strategies/references/cache-patterns.md +93 -0
- package/catalog/skills/caching-strategies/references/eviction-and-ttl.md +67 -0
- package/catalog/skills/caching-strategies/references/invalidation-and-stampede.md +99 -0
- package/catalog/skills/caching-strategies/scripts/ttl-jitter.mjs +99 -0
- package/catalog/skills/conventional-commits/SKILL.md +135 -0
- package/catalog/skills/conventional-commits/examples/commit-examples.md +60 -0
- package/catalog/skills/conventional-commits/examples/commitlint.config.js +31 -0
- package/catalog/skills/conventional-commits/references/breaking-changes-semver.md +50 -0
- package/catalog/skills/conventional-commits/references/scoping-commits.md +42 -0
- package/catalog/skills/conventional-commits/references/spec.md +78 -0
- package/catalog/skills/conventional-commits/scripts/lint-commit.mjs +97 -0
- package/catalog/skills/database-migrations/SKILL.md +137 -0
- package/catalog/skills/database-migrations/examples/add-column-safe.sql +34 -0
- package/catalog/skills/database-migrations/examples/migration-config.example.json +27 -0
- package/catalog/skills/database-migrations/references/expand-contract.md +74 -0
- package/catalog/skills/database-migrations/references/rollback-and-safety.md +71 -0
- package/catalog/skills/database-migrations/references/zero-downtime-changes.md +84 -0
- package/catalog/skills/database-migrations/scripts/check-migration-reversible.mjs +139 -0
- package/catalog/skills/error-handling-patterns/SKILL.md +136 -0
- package/catalog/skills/error-handling-patterns/examples/express-error-middleware.js +93 -0
- package/catalog/skills/error-handling-patterns/examples/result-pattern.ts +81 -0
- package/catalog/skills/error-handling-patterns/references/error-taxonomy.md +91 -0
- package/catalog/skills/error-handling-patterns/references/language-patterns.md +102 -0
- package/catalog/skills/error-handling-patterns/references/retry-and-backoff.md +76 -0
- package/catalog/skills/error-handling-patterns/scripts/lint-empty-catch.mjs +113 -0
- package/catalog/skills/git-collaboration-workflows/SKILL.md +104 -0
- package/catalog/skills/git-collaboration-workflows/examples/branch-protection.md +48 -0
- package/catalog/skills/git-collaboration-workflows/examples/pr-review-playbook.md +48 -0
- package/catalog/skills/git-collaboration-workflows/references/branching-models.md +52 -0
- package/catalog/skills/git-collaboration-workflows/references/history-hygiene.md +44 -0
- package/catalog/skills/git-collaboration-workflows/scripts/check-branch-hygiene.mjs +62 -0
- package/catalog/skills/owasp-top10/SKILL.md +112 -0
- package/catalog/skills/owasp-top10/examples/sql-injection-fix.md +33 -0
- package/catalog/skills/owasp-top10/examples/xss-fix.md +27 -0
- package/catalog/skills/owasp-top10/references/access-control.md +36 -0
- package/catalog/skills/owasp-top10/references/crypto-failures.md +29 -0
- package/catalog/skills/owasp-top10/references/injection.md +39 -0
- package/catalog/skills/owasp-top10/references/ssrf-and-design.md +35 -0
- package/catalog/skills/owasp-top10/scripts/scan-injection.mjs +73 -0
- package/catalog/skills/prompt-engineering/SKILL.md +100 -0
- package/catalog/skills/prompt-engineering/examples/classification-prompt.md +29 -0
- package/catalog/skills/prompt-engineering/examples/extraction-prompt.md +25 -0
- package/catalog/skills/prompt-engineering/references/prompt-injection.md +34 -0
- package/catalog/skills/prompt-engineering/references/structured-output.md +34 -0
- package/catalog/skills/prompt-engineering/references/techniques.md +33 -0
- package/catalog/skills/prompt-engineering/scripts/validate-output.mjs +71 -0
- package/catalog/skills/readme-writing/SKILL.md +176 -0
- package/catalog/skills/readme-writing/examples/README.template.md +73 -0
- package/catalog/skills/readme-writing/examples/good-readme-example.md +95 -0
- package/catalog/skills/readme-writing/references/anatomy.md +107 -0
- package/catalog/skills/readme-writing/references/maintenance.md +68 -0
- package/catalog/skills/readme-writing/references/writing-style.md +79 -0
- package/catalog/skills/readme-writing/scripts/lint-readme.mjs +117 -0
- package/catalog/skills/refactoring-patterns/SKILL.md +178 -0
- package/catalog/skills/refactoring-patterns/examples/extract-function.before-after.js +57 -0
- package/catalog/skills/refactoring-patterns/examples/replace-conditional-with-polymorphism.before-after.ts +63 -0
- package/catalog/skills/refactoring-patterns/references/code-smells.md +92 -0
- package/catalog/skills/refactoring-patterns/references/refactoring-catalog.md +78 -0
- package/catalog/skills/refactoring-patterns/references/safe-workflow.md +74 -0
- package/catalog/skills/refactoring-patterns/scripts/detect-smells.mjs +137 -0
- package/catalog/skills/rest-api-design/SKILL.md +147 -0
- package/catalog/skills/rest-api-design/examples/error-envelope.json +26 -0
- package/catalog/skills/rest-api-design/examples/orders-api.openapi.yaml +106 -0
- package/catalog/skills/rest-api-design/references/error-handling.md +43 -0
- package/catalog/skills/rest-api-design/references/pagination.md +41 -0
- package/catalog/skills/rest-api-design/references/status-codes.md +32 -0
- package/catalog/skills/rest-api-design/references/versioning.md +31 -0
- package/catalog/skills/rest-api-design/scripts/check-envelope.mjs +62 -0
- package/catalog/skills/secrets-management/SKILL.md +134 -0
- package/catalog/skills/secrets-management/examples/env-example.md +41 -0
- package/catalog/skills/secrets-management/examples/rotation-runbook.md +50 -0
- package/catalog/skills/secrets-management/references/leak-response.md +47 -0
- package/catalog/skills/secrets-management/references/rotation.md +46 -0
- package/catalog/skills/secrets-management/references/secret-managers.md +52 -0
- package/catalog/skills/secrets-management/scripts/detect-hardcoded.mjs +96 -0
- package/catalog/skills/secure-auth/SKILL.md +135 -0
- package/catalog/skills/secure-auth/examples/auth-flow.md +60 -0
- package/catalog/skills/secure-auth/examples/jwt-verify.ts +89 -0
- package/catalog/skills/secure-auth/references/auth-attacks.md +76 -0
- package/catalog/skills/secure-auth/references/oauth2-oidc.md +65 -0
- package/catalog/skills/secure-auth/references/password-hashing.md +70 -0
- package/catalog/skills/secure-auth/references/sessions-vs-jwt.md +69 -0
- package/catalog/skills/secure-auth/scripts/check-password-policy.mjs +114 -0
- package/catalog/skills/sql-index-tuning/SKILL.md +171 -0
- package/catalog/skills/sql-index-tuning/examples/explain-walkthrough.sql +91 -0
- package/catalog/skills/sql-index-tuning/examples/index-ddl.sql +67 -0
- package/catalog/skills/sql-index-tuning/references/btree-internals.md +58 -0
- package/catalog/skills/sql-index-tuning/references/composite-and-covering.md +60 -0
- package/catalog/skills/sql-index-tuning/references/reading-explain.md +54 -0
- package/catalog/skills/sql-index-tuning/references/selectivity-and-cardinality.md +62 -0
- package/catalog/skills/sql-index-tuning/scripts/suggest-index.mjs +213 -0
- package/catalog/skills/test-plan-design/SKILL.md +154 -0
- package/catalog/skills/test-plan-design/examples/test-case-template.md +45 -0
- package/catalog/skills/test-plan-design/examples/test-plan.md +57 -0
- package/catalog/skills/test-plan-design/references/case-design.md +79 -0
- package/catalog/skills/test-plan-design/references/risk-prioritization.md +58 -0
- package/catalog/skills/test-plan-design/references/test-pyramid.md +58 -0
- package/catalog/skills/test-plan-design/scripts/coverage-gaps.mjs +109 -0
- package/free-tier.json +15 -0
- package/package.json +15 -4
- package/premium-index.json +1 -0
- package/src/config.js +31 -1
- package/src/license.js +143 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caching-strategies
|
|
3
|
+
description: Deep reference for caching — what to cache, cache-aside vs read/write-through/write-behind, TTLs with jitter, eviction (LRU/LFU/FIFO), invalidation, and surviving stampedes (thundering herd / dogpile). Worked examples and a runnable jitter check.
|
|
4
|
+
type: skill
|
|
5
|
+
version: 2.0.0
|
|
6
|
+
updated: 2026-06-29
|
|
7
|
+
---
|
|
8
|
+
# Caching Strategies
|
|
9
|
+
|
|
10
|
+
Caching is the cheapest order-of-magnitude performance win available, and the fastest way to serve
|
|
11
|
+
*confidently wrong* data. The hard part was never reading from a cache — it's **invalidation**,
|
|
12
|
+
**consistency under concurrency**, and **what happens the moment the cache is cold or wrong**. This
|
|
13
|
+
skill is the deep reference: the patterns, the decisions, the trade-offs, and the failure modes that
|
|
14
|
+
page you at 3am. Heavy detail lives in `references/`; copy-paste material in `examples/`; a runnable
|
|
15
|
+
check in `scripts/`.
|
|
16
|
+
|
|
17
|
+
## Mental model
|
|
18
|
+
|
|
19
|
+
A cache is a **bet**: you trade memory and a correctness risk for latency and load reduction. Every
|
|
20
|
+
caching decision is answering four questions, in order:
|
|
21
|
+
|
|
22
|
+
| Question | What it decides |
|
|
23
|
+
|---|---|
|
|
24
|
+
| What is hot and tolerant of staleness? | *whether* to cache at all |
|
|
25
|
+
| How does data get *into* the cache? | the **read/write pattern** (cache-aside, write-through…) |
|
|
26
|
+
| How does stale data get *out*? | **TTL + invalidation** |
|
|
27
|
+
| What happens on a miss storm or a dead cache? | **stampede control + fallback** |
|
|
28
|
+
|
|
29
|
+
If you can't answer the third and fourth questions before you ship, you don't have a caching
|
|
30
|
+
strategy — you have a future incident. Decide the invalidation plan *first*.
|
|
31
|
+
|
|
32
|
+
## 1. What to cache
|
|
33
|
+
|
|
34
|
+
Cache data that is **read-often, expensive to produce, and tolerant of some staleness**. Good
|
|
35
|
+
candidates: rendered product pages, the result of an expensive aggregation, a third-party API
|
|
36
|
+
response, a permission lookup hit on every request. Bad candidates: a user's current account balance,
|
|
37
|
+
a one-time-read report, anything where serving a 30-second-old value is a correctness or compliance
|
|
38
|
+
bug.
|
|
39
|
+
|
|
40
|
+
Quantify it before caching. The benefit is roughly `hitRate × costPerMiss`. A 50% hit rate on a 200ms
|
|
41
|
+
query is enormous; a 50% hit rate on a 2ms query is noise that you've paid for with a consistency
|
|
42
|
+
risk. Measure hit rate in production — a cache below ~80% hit rate for point lookups usually means the
|
|
43
|
+
key space is too sparse or the TTL is too short.
|
|
44
|
+
|
|
45
|
+
## 2. Read & write patterns
|
|
46
|
+
|
|
47
|
+
The pattern is *how data flows between your app, the cache, and the source of truth*. Pick one
|
|
48
|
+
deliberately per dataset.
|
|
49
|
+
|
|
50
|
+
- **Cache-aside (lazy loading)** — the default. App checks cache; on miss, loads from the source and
|
|
51
|
+
populates the cache. Simple, resilient (a dead cache just means slow, not broken), but the first
|
|
52
|
+
read of every key is a miss.
|
|
53
|
+
- **Read-through** — the cache library itself loads from the source on a miss. Same shape as
|
|
54
|
+
cache-aside but the loading logic lives behind the cache abstraction.
|
|
55
|
+
- **Write-through** — writes go to cache *and* source synchronously. Cache is always fresh; writes
|
|
56
|
+
are slower.
|
|
57
|
+
- **Write-behind (write-back)** — writes hit the cache and are flushed to the source asynchronously.
|
|
58
|
+
Fast writes, but you risk data loss on a crash before flush.
|
|
59
|
+
|
|
60
|
+
Full comparison with sequence diagrams and when each one bites:
|
|
61
|
+
[references/cache-patterns.md](references/cache-patterns.md).
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
// Cache-aside — the 90% pattern. Note the explicit TTL and miss-population.
|
|
65
|
+
async function getProduct(id, { cache, db, ttl = 300 }) {
|
|
66
|
+
const key = `product:${id}`;
|
|
67
|
+
const hit = await cache.get(key);
|
|
68
|
+
if (hit !== null && hit !== undefined) return JSON.parse(hit); // cache hit
|
|
69
|
+
|
|
70
|
+
const product = await db.getProduct(id); // miss -> source of truth
|
|
71
|
+
if (product) await cache.set(key, JSON.stringify(product), { ex: ttl });
|
|
72
|
+
return product;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 3. TTL, eviction & jitter
|
|
77
|
+
|
|
78
|
+
A cache is finite, so entries leave in two ways: **TTL expiry** (time-based) and **eviction** (space
|
|
79
|
+
pressure). Tune both.
|
|
80
|
+
|
|
81
|
+
- **TTL** bounds staleness. Short TTL = fresher + more misses; long TTL = fewer misses + staler data.
|
|
82
|
+
- **Eviction policy** decides who gets dropped when memory is full: **LRU** (evict least recently
|
|
83
|
+
used — the sane default), **LFU** (least frequently used — better for skewed popularity), **FIFO**
|
|
84
|
+
(rarely what you want). See `references/eviction-and-ttl.md`.
|
|
85
|
+
- **TTL jitter is mandatory at scale.** If you populate 10,000 keys in a batch with a fixed 300s TTL,
|
|
86
|
+
they all expire in the same second and stampede the database together. Add randomness so expiries
|
|
87
|
+
spread out.
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
// Add ±10% jitter so a batch of keys never expires in lockstep.
|
|
91
|
+
function jitteredTtl(baseSeconds, jitterRatio = 0.1) {
|
|
92
|
+
const delta = baseSeconds * jitterRatio;
|
|
93
|
+
const offset = (Math.random() * 2 - 1) * delta; // uniform in [-delta, +delta]
|
|
94
|
+
return Math.max(1, Math.round(baseSeconds + offset));
|
|
95
|
+
}
|
|
96
|
+
// jitteredTtl(300) -> ~270..330, never the same instant across a batch
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`scripts/ttl-jitter.mjs` is a runnable, self-testing version of this function.
|
|
100
|
+
|
|
101
|
+
## 4. Invalidation
|
|
102
|
+
|
|
103
|
+
> "There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton
|
|
104
|
+
|
|
105
|
+
TTL alone is *eventual* freshness. When correctness matters, invalidate **on write**:
|
|
106
|
+
|
|
107
|
+
- **Explicit bust** — on update, delete the key (`cache.del(key)`) so the next read repopulates. With
|
|
108
|
+
cache-aside this is the standard move. Beware the race: delete *after* the DB commits, not before.
|
|
109
|
+
- **Write-through** — keep the cache correct on the write path so there's nothing to bust.
|
|
110
|
+
- **Versioned keys** — embed a version/etag in the key (`product:42:v7`); bumping the version
|
|
111
|
+
instantly orphans all old entries without a scan.
|
|
112
|
+
|
|
113
|
+
The subtle bugs (delete-before-commit races, distributed invalidation lag, cache-vs-DB ordering) are
|
|
114
|
+
covered in `references/invalidation-and-stampede.md`.
|
|
115
|
+
|
|
116
|
+
## 5. Stampede control (thundering herd / dogpile)
|
|
117
|
+
|
|
118
|
+
When a hot key expires, every concurrent request misses at once and they *all* hit the source —
|
|
119
|
+
the **thundering herd**. A single popular key can take down a database this way. Mitigations:
|
|
120
|
+
|
|
121
|
+
- **Per-key locking (single-flight):** the first miss acquires a lock and recomputes; everyone else
|
|
122
|
+
waits for the result instead of also recomputing.
|
|
123
|
+
- **Stale-while-revalidate:** serve the slightly-stale value while one background task refreshes it.
|
|
124
|
+
- **Early/probabilistic recomputation:** refresh a key *before* it expires, with probability rising
|
|
125
|
+
as expiry approaches, so the herd never forms.
|
|
126
|
+
- **TTL jitter:** prevents *synchronized* expiry in the first place (see §3).
|
|
127
|
+
|
|
128
|
+
Detailed algorithms and pseudocode: `references/invalidation-and-stampede.md`.
|
|
129
|
+
|
|
130
|
+
## 6. HTTP caching
|
|
131
|
+
|
|
132
|
+
Not all caching is a key-value store. The HTTP layer gives you free caching via `Cache-Control`,
|
|
133
|
+
`ETag`, and `Last-Modified` — push work to the browser and CDN before it ever reaches your app. See
|
|
134
|
+
`examples/http-cache-headers.md` for a `Cache-Control` / `ETag` / `304 Not Modified` walkthrough.
|
|
135
|
+
|
|
136
|
+
## Common pitfalls (failure modes & edge cases)
|
|
137
|
+
|
|
138
|
+
- **No invalidation plan** — TTL-only freshness shipped where correctness was required. The classic
|
|
139
|
+
"I updated it but it's still showing the old price for 5 minutes" bug.
|
|
140
|
+
- **Synchronized expiry** — fixed TTL on a batch of keys → herd at expiry. Always jitter.
|
|
141
|
+
- **Caching nulls badly** — either you don't cache "not found" (so every miss re-queries a row that
|
|
142
|
+
doesn't exist — **cache penetration**) or you cache it forever (so a later insert is invisible).
|
|
143
|
+
Cache negatives with a *short* TTL.
|
|
144
|
+
- **Unbounded cache / no eviction** — an in-process map with no size cap is a memory leak that ends in
|
|
145
|
+
an OOM. Always bound size and set an eviction policy.
|
|
146
|
+
- **Delete-before-commit race** — busting the key before the DB transaction commits lets a concurrent
|
|
147
|
+
read repopulate the cache with the *old* value. Bust after commit.
|
|
148
|
+
- **Stale-on-error** — refresh logic that overwrites a good cached value with an error/empty result.
|
|
149
|
+
On a source failure, keep serving stale rather than caching the failure.
|
|
150
|
+
- **Cache key collisions** — forgetting a discriminator (locale, tenant, auth scope) in the key, so
|
|
151
|
+
user A sees user B's cached personalized data. A security bug, not just a correctness one.
|
|
152
|
+
- **Thundering herd ignored** — works fine until the one hot key expires under load.
|
|
153
|
+
|
|
154
|
+
## When NOT to cache / trade-offs
|
|
155
|
+
|
|
156
|
+
Caching is not free — it adds a consistency risk, operational surface, and a second source of truth
|
|
157
|
+
to reason about. Skip or reconsider it when:
|
|
158
|
+
|
|
159
|
+
- **Strong consistency is required** (balances, inventory counts, anything legally/financially
|
|
160
|
+
binding) — a stale read is a bug, not a latency win.
|
|
161
|
+
- **Low read-to-write ratio** — if data changes nearly as often as it's read, you'll spend more effort
|
|
162
|
+
invalidating than you save on reads.
|
|
163
|
+
- **The source is already fast** — caching a 1ms lookup buys nothing and adds a failure mode.
|
|
164
|
+
- **Per-user, read-once data** — the entry is evicted before it's ever reused.
|
|
165
|
+
|
|
166
|
+
When you *do* cache, name the trade-off explicitly: you are choosing **availability and latency over
|
|
167
|
+
strict consistency** (an AP-leaning choice). Make the maximum staleness window a documented number,
|
|
168
|
+
not an accident of the TTL you happened to pick.
|
|
169
|
+
|
|
170
|
+
## Files in this package
|
|
171
|
+
|
|
172
|
+
- `references/cache-patterns.md` — cache-aside / read-through / write-through / write-behind, with flows
|
|
173
|
+
- `references/eviction-and-ttl.md` — LRU / LFU / FIFO, TTL tuning, jitter, negative caching
|
|
174
|
+
- `references/invalidation-and-stampede.md` — invalidation strategies, thundering herd, dogpile locks
|
|
175
|
+
- `examples/redis-cache-aside.js` — runnable cache-aside + single-flight against an in-memory fake
|
|
176
|
+
- `examples/http-cache-headers.md` — `Cache-Control`, `ETag`, and `304` revalidation
|
|
177
|
+
- `scripts/ttl-jitter.mjs` — runnable jittered-TTL helper with `--selftest`
|
|
178
|
+
|
|
179
|
+
Pairs with the `rest-api-design` skill (HTTP cache headers), the `database-scaling` skill (caching as a
|
|
180
|
+
read-load shield), and the `performance-engineer` agent.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# HTTP Caching: Cache-Control, ETag, and 304
|
|
2
|
+
|
|
3
|
+
The cheapest cache is the one you never run: push caching to the browser and CDN with HTTP headers. No
|
|
4
|
+
key-value store, no invalidation code — the protocol does it.
|
|
5
|
+
|
|
6
|
+
## Cache-Control
|
|
7
|
+
|
|
8
|
+
The primary knob. Tells browsers and shared caches (CDNs, proxies) how long a response is fresh.
|
|
9
|
+
|
|
10
|
+
```http
|
|
11
|
+
# Public, cacheable by CDNs, fresh for 1 hour, then revalidate.
|
|
12
|
+
Cache-Control: public, max-age=3600, must-revalidate
|
|
13
|
+
|
|
14
|
+
# Per-user/private data: only the browser may cache, never a shared CDN.
|
|
15
|
+
Cache-Control: private, max-age=0, no-cache
|
|
16
|
+
|
|
17
|
+
# Truly sensitive (never store):
|
|
18
|
+
Cache-Control: no-store
|
|
19
|
+
|
|
20
|
+
# Immutable, content-hashed asset (app.9f3a2.js) — cache for a year, never revalidate.
|
|
21
|
+
Cache-Control: public, max-age=31536000, immutable
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Key directives:
|
|
25
|
+
- `max-age=N` — fresh for N seconds.
|
|
26
|
+
- `s-maxage=N` — overrides `max-age` for *shared* caches (CDN) only.
|
|
27
|
+
- `public` / `private` — may a shared cache store it?
|
|
28
|
+
- `no-cache` — store it, but **revalidate** before each use (not "don't cache").
|
|
29
|
+
- `no-store` — never write it to any cache.
|
|
30
|
+
- `stale-while-revalidate=N` — serve stale up to N seconds while revalidating in the background.
|
|
31
|
+
- `immutable` — the body will never change at this URL; skip revalidation entirely.
|
|
32
|
+
|
|
33
|
+
## ETag + conditional requests (revalidation)
|
|
34
|
+
|
|
35
|
+
An `ETag` is an opaque fingerprint of the response body. The client sends it back to ask "still
|
|
36
|
+
valid?", and the server answers `304 Not Modified` with *no body* if unchanged — saving bandwidth.
|
|
37
|
+
|
|
38
|
+
```http
|
|
39
|
+
# First response
|
|
40
|
+
HTTP/1.1 200 OK
|
|
41
|
+
Cache-Control: no-cache
|
|
42
|
+
ETag: "v7-9f3a2c"
|
|
43
|
+
Content-Type: application/json
|
|
44
|
+
|
|
45
|
+
{ "id": 42, "name": "Product 42", "price": 1999 }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```http
|
|
49
|
+
# Client revalidates with the stored validator
|
|
50
|
+
GET /products/42 HTTP/1.1
|
|
51
|
+
If-None-Match: "v7-9f3a2c"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```http
|
|
55
|
+
# Server: unchanged -> tiny response, no body
|
|
56
|
+
HTTP/1.1 304 Not Modified
|
|
57
|
+
ETag: "v7-9f3a2c"
|
|
58
|
+
Cache-Control: no-cache
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`Last-Modified` + `If-Modified-Since` is the date-based equivalent; `ETag` is preferred because it's
|
|
62
|
+
exact (sub-second changes, content-based) rather than 1-second-granular.
|
|
63
|
+
|
|
64
|
+
## Picking a strategy
|
|
65
|
+
|
|
66
|
+
| Asset | Headers |
|
|
67
|
+
|---|---|
|
|
68
|
+
| Content-hashed JS/CSS (`app.9f3a2.js`) | `public, max-age=31536000, immutable` |
|
|
69
|
+
| HTML shell | `no-cache` + `ETag` (always revalidate, cheap when unchanged) |
|
|
70
|
+
| Public API list, slow-changing | `public, max-age=60, stale-while-revalidate=300` |
|
|
71
|
+
| Per-user dashboard JSON | `private, no-cache` + `ETag` |
|
|
72
|
+
| Auth tokens, payment pages | `no-store` |
|
|
73
|
+
|
|
74
|
+
## Pitfalls
|
|
75
|
+
|
|
76
|
+
- `no-cache` does **not** mean "don't cache" — it means "revalidate every time". Use `no-store` to
|
|
77
|
+
forbid storage.
|
|
78
|
+
- Forgetting `private` on personalized responses lets a CDN serve user A's data to user B.
|
|
79
|
+
- Long `max-age` on a URL whose content changes = stale clients with no way to bust. Use content-hashed
|
|
80
|
+
filenames (`immutable`) so the URL changes when the content does.
|
|
81
|
+
- `ETag` mismatches across a load-balanced fleet (e.g. inode-based ETags) cause needless re-downloads;
|
|
82
|
+
use a deterministic, content-derived ETag.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Cache-aside with single-flight (stampede protection), demonstrated against an
|
|
2
|
+
// in-memory fake that mimics the slice of the Redis API we use (get/set/del +
|
|
3
|
+
// SET NX for locking). Swap `new FakeRedis()` for a real client (e.g. ioredis)
|
|
4
|
+
// and the logic is unchanged.
|
|
5
|
+
//
|
|
6
|
+
// Run it directly with Node (built-ins only; ES module syntax):
|
|
7
|
+
// node redis-cache-aside.js
|
|
8
|
+
//
|
|
9
|
+
// It fires 50 concurrent reads of one cold key and shows that the expensive
|
|
10
|
+
// "DB" load runs exactly once — the herd is collapsed by the per-key lock.
|
|
11
|
+
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import process from 'node:process';
|
|
14
|
+
|
|
15
|
+
// --- tiny TTL helper (mirrors scripts/ttl-jitter.mjs) ------------------------
|
|
16
|
+
function jitteredTtl(baseSeconds, jitterRatio = 0.1) {
|
|
17
|
+
const delta = baseSeconds * jitterRatio;
|
|
18
|
+
const offset = (Math.random() * 2 - 1) * delta;
|
|
19
|
+
return Math.max(1, Math.round(baseSeconds + offset));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// --- minimal in-memory stand-in for Redis -----------------------------------
|
|
23
|
+
class FakeRedis {
|
|
24
|
+
constructor() { this.store = new Map(); } // key -> { value, expiresAt }
|
|
25
|
+
async get(key) {
|
|
26
|
+
const e = this.store.get(key);
|
|
27
|
+
if (!e) return null;
|
|
28
|
+
if (e.expiresAt && e.expiresAt < Date.now()) { this.store.delete(key); return null; }
|
|
29
|
+
return e.value;
|
|
30
|
+
}
|
|
31
|
+
async set(key, value, { ex } = {}) {
|
|
32
|
+
this.store.set(key, { value, expiresAt: ex ? Date.now() + ex * 1000 : 0 });
|
|
33
|
+
return 'OK';
|
|
34
|
+
}
|
|
35
|
+
// SET key val NX EX ttl -> returns 'OK' only if the key did not exist (lock).
|
|
36
|
+
// Atomic check-and-set: in real Redis this is one command; here we do the
|
|
37
|
+
// check + write synchronously (no await between) so concurrent callers can't
|
|
38
|
+
// both win the lock.
|
|
39
|
+
async setNx(key, value, ttlSeconds) {
|
|
40
|
+
const e = this.store.get(key);
|
|
41
|
+
const live = e && (!e.expiresAt || e.expiresAt >= Date.now());
|
|
42
|
+
if (live) return null;
|
|
43
|
+
this.store.set(key, { value, expiresAt: ttlSeconds ? Date.now() + ttlSeconds * 1000 : 0 });
|
|
44
|
+
return 'OK';
|
|
45
|
+
}
|
|
46
|
+
async del(key) { return this.store.delete(key) ? 1 : 0; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
50
|
+
|
|
51
|
+
// --- the expensive source of truth (counts how often it actually runs) ------
|
|
52
|
+
let dbCalls = 0;
|
|
53
|
+
async function loadProductFromDb(id) {
|
|
54
|
+
dbCalls += 1;
|
|
55
|
+
await sleep(50); // pretend this is a slow query
|
|
56
|
+
return { id, name: `Product ${id}`, price: 1999 };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- cache-aside read with single-flight lock -------------------------------
|
|
60
|
+
async function getProduct(id, { cache, ttl = 300, lockTtl = 5, maxWaitMs = 2000 }) {
|
|
61
|
+
const key = `product:${id}`;
|
|
62
|
+
const lockKey = `lock:${key}`;
|
|
63
|
+
|
|
64
|
+
const hit = await cache.get(key);
|
|
65
|
+
if (hit !== null) return JSON.parse(hit);
|
|
66
|
+
|
|
67
|
+
// Try to become the single loader for this key.
|
|
68
|
+
const gotLock = await cache.setNx(lockKey, '1', lockTtl);
|
|
69
|
+
if (gotLock === 'OK') {
|
|
70
|
+
try {
|
|
71
|
+
const product = await loadProductFromDb(id);
|
|
72
|
+
await cache.set(key, JSON.stringify(product), { ex: jitteredTtl(ttl) });
|
|
73
|
+
return product;
|
|
74
|
+
} finally {
|
|
75
|
+
await cache.del(lockKey);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Follower: wait for the winner to populate the cache, then read it.
|
|
80
|
+
const deadline = Date.now() + maxWaitMs;
|
|
81
|
+
while (Date.now() < deadline) {
|
|
82
|
+
await sleep(10);
|
|
83
|
+
const v = await cache.get(key);
|
|
84
|
+
if (v !== null) return JSON.parse(v);
|
|
85
|
+
}
|
|
86
|
+
// Last resort if the winner died: load directly (degrade, don't fail).
|
|
87
|
+
return loadProductFromDb(id);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function main() {
|
|
91
|
+
const cache = new FakeRedis();
|
|
92
|
+
const herd = Array.from({ length: 50 }, () => getProduct(42, { cache }));
|
|
93
|
+
const results = await Promise.all(herd);
|
|
94
|
+
|
|
95
|
+
console.log(`Concurrent reads: ${results.length}`);
|
|
96
|
+
console.log(`Distinct DB loads: ${dbCalls} (single-flight collapsed the herd)`);
|
|
97
|
+
console.log(`Sample result: ${JSON.stringify(results[0])}`);
|
|
98
|
+
|
|
99
|
+
// A second read is now a pure cache hit (no new DB load).
|
|
100
|
+
const before = dbCalls;
|
|
101
|
+
await getProduct(42, { cache });
|
|
102
|
+
console.log(`DB loads after warm: ${dbCalls - before} (0 == cache hit)`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const isMain = process.argv[1] === fileURLToPath(import.meta.url);
|
|
106
|
+
if (isMain) {
|
|
107
|
+
main().catch((e) => { console.error(e); process.exit(1); });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export { getProduct, jitteredTtl, FakeRedis };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Cache Read/Write Patterns
|
|
2
|
+
|
|
3
|
+
The *pattern* is how data flows between your application, the cache, and the source of truth (usually a
|
|
4
|
+
database). Choose one per dataset based on read/write ratio and consistency needs.
|
|
5
|
+
|
|
6
|
+
## Cache-aside (lazy loading) — the default
|
|
7
|
+
|
|
8
|
+
The application owns the cache logic. The cache knows nothing about the database.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
READ:
|
|
12
|
+
v = cache.get(key)
|
|
13
|
+
if v exists: return v # hit
|
|
14
|
+
v = db.load(key) # miss
|
|
15
|
+
cache.set(key, v, ttl)
|
|
16
|
+
return v
|
|
17
|
+
|
|
18
|
+
WRITE:
|
|
19
|
+
db.write(key, v)
|
|
20
|
+
cache.del(key) # invalidate; next read repopulates
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- **Pros:** simple; resilient — a dead cache degrades to "slow", not "broken"; only requested data is
|
|
24
|
+
cached, so the working set stays small.
|
|
25
|
+
- **Cons:** every key's first read is a miss (cold-start latency); cache and DB can drift if you forget
|
|
26
|
+
to invalidate on write.
|
|
27
|
+
- **Use when:** read-heavy workloads with tolerable staleness. This is the right answer ~80% of the
|
|
28
|
+
time.
|
|
29
|
+
|
|
30
|
+
## Read-through
|
|
31
|
+
|
|
32
|
+
Same flow as cache-aside, but the *cache layer* loads from the source on a miss via a configured
|
|
33
|
+
loader, instead of the application doing it inline.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
READ:
|
|
37
|
+
return cache.getOrLoad(key, loader=db.load) # cache calls loader on miss
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- **Pros:** loading logic is centralized behind the cache abstraction; application code is cleaner.
|
|
41
|
+
- **Cons:** requires a cache library/provider that supports loaders; still cold on first read.
|
|
42
|
+
- **Use when:** you want one canonical load path and your cache library supports it.
|
|
43
|
+
|
|
44
|
+
## Write-through
|
|
45
|
+
|
|
46
|
+
Writes are applied to the cache *and* the source synchronously, as one logical operation.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
WRITE:
|
|
50
|
+
cache.set(key, v)
|
|
51
|
+
db.write(key, v) # both, synchronously, before returning
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **Pros:** the cache is always consistent with the DB for written keys; reads after a write are always
|
|
55
|
+
fresh.
|
|
56
|
+
- **Cons:** every write pays the cache write cost; you can cache data that's never read again (write
|
|
57
|
+
amplification). Often paired with read-through.
|
|
58
|
+
- **Use when:** read-after-write freshness matters and writes are not the bottleneck.
|
|
59
|
+
|
|
60
|
+
## Write-behind (write-back)
|
|
61
|
+
|
|
62
|
+
Writes hit the cache immediately and are flushed to the source asynchronously (batched).
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
WRITE:
|
|
66
|
+
cache.set(key, v)
|
|
67
|
+
queue.push(key) # background worker flushes to db later
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- **Pros:** very fast writes; absorbs write spikes; enables batching/coalescing of DB writes.
|
|
71
|
+
- **Cons:** **data loss risk** if the process crashes before flush; the DB is eventually consistent
|
|
72
|
+
with the cache; ordering and failure handling get complex.
|
|
73
|
+
- **Use when:** high write throughput where some durability risk is acceptable (metrics, counters,
|
|
74
|
+
activity feeds). Never for data you can't afford to lose.
|
|
75
|
+
|
|
76
|
+
## Refresh-ahead
|
|
77
|
+
|
|
78
|
+
Proactively refresh entries that are *about to* expire and are being actively read, so a hot key never
|
|
79
|
+
serves a miss.
|
|
80
|
+
|
|
81
|
+
- **Pros:** hides refresh latency for hot keys; helps prevent stampedes.
|
|
82
|
+
- **Cons:** wasted refreshes for keys that won't be read again; needs access-pattern prediction.
|
|
83
|
+
- **Use when:** a small set of very hot, predictable keys.
|
|
84
|
+
|
|
85
|
+
## Quick selector
|
|
86
|
+
|
|
87
|
+
| Need | Pattern |
|
|
88
|
+
|---|---|
|
|
89
|
+
| General read-heavy, tolerate staleness | cache-aside |
|
|
90
|
+
| Centralized load path | read-through |
|
|
91
|
+
| Read-after-write must be fresh | write-through |
|
|
92
|
+
| Extreme write throughput, can risk loss | write-behind |
|
|
93
|
+
| Hot keys must never miss | refresh-ahead |
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Eviction, TTL & Jitter
|
|
2
|
+
|
|
3
|
+
A cache is finite. Entries leave by **TTL expiry** (time) or **eviction** (space pressure). Tune both
|
|
4
|
+
deliberately, or the cache tunes itself badly.
|
|
5
|
+
|
|
6
|
+
## Eviction policies
|
|
7
|
+
|
|
8
|
+
When memory is full and a new entry arrives, the policy picks a victim.
|
|
9
|
+
|
|
10
|
+
| Policy | Evicts | Good for | Watch out |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| **LRU** (Least Recently Used) | the entry untouched longest | general purpose, temporal locality | a full scan can flush the whole hot set |
|
|
13
|
+
| **LFU** (Least Frequently Used) | the entry with fewest accesses | skewed popularity (a few very hot keys) | new entries look "cold" and get evicted early; needs aging |
|
|
14
|
+
| **FIFO** (First In First Out) | the oldest inserted, regardless of use | rarely the right choice | evicts hot keys just because they're old |
|
|
15
|
+
| **Random** | a random entry | cheap, surprisingly OK under uniform access | unpredictable tail latency |
|
|
16
|
+
| **TTL-only** | nothing until expiry | when size is genuinely unbounded-safe | memory leak if the key space is large |
|
|
17
|
+
|
|
18
|
+
**Default to LRU.** Move to LFU (or a hybrid like LRU-K / W-TinyLFU) only when you've measured a
|
|
19
|
+
popularity skew where LRU underperforms. Modern Redis offers `allkeys-lru`, `allkeys-lfu`, and
|
|
20
|
+
`volatile-*` variants (evict only keys with a TTL set).
|
|
21
|
+
|
|
22
|
+
### Always bound the size
|
|
23
|
+
|
|
24
|
+
An in-process cache (a plain `Map`) with no max size is a memory leak that ends in an OOM crash. Cap by
|
|
25
|
+
entry count or bytes and attach an eviction policy. A bounded LRU map is the minimum viable in-process
|
|
26
|
+
cache.
|
|
27
|
+
|
|
28
|
+
## TTL tuning
|
|
29
|
+
|
|
30
|
+
TTL bounds the maximum staleness window. Pick it from the data's tolerance, not by feel:
|
|
31
|
+
|
|
32
|
+
- **Volatile, correctness-sensitive** (prices shown at checkout): seconds, plus explicit invalidation.
|
|
33
|
+
- **Slowly changing** (a product description, a config blob): minutes to hours.
|
|
34
|
+
- **Effectively static** (country list, feature flags read constantly): hours, with explicit bust on
|
|
35
|
+
change.
|
|
36
|
+
|
|
37
|
+
Document the chosen staleness window as a number. "Up to 5 minutes stale" is a contract; "300" is a
|
|
38
|
+
magic number nobody remembers the reason for.
|
|
39
|
+
|
|
40
|
+
## TTL jitter (mandatory at scale)
|
|
41
|
+
|
|
42
|
+
If many keys are populated together (a deploy warm-up, a batch import, a cache flush + refill) with the
|
|
43
|
+
*same* fixed TTL, they all expire in the same instant and stampede the source together. Spread expiry
|
|
44
|
+
by adding randomness:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
function jitteredTtl(baseSeconds, jitterRatio = 0.1) {
|
|
48
|
+
const delta = baseSeconds * jitterRatio;
|
|
49
|
+
const offset = (Math.random() * 2 - 1) * delta; // uniform in [-delta, +delta]
|
|
50
|
+
return Math.max(1, Math.round(baseSeconds + offset));
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
With `jitterRatio = 0.1`, a nominal 300s TTL becomes a value in `[270, 330]`, so a batch of 10,000 keys
|
|
55
|
+
expires smeared across a 60-second window instead of all at once. See `scripts/ttl-jitter.mjs` for a
|
|
56
|
+
self-tested version.
|
|
57
|
+
|
|
58
|
+
## Negative caching (avoid cache penetration)
|
|
59
|
+
|
|
60
|
+
If a key has no value in the source (a row that doesn't exist), naive cache-aside re-queries the DB on
|
|
61
|
+
*every* request for it — **cache penetration**, which an attacker can weaponize by requesting random
|
|
62
|
+
non-existent IDs.
|
|
63
|
+
|
|
64
|
+
- Cache the "not found" result too, but with a **short** TTL (e.g. 10-30s) so a later insert becomes
|
|
65
|
+
visible quickly.
|
|
66
|
+
- For high-cardinality miss attacks, front the cache with a **Bloom filter** of known-existing keys to
|
|
67
|
+
reject impossible lookups before they hit the DB.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Invalidation & Stampede Control
|
|
2
|
+
|
|
3
|
+
The two hardest caching problems: keeping the cache *correct* (invalidation) and keeping it *safe under
|
|
4
|
+
load* (stampedes).
|
|
5
|
+
|
|
6
|
+
## Invalidation strategies
|
|
7
|
+
|
|
8
|
+
### 1. Explicit bust (delete on write)
|
|
9
|
+
|
|
10
|
+
The standard move for cache-aside. On update, delete the key; the next read repopulates from the
|
|
11
|
+
source.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
db.update(key, newValue) # 1. commit to source of truth FIRST
|
|
15
|
+
cache.del(key) # 2. then invalidate
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Ordering matters.** If you `cache.del` *before* the DB commit, a concurrent read can miss, load the
|
|
19
|
+
*old* (pre-commit) value, and repopulate the cache with stale data that now never expires until TTL.
|
|
20
|
+
Always: commit, then delete.
|
|
21
|
+
|
|
22
|
+
Even commit-then-delete has a narrow race (a read that loaded the old value before commit can write it
|
|
23
|
+
back after your delete). Mitigations: a short TTL as a backstop, or delayed double-delete (delete,
|
|
24
|
+
wait briefly, delete again).
|
|
25
|
+
|
|
26
|
+
### 2. Write-through
|
|
27
|
+
|
|
28
|
+
Update the cache on the write path so there's nothing to invalidate — the cache is always correct for
|
|
29
|
+
written keys. Trades write latency for read correctness.
|
|
30
|
+
|
|
31
|
+
### 3. Versioned / generation keys
|
|
32
|
+
|
|
33
|
+
Embed a version in the key: `product:42:v7`. To invalidate, bump the version (stored separately or
|
|
34
|
+
derived from an `updated_at`/etag). Old entries are instantly orphaned and aged out by eviction — no
|
|
35
|
+
scan, no delete storm. Great for invalidating *groups* of related entries at once.
|
|
36
|
+
|
|
37
|
+
### 4. TTL as the only mechanism
|
|
38
|
+
|
|
39
|
+
Acceptable only when eventual freshness is genuinely fine. Document the staleness window.
|
|
40
|
+
|
|
41
|
+
### Distributed invalidation
|
|
42
|
+
|
|
43
|
+
With multiple app nodes each holding a local (in-process) cache, deleting on one node doesn't touch the
|
|
44
|
+
others. Options: a pub/sub invalidation channel (publish "key X changed", every node drops it), short
|
|
45
|
+
local TTLs, or a shared tier (Redis) as the single cache so there's one place to invalidate.
|
|
46
|
+
|
|
47
|
+
## Stampede control (thundering herd / dogpile)
|
|
48
|
+
|
|
49
|
+
When a hot key expires, every concurrent request misses simultaneously and all hit the source at once.
|
|
50
|
+
A single popular key can saturate a database this way. The "dogpile" is the pile-up of redundant
|
|
51
|
+
recomputations.
|
|
52
|
+
|
|
53
|
+
### Per-key lock (single-flight)
|
|
54
|
+
|
|
55
|
+
The first miss acquires a lock and recomputes; concurrent requests wait for that one result instead of
|
|
56
|
+
each recomputing.
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
function getWithLock(key):
|
|
60
|
+
v = cache.get(key)
|
|
61
|
+
if v exists: return v
|
|
62
|
+
if acquireLock(key, ttl=5s): # only one winner
|
|
63
|
+
try:
|
|
64
|
+
v = db.load(key)
|
|
65
|
+
cache.set(key, v, jitteredTtl(300))
|
|
66
|
+
return v
|
|
67
|
+
finally: releaseLock(key)
|
|
68
|
+
else:
|
|
69
|
+
wait briefly, then retry cache.get # follower waits for the winner
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`examples/redis-cache-aside.js` implements this single-flight pattern runnably.
|
|
73
|
+
|
|
74
|
+
### Stale-while-revalidate
|
|
75
|
+
|
|
76
|
+
Serve the slightly-stale value immediately while *one* background task refreshes it. Requires storing
|
|
77
|
+
a "soft" expiry alongside the hard TTL: past soft-expiry, return stale and trigger a refresh; past hard
|
|
78
|
+
expiry, block and reload. Hides all refresh latency and naturally prevents the herd.
|
|
79
|
+
|
|
80
|
+
### Probabilistic early expiration (XFetch)
|
|
81
|
+
|
|
82
|
+
Recompute a key *before* it expires, with a probability that rises as expiry approaches. Each reader
|
|
83
|
+
rolls the dice; on average exactly one refreshes early, so the herd never forms and no lock is needed.
|
|
84
|
+
A common form recomputes when `now - delta * beta * ln(rand()) >= expiry`.
|
|
85
|
+
|
|
86
|
+
### TTL jitter
|
|
87
|
+
|
|
88
|
+
The cheapest first line of defense: prevent *synchronized* expiry so the herd can't form for
|
|
89
|
+
batch-populated keys. See `references/eviction-and-ttl.md`.
|
|
90
|
+
|
|
91
|
+
## Choosing
|
|
92
|
+
|
|
93
|
+
| Symptom | Fix |
|
|
94
|
+
|---|---|
|
|
95
|
+
| Many keys expire together | TTL jitter |
|
|
96
|
+
| One hot key expires under load | single-flight lock or stale-while-revalidate |
|
|
97
|
+
| Refresh latency visible to users | stale-while-revalidate / refresh-ahead |
|
|
98
|
+
| Lock contention itself is a problem | probabilistic early expiration |
|
|
99
|
+
| Random non-existent keys hammer the DB | negative caching + Bloom filter |
|