vanara 0.1.0 → 0.3.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.
Files changed (195) hide show
  1. package/README.md +14 -0
  2. package/bin/vanara.js +66 -2
  3. package/catalog/agents/api-designer/AGENT.md +156 -0
  4. package/catalog/agents/api-designer/examples/openapi-snippet.yaml +130 -0
  5. package/catalog/agents/api-designer/examples/review-notes.md +61 -0
  6. package/catalog/agents/api-designer/references/contract-and-openapi.md +82 -0
  7. package/catalog/agents/api-designer/references/design-checklist.md +68 -0
  8. package/catalog/agents/api-designer/references/versioning-and-evolution.md +63 -0
  9. package/catalog/agents/api-designer/scripts/lint-openapi.mjs +180 -0
  10. package/catalog/agents/code-reviewer/AGENT.md +184 -0
  11. package/catalog/agents/code-reviewer/examples/pr-comment-template.md +48 -0
  12. package/catalog/agents/code-reviewer/examples/sample-review-output.md +38 -0
  13. package/catalog/agents/code-reviewer/references/review-checklist.md +46 -0
  14. package/catalog/agents/code-reviewer/references/security-review.md +62 -0
  15. package/catalog/agents/code-reviewer/references/severity-rubric.md +63 -0
  16. package/catalog/agents/code-reviewer/scripts/review-guard.mjs +102 -0
  17. package/catalog/agents/debugger/AGENT.md +128 -0
  18. package/catalog/agents/debugger/examples/postmortem-template.md +59 -0
  19. package/catalog/agents/debugger/examples/repro-template.md +50 -0
  20. package/catalog/agents/debugger/references/bisection.md +63 -0
  21. package/catalog/agents/debugger/references/debugging-method.md +57 -0
  22. package/catalog/agents/debugger/references/observability.md +61 -0
  23. package/catalog/agents/debugger/scripts/parse-stacktrace.mjs +109 -0
  24. package/catalog/agents/pr-summarizer/AGENT.md +172 -0
  25. package/catalog/agents/pr-summarizer/examples/sample-pr-summary.md +46 -0
  26. package/catalog/agents/pr-summarizer/examples/weak-vs-strong-summary.md +68 -0
  27. package/catalog/agents/pr-summarizer/references/identifying-risk.md +68 -0
  28. package/catalog/agents/pr-summarizer/references/reading-the-diff.md +68 -0
  29. package/catalog/agents/pr-summarizer/references/test-plan-and-missing-tests.md +64 -0
  30. package/catalog/agents/pr-summarizer/references/writing-the-summary.md +63 -0
  31. package/catalog/agents/pr-summarizer/scripts/diff-risk.mjs +111 -0
  32. package/catalog/agents/refactoring-specialist/AGENT.md +201 -0
  33. package/catalog/agents/refactoring-specialist/examples/before-after.md +91 -0
  34. package/catalog/agents/refactoring-specialist/examples/refactor-plan.md +97 -0
  35. package/catalog/agents/refactoring-specialist/references/large-scale-refactor.md +50 -0
  36. package/catalog/agents/refactoring-specialist/references/safe-workflow.md +75 -0
  37. package/catalog/agents/refactoring-specialist/references/smell-catalog.md +76 -0
  38. package/catalog/agents/refactoring-specialist/scripts/detect-smells.mjs +150 -0
  39. package/catalog/agents/security-auditor/AGENT.md +184 -0
  40. package/catalog/agents/security-auditor/examples/audit-report.md +68 -0
  41. package/catalog/agents/security-auditor/examples/finding-template.md +31 -0
  42. package/catalog/agents/security-auditor/references/audit-workflow.md +116 -0
  43. package/catalog/agents/security-auditor/references/severity-and-reporting.md +56 -0
  44. package/catalog/agents/security-auditor/references/vuln-classes.md +129 -0
  45. package/catalog/agents/security-auditor/scripts/scan-secrets.mjs +86 -0
  46. package/catalog/agents/technical-writer/AGENT.md +179 -0
  47. package/catalog/agents/technical-writer/examples/how-to-example.md +76 -0
  48. package/catalog/agents/technical-writer/examples/reference-example.md +75 -0
  49. package/catalog/agents/technical-writer/references/clarity-and-style.md +52 -0
  50. package/catalog/agents/technical-writer/references/diataxis-and-doc-types.md +61 -0
  51. package/catalog/agents/technical-writer/references/structure-and-maintenance.md +59 -0
  52. package/catalog/agents/technical-writer/scripts/readability.mjs +103 -0
  53. package/catalog/agents/test-author/AGENT.md +193 -0
  54. package/catalog/agents/test-author/examples/test-plan.md +54 -0
  55. package/catalog/agents/test-author/examples/unit-test-example.ts +55 -0
  56. package/catalog/agents/test-author/references/aaa-and-naming.md +75 -0
  57. package/catalog/agents/test-author/references/mocking-and-fixtures.md +94 -0
  58. package/catalog/agents/test-author/references/test-types-and-pyramid.md +60 -0
  59. package/catalog/agents/test-author/scripts/check-coverage.mjs +145 -0
  60. package/catalog/agents/threat-modeler/AGENT.md +179 -0
  61. package/catalog/agents/threat-modeler/examples/dfd-example.md +55 -0
  62. package/catalog/agents/threat-modeler/examples/threat-model.md +81 -0
  63. package/catalog/agents/threat-modeler/references/dfd-and-trust-boundaries.md +76 -0
  64. package/catalog/agents/threat-modeler/references/mitigation-catalog.md +77 -0
  65. package/catalog/agents/threat-modeler/references/stride-method.md +68 -0
  66. package/catalog/agents/threat-modeler/scripts/stride-checklist.mjs +183 -0
  67. package/catalog/agents/vuln-scanner/AGENT.md +162 -0
  68. package/catalog/agents/vuln-scanner/examples/finding-template.md +57 -0
  69. package/catalog/agents/vuln-scanner/examples/scan-report.md +49 -0
  70. package/catalog/agents/vuln-scanner/references/remediation-and-severity.md +68 -0
  71. package/catalog/agents/vuln-scanner/references/scan-types-and-tools.md +83 -0
  72. package/catalog/agents/vuln-scanner/references/triage-and-false-positives.md +73 -0
  73. package/catalog/agents/vuln-scanner/scripts/parse-scan-results.mjs +133 -0
  74. package/catalog/packs/code-review-pack/PACK.md +100 -0
  75. package/catalog/packs/code-review-pack/examples/pr-flow-example.md +50 -0
  76. package/catalog/packs/code-review-pack/references/rollout-guide.md +40 -0
  77. package/catalog/packs/security-pack/PACK.md +128 -0
  78. package/catalog/packs/security-pack/examples/owasp-vuln-triage-and-fix.md +66 -0
  79. package/catalog/packs/security-pack/examples/threat-model-then-audit.md +77 -0
  80. package/catalog/packs/security-pack/references/choosing-the-right-tool.md +37 -0
  81. package/catalog/packs/security-pack/references/threat-model-to-remediation.md +69 -0
  82. package/catalog/packs/security-pack/references/workflow.md +56 -0
  83. package/catalog/skills/api-pagination/SKILL.md +83 -0
  84. package/catalog/skills/api-pagination/examples/keyset-queries.sql +38 -0
  85. package/catalog/skills/api-pagination/examples/response-shapes.md +51 -0
  86. package/catalog/skills/api-pagination/references/consuming-pages.md +39 -0
  87. package/catalog/skills/api-pagination/references/counting-and-totals.md +41 -0
  88. package/catalog/skills/api-pagination/references/cursor-encoding.md +34 -0
  89. package/catalog/skills/api-pagination/scripts/check-cursor.mjs +51 -0
  90. package/catalog/skills/caching-strategies/SKILL.md +180 -0
  91. package/catalog/skills/caching-strategies/examples/http-cache-headers.md +82 -0
  92. package/catalog/skills/caching-strategies/examples/redis-cache-aside.js +110 -0
  93. package/catalog/skills/caching-strategies/references/cache-patterns.md +93 -0
  94. package/catalog/skills/caching-strategies/references/eviction-and-ttl.md +67 -0
  95. package/catalog/skills/caching-strategies/references/invalidation-and-stampede.md +99 -0
  96. package/catalog/skills/caching-strategies/scripts/ttl-jitter.mjs +99 -0
  97. package/catalog/skills/conventional-commits/SKILL.md +135 -0
  98. package/catalog/skills/conventional-commits/examples/commit-examples.md +60 -0
  99. package/catalog/skills/conventional-commits/examples/commitlint.config.js +31 -0
  100. package/catalog/skills/conventional-commits/references/breaking-changes-semver.md +50 -0
  101. package/catalog/skills/conventional-commits/references/scoping-commits.md +42 -0
  102. package/catalog/skills/conventional-commits/references/spec.md +78 -0
  103. package/catalog/skills/conventional-commits/scripts/lint-commit.mjs +97 -0
  104. package/catalog/skills/database-migrations/SKILL.md +137 -0
  105. package/catalog/skills/database-migrations/examples/add-column-safe.sql +34 -0
  106. package/catalog/skills/database-migrations/examples/migration-config.example.json +27 -0
  107. package/catalog/skills/database-migrations/references/expand-contract.md +74 -0
  108. package/catalog/skills/database-migrations/references/rollback-and-safety.md +71 -0
  109. package/catalog/skills/database-migrations/references/zero-downtime-changes.md +84 -0
  110. package/catalog/skills/database-migrations/scripts/check-migration-reversible.mjs +139 -0
  111. package/catalog/skills/error-handling-patterns/SKILL.md +136 -0
  112. package/catalog/skills/error-handling-patterns/examples/express-error-middleware.js +93 -0
  113. package/catalog/skills/error-handling-patterns/examples/result-pattern.ts +81 -0
  114. package/catalog/skills/error-handling-patterns/references/error-taxonomy.md +91 -0
  115. package/catalog/skills/error-handling-patterns/references/language-patterns.md +102 -0
  116. package/catalog/skills/error-handling-patterns/references/retry-and-backoff.md +76 -0
  117. package/catalog/skills/error-handling-patterns/scripts/lint-empty-catch.mjs +113 -0
  118. package/catalog/skills/git-collaboration-workflows/SKILL.md +104 -0
  119. package/catalog/skills/git-collaboration-workflows/examples/branch-protection.md +48 -0
  120. package/catalog/skills/git-collaboration-workflows/examples/pr-review-playbook.md +48 -0
  121. package/catalog/skills/git-collaboration-workflows/references/branching-models.md +52 -0
  122. package/catalog/skills/git-collaboration-workflows/references/history-hygiene.md +44 -0
  123. package/catalog/skills/git-collaboration-workflows/scripts/check-branch-hygiene.mjs +62 -0
  124. package/catalog/skills/owasp-top10/SKILL.md +112 -0
  125. package/catalog/skills/owasp-top10/examples/sql-injection-fix.md +33 -0
  126. package/catalog/skills/owasp-top10/examples/xss-fix.md +27 -0
  127. package/catalog/skills/owasp-top10/references/access-control.md +36 -0
  128. package/catalog/skills/owasp-top10/references/crypto-failures.md +29 -0
  129. package/catalog/skills/owasp-top10/references/injection.md +39 -0
  130. package/catalog/skills/owasp-top10/references/ssrf-and-design.md +35 -0
  131. package/catalog/skills/owasp-top10/scripts/scan-injection.mjs +73 -0
  132. package/catalog/skills/prompt-engineering/SKILL.md +100 -0
  133. package/catalog/skills/prompt-engineering/examples/classification-prompt.md +29 -0
  134. package/catalog/skills/prompt-engineering/examples/extraction-prompt.md +25 -0
  135. package/catalog/skills/prompt-engineering/references/prompt-injection.md +34 -0
  136. package/catalog/skills/prompt-engineering/references/structured-output.md +34 -0
  137. package/catalog/skills/prompt-engineering/references/techniques.md +33 -0
  138. package/catalog/skills/prompt-engineering/scripts/validate-output.mjs +71 -0
  139. package/catalog/skills/readme-writing/SKILL.md +176 -0
  140. package/catalog/skills/readme-writing/examples/README.template.md +73 -0
  141. package/catalog/skills/readme-writing/examples/good-readme-example.md +95 -0
  142. package/catalog/skills/readme-writing/references/anatomy.md +107 -0
  143. package/catalog/skills/readme-writing/references/maintenance.md +68 -0
  144. package/catalog/skills/readme-writing/references/writing-style.md +79 -0
  145. package/catalog/skills/readme-writing/scripts/lint-readme.mjs +117 -0
  146. package/catalog/skills/refactoring-patterns/SKILL.md +178 -0
  147. package/catalog/skills/refactoring-patterns/examples/extract-function.before-after.js +57 -0
  148. package/catalog/skills/refactoring-patterns/examples/replace-conditional-with-polymorphism.before-after.ts +63 -0
  149. package/catalog/skills/refactoring-patterns/references/code-smells.md +92 -0
  150. package/catalog/skills/refactoring-patterns/references/refactoring-catalog.md +78 -0
  151. package/catalog/skills/refactoring-patterns/references/safe-workflow.md +74 -0
  152. package/catalog/skills/refactoring-patterns/scripts/detect-smells.mjs +137 -0
  153. package/catalog/skills/rest-api-design/SKILL.md +147 -0
  154. package/catalog/skills/rest-api-design/examples/error-envelope.json +26 -0
  155. package/catalog/skills/rest-api-design/examples/orders-api.openapi.yaml +106 -0
  156. package/catalog/skills/rest-api-design/references/error-handling.md +43 -0
  157. package/catalog/skills/rest-api-design/references/pagination.md +41 -0
  158. package/catalog/skills/rest-api-design/references/status-codes.md +32 -0
  159. package/catalog/skills/rest-api-design/references/versioning.md +31 -0
  160. package/catalog/skills/rest-api-design/scripts/check-envelope.mjs +62 -0
  161. package/catalog/skills/secrets-management/SKILL.md +134 -0
  162. package/catalog/skills/secrets-management/examples/env-example.md +41 -0
  163. package/catalog/skills/secrets-management/examples/rotation-runbook.md +50 -0
  164. package/catalog/skills/secrets-management/references/leak-response.md +47 -0
  165. package/catalog/skills/secrets-management/references/rotation.md +46 -0
  166. package/catalog/skills/secrets-management/references/secret-managers.md +52 -0
  167. package/catalog/skills/secrets-management/scripts/detect-hardcoded.mjs +96 -0
  168. package/catalog/skills/secure-auth/SKILL.md +135 -0
  169. package/catalog/skills/secure-auth/examples/auth-flow.md +60 -0
  170. package/catalog/skills/secure-auth/examples/jwt-verify.ts +89 -0
  171. package/catalog/skills/secure-auth/references/auth-attacks.md +76 -0
  172. package/catalog/skills/secure-auth/references/oauth2-oidc.md +65 -0
  173. package/catalog/skills/secure-auth/references/password-hashing.md +70 -0
  174. package/catalog/skills/secure-auth/references/sessions-vs-jwt.md +69 -0
  175. package/catalog/skills/secure-auth/scripts/check-password-policy.mjs +114 -0
  176. package/catalog/skills/sql-index-tuning/SKILL.md +171 -0
  177. package/catalog/skills/sql-index-tuning/examples/explain-walkthrough.sql +91 -0
  178. package/catalog/skills/sql-index-tuning/examples/index-ddl.sql +67 -0
  179. package/catalog/skills/sql-index-tuning/references/btree-internals.md +58 -0
  180. package/catalog/skills/sql-index-tuning/references/composite-and-covering.md +60 -0
  181. package/catalog/skills/sql-index-tuning/references/reading-explain.md +54 -0
  182. package/catalog/skills/sql-index-tuning/references/selectivity-and-cardinality.md +62 -0
  183. package/catalog/skills/sql-index-tuning/scripts/suggest-index.mjs +213 -0
  184. package/catalog/skills/test-plan-design/SKILL.md +154 -0
  185. package/catalog/skills/test-plan-design/examples/test-case-template.md +45 -0
  186. package/catalog/skills/test-plan-design/examples/test-plan.md +57 -0
  187. package/catalog/skills/test-plan-design/references/case-design.md +79 -0
  188. package/catalog/skills/test-plan-design/references/risk-prioritization.md +58 -0
  189. package/catalog/skills/test-plan-design/references/test-pyramid.md +58 -0
  190. package/catalog/skills/test-plan-design/scripts/coverage-gaps.mjs +109 -0
  191. package/free-tier.json +15 -0
  192. package/package.json +15 -4
  193. package/premium-index.json +1 -0
  194. package/src/config.js +31 -1
  195. package/src/license.js +143 -0
@@ -0,0 +1,41 @@
1
+ # Counting and Totals
2
+
3
+ `COUNT(*)` is the hidden cost center of pagination. On a large Postgres table it is a full index
4
+ or heap scan — routinely slower than fetching the page itself — and the number is stale the moment
5
+ it is computed.
6
+
7
+ ## Decision table
8
+
9
+ | Product need | Implementation |
10
+ |---|---|
11
+ | "Load more" / infinite scroll | No count at all — `has_more` from the +1-row trick |
12
+ | "~12,300 results" | Estimated count (below), clearly rounded in the UI |
13
+ | Exact count on small filtered sets (<10k rows) | Real `COUNT(*)`, acceptable |
14
+ | Exact count on big tables, hard requirement | Maintained counter (trigger or async rollup) |
15
+
16
+ ## Estimated counts (Postgres)
17
+
18
+ ```sql
19
+ -- Table-wide estimate: instant, from the planner's statistics
20
+ SELECT reltuples::bigint FROM pg_class WHERE relname = 'items';
21
+
22
+ -- Filtered estimate: run the query through EXPLAIN and parse rows=
23
+ EXPLAIN (FORMAT JSON) SELECT 1 FROM items WHERE status = 'active';
24
+ -- -> Plan.["Plan Rows"]
25
+ ```
26
+
27
+ Estimates drift after bulk writes until autovacuum/analyze runs — fine for "~N results", wrong for
28
+ billing or quotas.
29
+
30
+ ## Maintained counters
31
+
32
+ A trigger-updated counter table gives exact totals at O(1) read cost, but serializes writes on hot
33
+ rows (every insert contends on the same counter row). For high write rates, shard the counter
34
+ (N rows, sum on read) or roll up asynchronously and accept seconds of lag. Never compute quotas
35
+ or invoices from pagination counts — those need their own transactional bookkeeping.
36
+
37
+ ## The `total_pages` trap
38
+
39
+ Exposing `total_pages` invites clients to build page links (`?page=812`), which drags you back to
40
+ deep offsets. If the UI needs a jump-to-end affordance, offer a reversed sort (`?order=asc`)
41
+ instead — last page becomes first page, at first-page cost.
@@ -0,0 +1,34 @@
1
+ # Cursor Encoding
2
+
3
+ A cursor must carry enough to rebuild the keyset `WHERE` clause and nothing more: the sort-key
4
+ values, the direction, and a version byte. It must be opaque, tamper-evident (public APIs), and
5
+ survive encoding changes across deploys.
6
+
7
+ ## Layout
8
+
9
+ ```
10
+ v1 . base64url( json({ k: ["2026-07-01T12:00:00.000Z", 4711], d: "desc" }) ) . hmac10
11
+ ```
12
+
13
+ - **`v1` prefix** — bump when the payload shape changes; reject unknown versions with 400
14
+ `CURSOR_INVALID`, which clients treat as "restart iteration".
15
+ - **base64url**, not base64 — cursors travel in query strings; `+/=` break naive clients.
16
+ - **HMAC (first 10 bytes, hex)** over `version + payload` with a server-side key. Not secrecy —
17
+ integrity. Without it, clients will reverse-engineer the payload and build their own cursors,
18
+ and your encoding becomes an accidental public API you can never change.
19
+
20
+ ## Rules
21
+
22
+ - Timestamps in the payload must be **full precision** (milliseconds or better). Truncated seconds
23
+ re-introduce the duplicate-key ambiguity the tiebreaker was meant to solve.
24
+ - Never embed user IDs, filters, or auth state in the cursor — re-derive those from the request.
25
+ A cursor pasted into another user's session must not leak or authorize anything.
26
+ - Cursors may expire (e.g., if the underlying snapshot is gone). Expired cursor → 400 with a
27
+ distinct `CURSOR_EXPIRED` code so clients restart rather than retry.
28
+ - Treat decode failures as client errors (400), never 500 — fuzzers will send garbage.
29
+
30
+ ## Anti-pattern: the "readable" cursor
31
+
32
+ `?after_id=4711&after_ts=...` looks friendlier but freezes your sort semantics forever: adding a
33
+ tiebreaker, changing direction defaults, or switching the sort column becomes a breaking change.
34
+ Opaque tokens keep the contract at "pass back what we gave you".
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ // Runnable check for the opaque-cursor contract (encode -> decode -> tamper -> version).
3
+ // Usage: node scripts/check-cursor.mjs (exits 0 on pass, 1 on first failure)
4
+ import { createHmac } from 'node:crypto';
5
+
6
+ const KEY = 'demo-key-rotate-me';
7
+ const VERSION = 'v1';
8
+
9
+ const b64u = (buf) => Buffer.from(buf).toString('base64url');
10
+ const sign = (s) => createHmac('sha256', KEY).update(s).digest('hex').slice(0, 20);
11
+
12
+ export function encodeCursor(keyValues, direction = 'desc') {
13
+ const payload = b64u(JSON.stringify({ k: keyValues, d: direction }));
14
+ return `${VERSION}.${payload}.${sign(VERSION + payload)}`;
15
+ }
16
+
17
+ export function decodeCursor(token) {
18
+ const [version, payload, mac] = String(token).split('.');
19
+ if (version !== VERSION) return { error: 'CURSOR_INVALID' }; // unknown version
20
+ if (!payload || sign(version + payload) !== mac) return { error: 'CURSOR_INVALID' };
21
+ try {
22
+ const { k, d } = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
23
+ if (!Array.isArray(k) || !['asc', 'desc'].includes(d)) return { error: 'CURSOR_INVALID' };
24
+ return { keyValues: k, direction: d };
25
+ } catch {
26
+ return { error: 'CURSOR_INVALID' };
27
+ }
28
+ }
29
+
30
+ let failed = 0;
31
+ const check = (name, ok) => {
32
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${name}`);
33
+ if (!ok) failed = 1;
34
+ };
35
+
36
+ // Round trip preserves full timestamp precision and the tiebreaker.
37
+ const token = encodeCursor(['2026-07-01T12:00:00.412Z', 4711]);
38
+ const out = decodeCursor(token);
39
+ check('round-trip', out.keyValues?.[1] === 4711 && out.keyValues?.[0].endsWith('.412Z'));
40
+ check('url-safe (no + / =)', !/[+/=]/.test(token));
41
+
42
+ // Tampered payload must be rejected, not half-parsed.
43
+ const [v, p, m] = token.split('.');
44
+ check('tamper detected', decodeCursor(`${v}.${p.slice(0, -2)}xx.${m}`).error === 'CURSOR_INVALID');
45
+
46
+ // Unknown version and garbage are client errors, never crashes.
47
+ check('unknown version rejected', decodeCursor(`v9.${p}.${m}`).error === 'CURSOR_INVALID');
48
+ check('garbage rejected', decodeCursor('not-a-cursor').error === 'CURSOR_INVALID');
49
+ check('empty rejected', decodeCursor('').error === 'CURSOR_INVALID');
50
+
51
+ process.exit(failed);
@@ -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.