Redis caching dengan Claude Code: TTL, invalidasi, dan anti-stampede
Panduan praktis Redis caching dengan Claude Code: desain key, TTL, invalidasi, kode Node.js, test, dan checklist review.
Redis adalah key-value store cepat yang menyimpan data di memori. Artinya, data berada di RAM dan diambil memakai key, sehingga cocok untuk agregasi database, respons API eksternal, ranking, session singkat, dan rate limit. Namun Redis juga mudah disalahgunakan: jika Claude Code hanya diberi instruksi “tambahkan cache Redis”, hasilnya bisa berupa harga lama, stok tidak sinkron, data pengguna masuk shared cache, atau Redis produksi tersendat karena scan key yang terlalu luas.
Artikel ini memberi workflow yang bisa langsung diberikan ke Claude Code: cache policy, desain key, TTL, invalidasi, pencegahan cache stampede, implementasi Node.js, test, dan checklist review. Untuk gambaran cache lintas layer, baca juga Claude Code caching strategies. Jika Redis juga dipakai untuk background job, lihat Claude Code queue system.
flowchart LR
Request["HTTP request"] --> Cache["Redis cache"]
Cache -->|hit| Response["Fast response"]
Cache -->|miss| Lock["Short lock"]
Lock --> Loader["DB or external API"]
Loader --> Cache
Loader --> Response
Admin["Update event"] --> Invalidate["Delete known keys"]
Invalidate --> Cache
Mulai dari cache policy
Sebelum menulis kode, simpan policy di CLAUDE.md atau di prompt tugas. Claude Code dapat membaca alur teknis, tetapi tidak bisa menebak berapa lama data boleh stale secara bisnis.
| Data | Contoh key | TTL | Invalidasi | Risiko |
|---|---|---|---|---|
| Daftar produk publik | claudecodelab:v1:products:list:id | 5 menit | Hapus setelah update produk | Perubahan harga jangan hanya menunggu TTL |
| Detail artikel | claudecodelab:v1:posts:item:{slug} | 10 menit | Publish, edit, atau unpublish | Jangan cache draft dan preview |
| Statistik admin | claudecodelab:v1:analytics:daily:{date} | 30 detik | Biasanya cukup TTL | Bukan sumber kebenaran akuntansi |
| API eksternal | claudecodelab:v1:exchange-rate:usd-idr | 1-15 menit | TTL atau refresh manual | Cek ketentuan provider |
| Data user login | Default dikecualikan | 0 detik | Tidak ada | Jangan masuk shared cache |
Aturan praktisnya: data publik, bisa diulang, dan bisa dibuat ulang cocok untuk Redis. Permission, billing, authentication, dan data pribadi harus dikecualikan kecuali ada desain keamanan khusus.
Prompt untuk Claude Code
Tambahkan layer Redis cache-aside ke aplikasi Node.js ini.
Requirement:
1. Gunakan package resmi node-redis bernama redis
2. Buat key dengan format claudecodelab:v1:{domain}:{resource}:{id}
3. Pilih TTL dari cache policy dan tambahkan jitter sampai 10%
4. Pada update, hapus related key yang diketahui hanya setelah DB write berhasil
5. Jangan gunakan KEYS di production code; gunakan known keys, SCAN, atau related-key sets
6. Cegah cache stampede pada key populer dengan short lock
7. Tambahkan node:test untuk key generation, rentang TTL, getOrSet, dan concurrent miss
Kembalikan:
- File yang berubah
- Test yang dijalankan
- Data yang sengaja tidak di-cache dan alasannya
Prompt ini cocok digabung dengan Claude Code code review checklist supaya review tidak berhenti pada “lebih cepat”, tetapi juga mengecek “bisa invalidasi dengan benar”.
Implementasi Node.js
Client resmi Node.js adalah package redis; pola koneksinya ada di node-redis guide.
mkdir redis-cache-demo
cd redis-cache-demo
npm init -y
npm install redis
docker run --name redis-cache-demo -p 6379:6379 -d redis:7-alpine
Pusatkan key dan TTL dalam satu file.
// cache-policy.js
const CACHE_PREFIX = "claudecodelab";
const CACHE_VERSION = "v1";
const CACHE_POLICY = {
productList: { ttl: 300, jitter: 30 },
productItem: { ttl: 600, jitter: 60 },
dailyStats: { ttl: 30, jitter: 5 },
};
function normalizePart(value) {
const part = String(value).trim().toLowerCase();
if (part.length === 0) {
throw new Error("cache key part must not be empty");
}
return encodeURIComponent(part);
}
function cacheKey(parts) {
if (!Array.isArray(parts) || parts.length === 0) {
throw new Error("cacheKey requires a non-empty parts array");
}
return [CACHE_PREFIX, CACHE_VERSION, ...parts.map(normalizePart)].join(":");
}
function ttlWithJitter(baseSeconds, maxJitterSeconds = 30) {
if (!Number.isInteger(baseSeconds) || baseSeconds <= 0) {
throw new Error("base TTL must be a positive integer");
}
const jitter = Math.max(0, Math.floor(maxJitterSeconds));
return baseSeconds + Math.floor(Math.random() * (jitter + 1));
}
module.exports = { CACHE_POLICY, cacheKey, ttlWithJitter };
Gunakan satu Redis client bersama.
// redis-client.js
const { createClient } = require("redis");
const redis = createClient({
url: process.env.REDIS_URL || "redis://localhost:6379",
});
redis.on("error", (error) => {
console.error("Redis Client Error", error);
});
let connecting;
async function getRedis() {
if (redis.isOpen) return redis;
if (!connecting) {
connecting = redis.connect();
}
await connecting;
return redis;
}
async function closeRedis() {
if (redis.isOpen) {
await redis.quit();
}
connecting = undefined;
}
module.exports = { getRedis, closeRedis };
Helper berikut memakai cache-aside. Ia membaca Redis lebih dulu, menjalankan loader hanya saat miss, menyimpan JSON dengan TTL, dan memakai SET NX PX sebagai lock singkat agar request paralel tidak menyerbu database saat key populer expire.
// redis-cache.js
const { randomUUID } = require("node:crypto");
const UNLOCK_SCRIPT = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end
return 0
`;
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
class RedisJsonCache {
constructor(redis, options = {}) {
this.redis = redis;
this.defaultTtl = options.defaultTtl || 300;
this.lockMs = options.lockMs || 5000;
this.waitMs = options.waitMs || 50;
this.waitRetries = options.waitRetries || 10;
}
async get(key) {
const raw = await this.redis.get(key);
if (raw === null) return null;
try {
return JSON.parse(raw);
} catch {
await this.redis.del(key);
return null;
}
}
async set(key, value, ttlSeconds = this.defaultTtl) {
if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
throw new Error("ttlSeconds must be a positive integer");
}
await this.redis.set(key, JSON.stringify(value), { EX: ttlSeconds });
}
async invalidate(keys) {
const list = Array.isArray(keys) ? keys : [keys];
if (list.length === 0) return 0;
return this.redis.del(list);
}
async getOrSet(key, ttlSeconds, loader) {
const cached = await this.get(key);
if (cached !== null) {
return { value: cached, cacheStatus: "hit" };
}
const lockKey = `${key}:lock`;
const token = randomUUID();
const acquired = await this.redis.set(lockKey, token, { NX: true, PX: this.lockMs });
if (acquired === "OK") {
try {
const fresh = await loader();
await this.set(key, fresh, ttlSeconds);
return { value: fresh, cacheStatus: "miss" };
} finally {
await this.redis.eval(UNLOCK_SCRIPT, { keys: [lockKey], arguments: [token] });
}
}
for (let attempt = 0; attempt < this.waitRetries; attempt += 1) {
await sleep(this.waitMs);
const afterWait = await this.get(key);
if (afterWait !== null) {
return { value: afterWait, cacheStatus: "hit-after-wait" };
}
}
const fallback = await loader();
await this.set(key, fallback, Math.max(5, Math.floor(ttlSeconds / 3)));
return { value: fallback, cacheStatus: "miss-after-timeout" };
}
}
module.exports = { RedisJsonCache };
Demo ini bisa dijalankan langsung. Pada aplikasi nyata, ganti loadProductsFromDb() dengan Prisma, Supabase, atau API client. Lihat juga Prisma ORM with Claude Code dan Supabase integration with Claude Code.
// demo-products.js
const { CACHE_POLICY, cacheKey, ttlWithJitter } = require("./cache-policy");
const { getRedis, closeRedis } = require("./redis-client");
const { RedisJsonCache } = require("./redis-cache");
const db = {
products: [
{ id: "p1", locale: "id", name: "Template CLAUDE.md", price: 9, published: true },
{ id: "p2", locale: "id", name: "Pelatihan Claude Code", price: 199, published: true },
],
};
async function loadProductsFromDb(locale) {
await new Promise((resolve) => setTimeout(resolve, 80));
return db.products.filter((product) => product.locale === locale && product.published);
}
async function listPublishedProducts(cache, locale) {
const key = cacheKey(["products", "list", locale]);
const ttl = ttlWithJitter(CACHE_POLICY.productList.ttl, CACHE_POLICY.productList.jitter);
return cache.getOrSet(key, ttl, () => loadProductsFromDb(locale));
}
async function main() {
const redis = await getRedis();
const cache = new RedisJsonCache(redis);
const first = await listPublishedProducts(cache, "id");
const second = await listPublishedProducts(cache, "id");
console.log({ firstStatus: first.cacheStatus, secondStatus: second.cacheStatus, products: second.value });
await cache.invalidate([cacheKey(["products", "list", "id"])]);
await closeRedis();
}
main().catch(async (error) => {
console.error(error);
await closeRedis();
process.exitCode = 1;
});
node demo-products.js
Test yang wajib ada
Cache bug sering tidak terlihat karena response tetap cepat. Minimal test key, TTL, cache hit, dan concurrent miss.
// redis-cache.test.js
const test = require("node:test");
const assert = require("node:assert/strict");
const { cacheKey, ttlWithJitter } = require("./cache-policy");
const { RedisJsonCache } = require("./redis-cache");
class FakeRedis {
constructor() {
this.store = new Map();
}
async get(key) {
const entry = this.store.get(key);
if (!entry) return null;
if (entry.expiresAt && entry.expiresAt <= Date.now()) {
this.store.delete(key);
return null;
}
return entry.value;
}
async set(key, value, options = {}) {
if (options.NX && (await this.get(key)) !== null) return null;
const ttlMs = options.PX || (options.EX ? options.EX * 1000 : 0);
this.store.set(key, { value, expiresAt: ttlMs ? Date.now() + ttlMs : 0 });
return "OK";
}
async del(keys) {
const list = Array.isArray(keys) ? keys : [keys];
let deleted = 0;
for (const key of list) {
if (this.store.delete(key)) deleted += 1;
}
return deleted;
}
async eval(_script, options) {
const [key] = options.keys;
const [token] = options.arguments;
if ((await this.get(key)) === token) return this.del(key);
return 0;
}
}
test("cacheKey encodes dynamic parts", () => {
assert.equal(cacheKey(["Products", "List", "id/ID"]), "claudecodelab:v1:products:list:id%2Fid");
});
test("ttlWithJitter stays inside the expected range", () => {
for (let i = 0; i < 50; i += 1) {
const ttl = ttlWithJitter(300, 30);
assert.ok(ttl >= 300);
assert.ok(ttl <= 330);
}
});
test("getOrSet caches the first loader result", async () => {
const cache = new RedisJsonCache(new FakeRedis());
let loads = 0;
const first = await cache.getOrSet("products:list", 60, async () => {
loads += 1;
return [{ id: "p1" }];
});
const second = await cache.getOrSet("products:list", 60, async () => {
loads += 1;
return [{ id: "p2" }];
});
assert.equal(first.cacheStatus, "miss");
assert.equal(second.cacheStatus, "hit");
assert.equal(loads, 1);
assert.deepEqual(second.value, [{ id: "p1" }]);
});
test("getOrSet waits instead of running duplicate loaders", async () => {
const cache = new RedisJsonCache(new FakeRedis(), { waitMs: 5, waitRetries: 20 });
let loads = 0;
const loader = async () => {
loads += 1;
await new Promise((resolve) => setTimeout(resolve, 20));
return { total: 42 };
};
const results = await Promise.all([
cache.getOrSet("analytics:daily", 30, loader),
cache.getOrSet("analytics:daily", 30, loader),
]);
assert.equal(loads, 1);
assert.deepEqual(results[0].value, { total: 42 });
assert.deepEqual(results[1].value, { total: 42 });
});
node --test redis-cache.test.js
Use case praktis
Pertama, katalog publik dan daftar artikel. Banyak pembaca melihat respons yang sama, sehingga invalidasi key daftar dan detail cukup jelas.
Kedua, dashboard admin. PV, conversion rate, dan preview revenue bisa stale beberapa detik atau menit. Permission, invoice, dan keputusan keamanan tidak boleh bergantung pada cache ini.
Ketiga, API eksternal. Kurs, cuaca, plan SaaS, dan metadata publik bisa di-cache singkat untuk mengurangi rate limit, selama ketentuan provider mengizinkan.
Keempat, rate limit. INCR dan EXPIRE cocok untuk window pendek, tetapi data autentikasi perlu review keamanan tersendiri.
Kesalahan umum
Key yang tidak lengkap adalah sumber bug paling sering. Jika locale, currency, tenant, role, query, atau version mengubah output, masukkan ke key.
Jangan hapus cache sebelum DB write berhasil. Urutan aman: DB write sukses, hapus Redis key yang diketahui, lalu CDN purge jika perlu.
Hindari KEYS user:* di production. Gunakan known keys, related-key Set, atau maintenance command berbasis SCAN.
Untuk negative cache, simpan objek seperti { found: false }; helper di atas menganggap null sebagai miss.
Checklist review
- Data personal, permission, dan billing dikecualikan atau dijustifikasi
- Key memuat locale, tenant, role, query, dan version jika relevan
- TTL dijelaskan oleh kebutuhan freshness bisnis
- Invalidation dilakukan setelah DB write sukses
- Tidak ada
KEYSdi production code - Lock, jitter, atau fallback mengurangi stampede
- Test mencakup key, TTL, hit, dan concurrent miss
- Hit rate dan fallback terlihat di log atau metrics
Referensi dan langkah berikutnya
Masukkan link resmi ke tugas Claude Code: Redis documentation, node-redis guide, Redis patterns, dan Redis optimization.
Untuk menjadikannya workflow tim, mulai dari produk dan template ClaudeCodeLab. Jika perlu memetakan Redis, CDN, DB write, dan observability ke repo nyata, gunakan pelatihan dan konsultasi Claude Code.
Dalam demo Node.js kecil, Masa melihat request pertama menjalankan loader, request kedua menjadi Redis hit, dan dua concurrent miss hanya menjalankan loader sekali. Keuntungan utamanya bukan hanya Redis, tetapi policy key, TTL, invalidasi, dan review yang ditulis sebelum Claude Code mengubah kode.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Permission receipt Claude Code: mencatat scope, bukti, dan rollback
Pola permission receipt untuk Claude Code: aksi yang diizinkan, batas approval, command verifikasi, rollback, dan cek CTA revenue.
Agent Harness Aman untuk Claude Code dan Codex: Permission, Verifikasi, dan Rollback
Rancang Agent Harness praktis untuk Claude Code dan Codex dengan policy, plan, verification, dan recovery layer.
Subagent Claude Code: panduan praktis untuk delegasi artikel dan kode
Panduan subagent Claude Code untuk membagi pekerjaan artikel dan kode: aturan delegasi, prompt, risiko, dan checklist.