Strategi Caching di Backend dengan Redis: Implementasi pada Express.js API
Mengapa Backend API Membutuhkan Strategi Caching
Setiap API backend yang melayani request dalam jumlah besar pasti menghadapi masalah latensi dan beban database yang berulang. Bayangkan endpoint /api/products yang dipanggil ribuan kali per detik — tanpa caching, setiap request akan menerjemahkan query database yang sama, membaca data yang identik dari disk, dan mengembalikan response yang tidak berubah. Pola ini membuang resource dan memperlambat waktu respons.
Konsep cache hadir sebagai lapisan penyimpanan sementara yang diletakkan di antara aplikasi dan database. Alih-alih selalu membaca dari database, kita menyimpan hasil query yang sering diakses ke media penyimpanan yang jauh lebih cepat. Redis, sebagai in-memory data store, menjadi pilihan utama untuk tugas ini karena kemampuannya menyediakan akses data dalam waktu sub-milidetik. Operasi baca dari Redis hanya membutuhkan akses ke RAM, berbeda dengan database relasional yang harus membaca dari disk (disk I/O) melalui proses yang lebih berat.
Perbandingannya cukup sederhana: query database rata-rata memakan waktu 10–50 milidetik untuk data sederhana, sedangkan Redis merespons dalam 0.5–2 milidetik. Untuk skenario dengan ribuan request per detik, perbedaan ini menjadi sangat berarti. Redis juga mendukung struktur data yang kaya seperti string, hash, list, set, dan sorted set, sehingga kita bisa menyimpan berbagai bentuk data cache — dari JSON response hingga session user.
Setup Redis dan Koneksi ke Aplikasi Express.js
Langkah pertama adalah menyiapkan Redis server. Cara paling praktis untuk development adalah menggunakan Docker. Jalankan perintah berikut untuk memulai container Redis versi 7-alpine:
docker run -d --name redis-cache -p 6379:6379 redis:7-alpineSetelah Redis berjalan, kita perlu menginstal library ioredis sebagai Redis client untuk Node.js. Library ini menyediakan API yang lengkap, mendukung Promise, dan memiliki fitur retry strategy bawaan.
npm install ioredisSelanjutnya, kita buat file redis.js untuk mengelola koneksi Redis di aplikasi Express.js:
import Redis from 'ioredis';
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
retryStrategy: (times) => {
const delay = Math.min(times * 100, 3000);
return delay;
},
maxRetriesPerRequest: 3
});
redis.on('connect', () => {
console.log('Redis connected successfully');
});
redis.on('error', (err) => {
console.error('Redis connection error:', err);
});
export default redis;Kode di atas mengonfigurasi koneksi Redis dengan retry logic eksponensial. Jika koneksi terputus, ioredis akan mencoba reconnect dengan delay yang meningkat bertahap hingga maksimal 3 detik. Event connect memberi kita konfirmasi bahwa koneksi berhasil, sementara event error menangani skenario ketika Redis tidak tersedia. File redis.js ini bisa diimpor ke route handler mana pun.
Pola Cache-Aside — Logic Read dan Write dengan Redis
Cache-Aside adalah pola caching paling umum dan paling mudah diimplementasikan. Dalam pola ini, aplikasi bertanggung jawab penuh mengelola cache — bukan Redis yang otomatis menyinkronkan data. Alur kerjanya sederhana: aplikasi pertama-tama memeriksa Redis. Jika data ditemukan (cache hit), data langsung dikembalikan. Jika tidak ditemukan (cache miss), aplikasi mengambil data dari database, menyimpannya ke Redis, lalu mengembalikannya.

Gambar: Diagram pola Cache-Aside yang menunjukkan alur cache hit (data ditemukan di cache) dan cache miss (data diambil dari database lalu disimpan ke cache) — Sumber: [Microsoft Learn](https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside)
Berikut implementasi helper getOrSetCache yang mengabstraksi pola ini:
import redis from './redis.js';
export async function getOrSetCache(key, fetchFn, ttl = 60) {
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
const data = await fetchFn();
await redis.set(key, JSON.stringify(data), 'EX', ttl);
return data;
}
MERN Stack Development
Launch your journey into full-stack web development with this comprehensive, pro...
Kita bisa menggunakan helper ini langsung di route handler Express.js:
app.get('/api/products/:id', async (req, res) => {
const { id } = req.params;
const product = await getOrSetCache(
`product:${id}`,
async () => {
return await db.product.findUnique({ where: { id } });
},
300
);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
res.json({ data: product });
});Pada route di atas, kita menggunakan key product:123 untuk menyimpan data produk dengan ID tertentu. TTL 300 detik (5 menit) dipilih sebagai compromise antara freshness data dan pengurangan beban database. Jika dalam 5 menit produk yang sama diminta lagi, Redis akan mengembalikan data tanpa menyentuh database sama sekali. Logic ini mengurangi jumlah query yang harus ditangani database, terutama untuk endpoint populer.
Pemilihan nilai TTL sangat bergantung pada karakteristik data. Data yang jarang berubah seperti daftar kategori bisa menggunakan TTL 1 jam atau lebih. Data yang lebih dinamis seperti stok produk sebaiknya menggunakan TTL yang lebih pendek, misalnya 30–60 detik.
Cache Invalidation Strategy — Menjaga Data Tetap Fresh
Cache invalidation adalah tantangan terbesar dalam strategi caching. Data yang tersimpan di Redis bisa menjadi basi (stale) saat data di database berubah. Kita membutuhkan strategi untuk memastikan cache tetap sinkron dengan database.
Ada tiga pendekatan utama dalam cache invalidation. Pertama, TTL-based expiry — membiarkan Redis menghapus cache secara otomatis setelah waktu tertentu. Kedua, write-through cache — memperbarui cache setiap kali ada perubahan data di database. Ketiga, explicit deletion — menghapus cache yang relevan saat operasi write terjadi.
Setiap pendekatan memiliki trade-off. TTL-based expiry sangat sederhana tapi bisa menyajikan data basi dalam periode TTL. Explicit deletion memberikan konsistensi lebih baik tapi membutuhkan logic tambahan di setiap operasi write. Write-through cache menjaga cache tetap sinkron sempurna namun meningkatkan latensi operasi write.
Pilihan strategi bergantung pada tipe data. Data master seperti daftar kategori atau konfigurasi aplikasi jarang berubah, sehingga TTL-based expiry sudah cukup. Data transaksional seperti stok produk atau status pesanan memerlukan explicit deletion agar konsistensi terjaga.
Berikut contoh implementasi explicit cache invalidation pada route POST:
app.post('/api/products', async (req, res) => {
const product = await db.product.create({ data: req.body });
await redis.del('products:list');
await redis.del(`product:${product.id}`);
await redis.publish('cache:invalidation', JSON.stringify({
pattern: 'product:*',
action: 'create',
productId: product.id
}));
res.status(201).json({ data: product });
});Pada kode di atas, setelah membuat produk baru, kita menghapus cache daftar produk (products:list) dan cache produk individual (product:${id}). Selain itu, kita juga mengirim pesan melalui Redis Pub/Sub agar service lain yang mungkin menyimpan cache produk juga bisa melakukan invalidasi. Pola ini berguna dalam arsitektur microservices di mana beberapa service bisa menyimpan cache untuk data yang sama.
Mencegah Cache Stampede dengan Distributed Lock
Cache stampede terjadi saat banyak request simultan mengenai endpoint yang sama tepat ketika cache telah expired. Semua request tersebut akan mendapati cache miss dan secara bersamaan menuju database, menyebabkan lonjakan beban yang bisa membuat database kewalahan atau bahkan downtime.
Dampak dari cache stampede sangat serius. Latensi API meningkat drastis karena database harus melayani banyak query identik dalam waktu singkat. Pada skala tertentu, kondisi ini bisa menyebabkan efek domino — database melambat, request menumpuk, server kehabisan koneksi, dan akhirnya API tidak responsif.

Gambar: Ilustrasi mekanisme cache stampede dan tiga strategi mitigasi: request coalescing, distributed lock, dan probabilistic early refresh — Sumber: [sujeet.pro](https://github.com/sujeet-pro/sujeet.pro/blob/main/content/articles/caching-fundamentals-and-strategies/README.md)
Solusi untuk masalah ini adalah distributed lock dengan Redis. Ide dasarnya: saat cache miss, request pertama yang datang akan mengamankan lock, mengambil data dari database, dan mengisi cache. Request lain yang datang bersamaan akan melihat lock sedang aktif dan menunggu atau langsung mengambil data lama yang masih tersimpan. Redis mendukung pola ini melalui perintah SET dengan argumen NX (set if not exists) dan PX (expiry dalam milidetik).
Berikut implementasi distributed lock yang aman:
async function acquireLock(key, ttl = 5000) {
const lockKey = `lock:${key}`;
const lockValue = `${Date.now()}-${Math.random()}`;
const acquired = await redis.set(lockKey, lockValue, 'NX', 'PX', ttl);
if (acquired === 'OK') {
return {
value: lockValue,
async release() {
const script = `
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
end
return 0
`;
await redis.eval(script, 1, lockKey, lockValue);
}
};
}
return null;
}Lua script pada method release memastikan bahwa hanya pemilik lock yang bisa melepaskannya. Ini mencegah situasi di mana proses A melepas lock yang sebenarnya dimiliki oleh proses B karena timeout. Kombinasikan lock ini dengan staggered TTL — menambahkan sedikit variasi acak pada nilai TTL cache — untuk menyebarkan waktu expiry dan mengurangi probabilitas stampede secara keseluruhan.
Memonitor Cache Performance — Hit Rate, Memory, dan Latency
Setelah caching berjalan, kita perlu memonitor efektivitasnya. Metrik paling penting adalah cache hit ratio — persentase request yang berhasil dilayani dari cache tanpa menyentuh database. Rasio di atas 90% mengindikasikan strategi caching yang baik. Jika rasio di bawah 70%, kita perlu mengevaluasi kembali pemilihan key, TTL, atau pola caching yang digunakan.
Redis menyediakan command INFO yang menampilkan keyspace_hits dan keyspace_misses untuk menghitung hit ratio, serta used_memory untuk memantau konsumsi RAM. Jalankan redis-cli INFO stats untuk melihat data ini secara real-time.
Selain hit ratio, perhatikan eviction count dan memory fragmentation. Redis memiliki konfigurasi maxmemory yang membatasi penggunaan RAM. Saat batas tercapai, Redis mengeksekusi kebijakan eviction. Kebijakan allkeys-lru cocok untuk sebagian besar use case. Alternatif lain adalah volatile-ttl yang menghapus data dengan TTL terpendek, atau volatile-lru yang hanya bekerja pada key dengan TTL.
Untuk monitoring visual, Redis Insight menyediakan dashboard interaktif yang menampilkan metrik-metrik tersebut dalam bentuk grafik. Jika infrastruktur sudah menggunakan Prometheus, kita bisa menambahkan redis_exporter untuk mengintegrasikan metrik Redis ke dashboard Grafana.
Ketika dataset melebihi kapasitas satu node, pertimbangkan Redis Cluster yang mendistribusikan data ke beberapa node. Setiap node menyimpan sebagian data (sharding), dan routing request ditangani secara transparan.
Implementasi caching dengan Redis bukan sekadar menyimpan data di memory — ini tentang merancang strategi yang tepat: kapan menyimpan cache, kapan menginvalidasinya, bagaimana mencegah stampede, dan bagaimana memonitor efektivitasnya. Mulailah dengan endpoint yang paling sering diakses, pilih pola cache-aside untuk fleksibilitas maksimal, dan tingkatkan kompleksitas secara bertahap sesuai kebutuhan. Pelajari implementasi production-grade lebih dalam di kelas Node.js Lanjutan Rumah Coding.
Kursus Terkait
MERN Stack Development
Launch your journey into full-stack web development with this comprehensive, project-driven course. Designed for beginners, this course demystifies the MERN stack (MongoDB, Express.js, React.js, Node.js) by guiding you step-by-step in building a real-world application from scratch. By the end of this course, you will have the practical skills and a complete portfolio project to confidently step into the modern web development industry.
EduStream - Mini Learning Management System (LMS)
- Secure Authentication & Authorization: Robust user registration and login using JWT, with strict role-based access control (Admin/Instructor vs. Student).
- Course Management (Admin Dashboard): Full CRUD (Create, Read, Update, Delete) capabilities for administrators to manage course details, including titles, descriptions, pricing, and thumbnail image uploads.
- Public Course Catalog: An interactive and responsive storefront where users can browse available courses.