Advanced (Diperbarui: 1/6/2026)

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 caching dengan Claude Code: TTL, invalidasi, dan anti-stampede

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.

DataContoh keyTTLInvalidasiRisiko
Daftar produk publikclaudecodelab:v1:products:list:id5 menitHapus setelah update produkPerubahan harga jangan hanya menunggu TTL
Detail artikelclaudecodelab:v1:posts:item:{slug}10 menitPublish, edit, atau unpublishJangan cache draft dan preview
Statistik adminclaudecodelab:v1:analytics:daily:{date}30 detikBiasanya cukup TTLBukan sumber kebenaran akuntansi
API eksternalclaudecodelab:v1:exchange-rate:usd-idr1-15 menitTTL atau refresh manualCek ketentuan provider
Data user loginDefault dikecualikan0 detikTidak adaJangan 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 KEYS di 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.

#Claude Code #Redis #cache #Node.js #performance
Gratis

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.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.