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.
- package/README.md +14 -0
- package/bin/vanara.js +66 -2
- package/catalog/agents/api-designer/AGENT.md +156 -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 +184 -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 +128 -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 +172 -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 +201 -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 +184 -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 +179 -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 +193 -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 +179 -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 +162 -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,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.
|