Menurunkan latency API dengan strategi caching berlapis: cache di aplikasi dengan Redis, HTTP caching di CDN, cache invalidation yang benar, dan optimasi query untuk menghindari beban berulang ke database

Setelah di episode 8 kode kalian teruji, pertanyaan berikutnya: apakah cepat? User tidak peduli seberapa bagus arsitektur kalian — mereka peduli halaman/API yang merespons dalam milidetik. Dan sebagian besar latency di backend bukan berasal dari logika, melainkan dari query database yang diulang-ulang untuk data yang sama.
Mengapa caching penting? Karena membaca dari memori (Redis) ribuan kali lebih cepat daripada query database: Redis bisa melayani ratusan ribu request per detik dengan latency submilidetik, sedangkan query database mahal (I/O disk + parsing + komputasi). Episode ini membangun strategi caching berlapis — cache aplikasi, HTTP cache di CDN, dan optimasi query — plus aturan paling penting: cache invalidation.
Lihat pola paling umum di backend: daftar produk yang sering dibuka semua user, tapi diambil dari database setiap request.
router.get("/v1/products", async (req, res) => {
const products = await db.query(
"SELECT * FROM products WHERE stock > 0 ORDER BY created_at DESC LIMIT 50",
)
res.json({ data: products.rows })
})Jika katalog dibaca 1.000 request/detik, database mengeksekusi query yang sama 1.000 kali — padahal datanya jarang berubah. Inilah pemborosan yang dibunuh cache.
Pola paling umum adalah cache-aside: cek cache dulu, jika kosong ambil dari database lalu simpan ke cache.
import { Redis } from "ioredis"
const redis = new Redis(process.env.REDIS_URL!)
const TTL_SECONDS = 300
async function getFeaturedProducts() {
const cached = await redis.get("catalog:featured")
if (cached) {
return JSON.parse(cached)
}
const { rows } = await db.query(
"SELECT * FROM products WHERE stock > 0 ORDER BY created_at DESC LIMIT 50",
)
await redis.set("catalog:featured", JSON.stringify(rows), "EX", TTL_SECONDS)
return rows
}Istilah penting: cache hit (data ditemukan di cache, cepat) dan cache miss (harus ke database, lambat). Tujuan caching adalah menaikkan hit ratio.
"There are only two hard things in computer science: cache invalidation and naming things."
TTL (time-to-live) adalah pengaman dasar — data kadaluarsa otomatis. EX 300 berarti data dianggap valid 5 menit. Tapi TTL tidak menangani update segera: jika admin mengubah produk, cache lama tetap terlayani hingga TTL habis. Dua pendekatan:
router.patch("/v1/products/:id", async (req, res) => {
const { id } = req.params
const { rows } = await db.query(
`UPDATE products SET name = $1, price = $2
WHERE id = $3 RETURNING *`,
[req.body.name, req.body.price, id],
)
await redis.del("catalog:featured")
await redis.del(`catalog:product:${id}`)
res.json({ data: rows[0] })
})Update database dan cache dalam satu alur — lebih konsisten, tapi lebih kompleks dan rawan error jika setengah gagal. TTL tetap dipakai sebagai jaring pengaman.
Note
Aturan emas invalidation: cache dihapus pada saat data diubah, bukan saat data dibaca. Selalu daftar key mana yang dipengaruhi oleh sebuah write — katalog produk, detail produk, dan statistik bisa jadi key berbeda yang semuanya harus dihapus saat satu produk berubah.
Caching tidak berhenti di aplikasi. HTTP sendiri punya mekanisme cache yang memungkinkan browser atau CDN menyimpan respons, sehingga request tidak sampai ke server sama sekali.
res.set("Cache-Control", "public, max-age=300")max-age=300 — respons valid 5 menit; client/CDN tidak perlu minta ulang.If-None-Match, server membalas 304 Not Modified jika sama.CDN (Content Delivery Network) menempatkan salinan konten di edge server dekat user. Cocok untuk konten yang publik dan jarang berubah — gambar, halaman statis, katalog produk. Data personal (keranjang, order) tidak boleh di-cache publik.
User (Jakarta) → CDN edge (SG) → Origin server (ID)
Latency dengan CDN: jauh lebih kecil untuk konten cacheable| Tipe data | Strategi | Cache-Control / TTL |
|---|---|---|
| Katalog produk | CDN + Redis + ETag | max-age 300 |
| Session user | Redis (private) | private, no-store |
| Keranjang | Redis per user | private, no-store |
| Harga/stock | Redis, invalidasi pada write | TTL pendek 30-60s |
| Static assets | CDN | max-age 31536000 |
Cache tidak menyelesaikan semuanya — saat cache miss, query harus tetap cepat. Tiga optimasi dasar dari episode 5 yang sering cukup:
WHERE/JOIN/ORDER BY ter-index.JOIN atau batch.Tanpa JOIN: 1 query products + 50 query stock (51 query)
Dengan JOIN: 1 query (SELECT p.*, s.qty FROM products p JOIN stock s ...)SELECT * menarik semua kolom termasuk yang tidak dipakai.Warning
Ukur dulu sebelum mengoptimasi: identifikasi query terberat dengan EXPLAIN ANALYZE dan monitoring (episode 11). Jangan meng-cache data yang jarang dibaca atau mengoptimasi query yang tidak pernah jadi bottleneck — itu membuang waktu. Cache yang salah justru menambah kompleksitas dan bug kesegaran data.
Saat cache miss pada trafik tinggi, ribuan request menyerbu database bersamaan. Solusi: locking (satu request yang mengisi cache, lainnya menunggu) atau stale-while-revalidate.
Meng-cache respons berisi data user di CDN = kebocoran data. Data personal harus private, no-store (episode 19 soal compliance).
Stok yang kadaluarsa TTL-nya panjang membuat user membeli barang yang sudah habis. Pilih TTL sesuai seberapa cepat data berubah — stok pendek, katalog sedang, static assets panjang.
Tanpa metrik cache hit/miss, kalian tidak tahu apakah caching bekerja. Kirim header X-Cache: HIT/MISS dan ukur — episode 11.
# sebelum (miss)
curl -s -o /dev/null -w "latency: %{time_total}s\n" http://localhost:3000/v1/products
# sesudah (hit)
curl -s -o /dev/null -w "latency: %{time_total}s\n" http://localhost:3000/v1/productsPerbedaan pertama vs kedua menunjukkan kekuatan cache. Kalian akan melihat latency turun dari puluhan milidetik ke submilidetik.
Episode 9 membangun strategi caching berlapis: cache-aside dengan Redis untuk data panas, HTTP caching + CDN untuk konten publik, optimasi query untuk miss, dan invalidation sebagai disiplin utamanya.
Inti yang harus dibawa pulang:
private, no-store.Di episode 10 selanjutnya kita akan melepas beban dari request: message queues & async — RabbitMQ, Kafka, dan worker untuk memproses pekerjaan berat di latar belakang. Sampai jumpa di episode 10!