Setiap kali memanggil Cache::put(), kita wajib mengambil satu keputusan: berapa lama entri boleh hidup. Angka ini — TTL, time-to-live — adalah kontrak antara kecepatan dan kesegaran data: terlalu panjang, pengguna melihat angka basi; terlalu pendek, cache nyaris tidak pernah kena. Pada chapter ini kita bedah dua bentuk penulisan TTL, jebakan nilai null versus 0, method yang menyimpan selamanya, dan cara menguji kedaluwarsa tanpa benar-benar menunggu.

Dua Bentuk TTL #

Argumen ketiga put() menerima jumlah detik atau objek yang mengimplementasikan DateTimeInterface seperti Carbon. Runner berikut menyimpan dua entri — satu dengan TTL berupa detik, satu dengan objek Carbon — lalu membandingkan kolom expiration di tabel cache:

php
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Carbon;

Carbon::setTestNow('2026-01-01 10:00:00');

Cache::forget('t:detik');
Cache::forget('t:carbon');
Cache::put('t:detik', 'a', 300);                    // 300 detik
Cache::put('t:carbon', 'b', now()->addMinutes(10)); // 10 menit

foreach (['t:detik', 't:carbon'] as $k) {
    $row = DB::table('cache')->where('key', 'like', "%{$k}")->first();
    echo $k . ': expiration=' . $row->expiration
        . ' (' . Carbon::createFromTimestamp($row->expiration)->format('H:i:s') . ")\n";
}

Output yang Dihasilkan:

text
t:detik: expiration=1767261900 (10:05:00)
t:carbon: expiration=1767262200 (10:10:00)

Laravel menormalkan kedua bentuk menjadi timestamp absolut saat penulisan, sehingga hasilnya identik secara format. Bentuk now()->addMinutes(10) lebih ekspresif saat TTL dinyatakan dalam satuan waktu (“sepuluh menit dari sekarang”), sedangkan angka mentah lebih ringkas untuk konstanta seperti 300. Gunakan yang paling jelas dibaca di konteksnya.

Perilaku tersebut bisa kita buktikan langsung. Penulisan dengan null menghasilkan entri yang tetap terbaca, sementara penulisan dengan 0 tidak meninggalkan apa pun:

php
Cache::forget('t:null');
Cache::forget('t:nol');
Cache::put('t:null', 'simpan selamanya', null);
Cache::put('t:nol', 'kedaluwarsa langsung', 0);

echo 'ttl_null_get=' . var_export(Cache::get('t:null'), true) . "\n";
echo 'ttl_nol_get=' . var_export(Cache::get('t:nol'), true) . "\n";

Output yang Dihasilkan:

text
ttl_null_get='simpan selamanya'
ttl_nol_get=NULL

forever() dan Keluarganya #

Untuk menyatakan niat “selamanya” secara eksplisit, Laravel menyediakan Cache::forever($key, $value) dan Cache::rememberForever($key, $callback) alih-alih memakai null. Pada driver database, keduanya diimplementasikan sebagai TTL raksasa 315.360.000 detik — sekitar sepuluh tahun — yang terlihat jelas di kolom expiration:

php
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Carbon;

Carbon::setTestNow('2026-01-01 10:00:00');
Cache::forget('t:forever');
Cache::forever('t:forever', 'konfigurasi');

$row = DB::table('cache')->where('key', 'like', '%t:forever')->first();
echo 'forever_db: value=' . var_export($row->value, true)
    . ' expiration=' . $row->expiration . "\n";

Carbon::setTestNow(Carbon::now()->addYears(5));
echo 'setelah_5_tahun=' . var_export(Cache::get('t:forever'), true) . "\n";
Carbon::setTestNow();

Output yang Dihasilkan:

text
forever_db: value='s:11:"konfigurasi";' expiration=2082621600
setelah_5_tahun='konfigurasi'

Gunakan forever() hanya untuk data yang tidak berubah tanpa deploy ulang, misalnya hasil konversi file yang deterministik. Data apa pun yang bisa berubah harus punya TTL sebagai jaring pengaman — alasannya jelas di chapter Cache Invalidation .

add() dan Sifat Atomiknya #

Cache::add() memeriksa keberadaan kunci dan menulisnya dalam satu operasi, sehingga dua request yang berebut kunci yang sama tidak saling menimpa — hanya satu yang menang:

php
Cache::forget('t:add');
$add1 = Cache::add('t:add', 'pertama', 60);
$add2 = Cache::add('t:add', 'kedua', 60);

echo 'add1=' . var_export($add1, true) . ' add2=' . var_export($add2, true)
    . ' nilai=' . Cache::get('t:add') . "\n";

Output yang Dihasilkan:

text
add1=true add2=false nilai=pertama

Perbedaannya dengan put(): add() menghormati entri yang sudah ada, sementara put() selalu menimpa tanpa bertanya. Pada driver database, atomisitas ini dijamin insertOrIgnore() yang menolak baris dengan primary key ganda.

Menguji Kedaluwarsa dengan Waktu Palsu #

Menunggu 600 detik sungguhan di dalam test jelas tidak praktis. Laravel menyediakan dua alat untuk memalsukan waktu: Carbon::setTestNow() di runner biasa, dan $this->travel() di feature test. Feature test berikut memajukan jam melewati TTL tanpa menunggu:

php
public function test_kedaluwarsa_dengan_travel(): void
{
    Cache::put('kota', 'Bandung', 600);

    $this->travel(599)->seconds();
    $detik599 = Cache::get('kota');

    $this->travel(2)->seconds(); // total 601 detik, melewati TTL
    $detik601 = Cache::get('kota');

    fwrite(STDERR, "travel: detik_599={$detik599} detik_601="
        . var_export($detik601, true) . "\n");
}

Output yang Dihasilkan:

text
travel: detik_599=Bandung detik_601=NULL

Pada detik ke-599 entri masih hidup, dan dua detik kemudian kunci kota menghilang. Batas kedaluwarsa memang dievaluasi terhadap timestamp saat pembacaan, bukan dijadwalkan dihapus tepat waktu.

Ke Mana Entri Kedaluwarsa Pergi #

Di driver database, baris kedaluwarsa tidak dihapus oleh timer apa pun — ia dipangkas secara malas saat kunci itu dibaca lagi. Kita bisa membuktikannya dengan mengintip tabel sebelum dan sesudah pembacaan:

php
Carbon::setTestNow('2026-01-01 10:00:00');
Cache::forget('t:prune');
Cache::put('t:prune', 'x', 30);
$ada = DB::table('cache')->where('key', 'like', '%t:prune')->exists();

Carbon::setTestNow(Carbon::now()->addSeconds(60));
echo 'sebelum_dibaca_baris_ada=' . var_export($ada, true)
    . ' get=' . var_export(Cache::get('t:prune'), true) . "\n";

$masih = DB::table('cache')->where('key', 'like', '%t:prune')->exists();
echo 'setelah_dibaca_baris_ada=' . var_export($masih, true) . "\n";
Carbon::setTestNow();

Output yang Dihasilkan:

text
sebelum_dibaca_baris_ada=true get=NULL
setelah_dibaca_baris_ada=false

Enam puluh detik setelah TTL habis, barisnya masih menumpuk di tabel sampai get() membacanya — saat itulah Laravel membuangnya sambil mengembalikan NULL. Artinya tabel cache yang sibuk dengan kunci sekali-pakai bisa menumpuk baris mati; Cache::flush() berkala atau TTL yang tidak terlalu panjang menjaga tumpukan itu tetap wajar. Perintah cache:prune-stale-tags yang tersedia di artisan menangani pembersihan khusus tag pada Redis, bukan baris biasa.

Memilih TTL per Jenis Data #

TTL bukan angka seragam untuk semua kunci. Berikut pegangan praktis berdasarkan jenis data:

Jenis dataTTL khasAlasan
Statistik dashboard5–15 menitAngka yang meleset beberapa menit tidak fatal
Response API pihak ketigaMengikuti batas rate limitMenghormati kuota dan memangkas latensi
Daftar halaman publik1–5 menit + invalidasi eventPerlu segar saat konten berubah
Konfigurasi/hasil konversi deterministikforever() + invalidasi eksplisitTidak berubah tanpa deploy
Data sekali-pakai (token)1–15 menit + pull()Dibaca sekali lalu hilang

Prinsipnya: makin mahal komputasi dan makin jarang berubah, makin layak TTL panjang. TTL adalah keputusan bisnis tentang “berapa lama data basi masih bisa diterima” — pertanyaan yang jawabannya berbeda tiap kunci.

Ketika jumlah kunci membludak, menghapus satu-satu berdasarkan nama menjadi rapuh: satu kunci lupa dihapus berarti data basi bertahan sampai TTL habis. Untuk itulah Laravel punya cache tags — kelompok kunci yang bisa dihapus massal, materi chapter Cache Tags . Seluruh perilaku TTL di atas dirujuk di dokumentasi Cache .