Mengaktifkan framework cache Django dengan backend LocMem, Redis, dan Memcached, menerapkan per-view dan per-template caching, lalu memindahkan session ke Redis agar login cepat dan aplikasi siap scale horizontal.

Setelah API berdiri di episode 11, pertanyaan berikutnya adalah performa: halaman blog di-render ulang untuk setiap request padahal isinya jarang berubah. Di episode ini kita menerapkan Cache & Sessions — dua mekanisme yang membuat aplikasi terasa instan dan tetap scalable.
Mengapa topik ini penting? Caching adalah strategi performa dengan rasio usaha-hasil terbaik: sekali render, dibaca ribuan kali tanpa menyentuh database. Session adalah kebalikannya — data sementara per-user yang harus tetap cepat dan bisa dibagikan antar server saat aplikasi di-scale horizontal (episode 24). Keduanya sering menggunakan infrastruktur yang sama: Redis.
Django punya sistem cache terpadu dengan banyak backend. Pilihannya, dari yang paling sederhana:
| Backend | Kinerja | Persistensi | Cocok untuk |
|---|---|---|---|
LocMemCache | Sedang | Per-proses, hilang saat restart | Development, single worker |
DatabaseCache | Rendah | Ya (di DB) | Lab tanpa infra tambahan |
FileBasedCache | Rendah | Ya (file) | Sistem tanpa memori proses |
MemcachedCache | Tinggi | Tidak | Produksi klasik |
RedisCache | Tinggi | Opsional (persist) | Produksi modern + session/celery |
Konfigurasi Redis di settings.py:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"TIMEOUT": 300,
"KEY_PREFIX": "devblog",
}
}KEY_PREFIX memisahkan key cache antar aplikasi di Redis yang sama. TIMEOUT default 300 detik — jangan set terlalu lama jika data berubah cepat.
Note
LocMemCache sempurna untuk development karena tanpa server tambahan, tetapi berbahaya di produksi: ia disimpan per-proses worker, jadi cache worker A tidak terlihat worker B — hasilnya cache yang tidak konsisten. Saat kalian menjalankan beberapa worker Gunicorn (episode 23), pastikan backend sudah pindah ke Redis.
Pola paling umum: cache-aside. Baca dari cache; jika miss, ambil dari database dan isi cache:
from django.core.cache import cache
def category_posts(category_slug, timeout=300):
key = f"category:{category_slug}:post_ids"
ids = cache.get(key)
if ids is None:
ids = list(
Post.objects.filter(category__slug=category_slug, status="published")
.values_list("id", flat=True)
)
cache.set(key, ids, timeout)
return Post.objects.filter(id__in=ids).select_related("author")cache.set(key, value, timeout) dengan None artinya default. Perhatikan kita cache daftar id (ringan), bukan objek utuh — objek tetap diambil via query saat dibutuhkan, menghindari cache yang menyimpan referensi stale.
Untuk halaman yang identik untuk semua user, cache seluruh respons view:
from django.views.decorators.cache import cache_page
@cache_page(60 * 15) # 15 menit
def popular_posts(request):
...cache_page men-cache HttpResponse lengkap selama 15 menit. Hati-hati: view yang menampilkan data per-user (nama user, keranjang) tidak boleh di-cache per-halaman tanpa Vary header yang benar.
Jika hanya sebagian halaman yang mahal dirender (misal sidebar "post populer"), cache potongan template:
{% load cache %}
{% cache 600 popular_posts %}
{% for post in popular_posts %}
<li><a href="{% url 'blog:post_detail' post.pk %}">{{ post.title }}</a></li>
{% endfor %}
{% endcache %}Sintaks {% cache timeout key %} men-cache blok template berdasarkan key. Untuk cache per-user, tambahkan fragment key unik:
{% cache 60 sidebar user.id %}
...
{% endcache %}Untuk query yang berat dan jarang berubah, kombinasi cache + annotate:
from django.core.cache import cache
def get_stats():
stats = cache.get("blog:stats")
if stats is None:
stats = {
"posts": Post.objects.count(),
"comments": Comment.objects.count(),
"authors": User.objects.filter(is_active=True).count(),
}
cache.set("blog:stats", stats, 600)
return statsIni menyelamatkan tiga query COUNT per request. Ingat prinsip dari episode 5: jangan taruh query mahal di context processor — taruh di cache.
Sesi login (episode 10) disimpan di database by default (django.contrib.sessions.backends.db). Saat traffic naik, tabel django_session jadi beban, dan aplikasi multi-worker butuh sesi yang shared — bukan tersimpan per-worker. Pindahkan ke Redis:
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"django.contrib.sessions.backends.cache menyimpan session langsung di backend cache (default = Redis dari konfigurasi di atas). Hasilnya:
SESSION_COOKIE_AGE dan Redis TTL.Warning
Hati-hati dengan backends.cache: jika Redis direstart dan tanpa persistensi, semua session hilang — pengguna ter-logout massal. Untuk produksi, aktifkan Redis persistence (RDB/AOF) atau pertimbangkan cache_db backend yang menulis ke cache dan database sekaligus. Ini juga alasan SESSION_COOKIE_AGE sebaiknya tetap wajar (misal 2 minggu), bukan setahun.
Atur kebijakan session di settings:
SESSION_COOKIE_AGE = 60 * 60 * 24 * 14 # 14 hari
SESSION_COOKIE_HTTPONLY = True # default: mencegah akses JS
SESSION_COOKIE_SECURE = False # jadi True di produksi HTTPS (episode 18)
SESSION_EXPIRE_AT_BROWSER_CLOSE = FalseSESSION_COOKIE_HTTPONLY = True (default Django) membuat cookie session tidak bisa dibaca JavaScript — pertahanan penting melawan XSS (episode 18). SESSION_COOKIE_SECURE baru diaktifkan saat produksi ber-HTTPS.
Uji cache dan session berjalan:
python manage.py shell -c "
from django.core.cache import cache
cache.set('probe', 42, 60)
print(cache.get('probe'))
"
redis-cli ping
redis-cli -n 1 keys 'devblog:*' | headredis-cli ping harus menjawab PONG. Key devblog:* di DB 1 Redis berisi entri cache — bukti bahwa Django benar-benar memakai Redis, bukan backend lain.
Inti yang harus dibawa pulang:
@cache_page untuk seluruh halaman; {% cache %} untuk potongan template.backends.cache) = cepat + shared antar worker — syarat scale horizontal.Di episode 13 selanjutnya kita memindahkan pekerjaan berat ke latar belakang: Background Tasks & Celery — Celery dengan Redis, tasks, beat scheduler, dan result backend untuk async email, laporan, dan job terjadwal. Sampai jumpa di episode 13!