Cache favicon yang bandel biasanya terjadi karena favicon dan aset kecil lain diperlakukan sebagai aset statis yang aman di-cache lama oleh browser, CDN, atau proxy. Solusi paling stabil bukan memaksa semua cache untuk lupa, melainkan membuat URL aset berubah saat kontennya berubah, lalu melakukan purge CDN secara terkontrol untuk kasus yang memang membutuhkan invalidasi cepat.

Eksperimen seperti menyimpan website di dalam favicon menunjukkan sesuatu yang menarik: favicon bukan sekadar ikon kecil. Ia adalah resource HTTP yang bisa membawa byte apa pun, bisa di-cache, dan bisa dibaca ulang oleh klien. Dari sudut pandang operasional, ini juga peringatan: jangan sembarangan memakai aset statis seperti favicon sebagai pembawa state, konfigurasi, atau data yang harus selalu segar.

Mengapa cache favicon sering terasa lebih bandel?

Favicon sering diminta otomatis oleh browser melalui URL umum seperti /favicon.ico, meskipun halaman tidak selalu merujuknya secara eksplisit. Browser juga dapat menyimpan ikon untuk tab, bookmark, riwayat, shortcut, atau UI internal. Akibatnya, perubahan favicon kadang tidak terlihat meskipun file di origin sudah diganti.

Di sisi infrastruktur, favicon hampir selalu dianggap aset statis. CDN, reverse proxy, dan konfigurasi web server sering memberi cache header panjang untuk file kecil seperti .ico, .png, .svg, .css, dan .js. Ini baik untuk performa, tetapi berbahaya jika URL yang sama dipakai untuk konten yang berubah makna.

Masalah umumnya bukan hanya pada favicon. Pola yang sama berlaku untuk aset kecil lain seperti:

  • config.json yang dibaca frontend saat boot.
  • manifest aplikasi web seperti site.webmanifest.
  • file feature flag statis.
  • sprite SVG, logo, atau file branding.
  • file bootstrap yang menentukan endpoint API atau versi aplikasi.

Aturan praktis: jika konten harus berubah dan klien harus segera melihat perubahan itu, jangan bergantung pada URL statis yang di-cache lama tanpa strategi versioning dan invalidasi.

Risiko memakai aset statis sebagai pembawa state atau config

Menyimpan konfigurasi di file statis terlihat sederhana: deploy file, biarkan CDN menyajikannya cepat, frontend membacanya saat startup. Namun pendekatan ini memiliki risiko jika konsistensi datanya penting.

  • Stale config: sebagian pengguna membaca versi lama, sebagian lain membaca versi baru, tergantung cache browser, CDN POP, atau proxy.
  • Rollback sulit diprediksi: mengganti file di origin tidak menjamin semua edge cache langsung berubah.
  • Debugging membingungkan: developer melihat file baru via origin, tetapi user masih menerima file lama dari CDN.
  • Race condition deploy: HTML sudah menunjuk aset baru, tetapi manifest atau aset terkait belum tersedia di semua lokasi.
  • Keamanan dan compliance: konfigurasi sensitif tidak boleh disimpan di aset publik, meskipun namanya tampak tidak penting seperti favicon.

Untuk konfigurasi yang benar-benar dinamis, pertimbangkan API dengan cache pendek, ETag, atau mekanisme feature flag yang didesain untuk konsistensi operasional. Untuk aset statis yang berubah hanya saat rilis, gunakan versioning berbasis konten.

Strategi utama: versioning dengan content hash

Pola paling aman untuk aset statis adalah content-addressed asset: nama file atau URL mengandung hash dari isi file. Jika isi berubah, URL berubah. Jika URL berubah, browser dan CDN tidak perlu menebak apakah kontennya baru.

Contoh:

/assets/favicon.2f8a9c.ico
/assets/app.91df20.js
/assets/config.4b18aa.json

Dengan pola ini, Anda bisa memberi cache header yang agresif karena URL tersebut merepresentasikan konten yang tidak berubah.

Cache-Control: public, max-age=31536000, immutable

immutable memberi sinyal bahwa selama masa cache berlaku, konten pada URL itu tidak akan berubah. Ini cocok untuk file dengan content hash. Sebaliknya, jangan gunakan header ini untuk URL stabil seperti /favicon.ico jika file tersebut bisa diganti tanpa mengubah URL.

Peran manifest versi

Karena nama file berubah, aplikasi perlu tahu URL terbaru. Biasanya build pipeline menghasilkan manifest versi yang memetakan nama logis ke file hasil build.

{
  'favicon.ico': '/assets/favicon.2f8a9c.ico',
  'app.js': '/assets/app.91df20.js',
  'config.json': '/assets/config.4b18aa.json'
}

Server-side rendering, template HTML, atau proses deploy dapat membaca manifest ini untuk menghasilkan tag yang benar:

<link rel='icon' href='/assets/favicon.2f8a9c.ico'>
<script src='/assets/app.91df20.js' defer></script>

Jika Anda tetap ingin mendukung /favicon.ico karena browser sering mencarinya secara otomatis, jadikan URL itu sebagai redirect atau file kompatibilitas dengan cache pendek, bukan sumber utama yang di-cache setahun.

# Contoh kebijakan konseptual
/assets/favicon.<hash>.ico  -> Cache-Control: public, max-age=31536000, immutable
/favicon.ico                 -> Cache-Control: public, max-age=300

Trade-off-nya: content hash membutuhkan pipeline build yang rapi dan manifest yang konsisten. Namun sebagai gantinya, Anda mengurangi kebutuhan purge manual dan menghindari banyak masalah cache yang sulit direproduksi.

Purge CDN: gunakan queue, bukan panggilan langsung dari deploy

Content hash mengurangi kebutuhan purge, tetapi tidak menghapusnya sepenuhnya. Anda masih mungkin perlu purge untuk URL stabil seperti HTML entry point, /favicon.ico kompatibilitas, /site.webmanifest, atau endpoint statis yang tidak mudah diberi hash.

Jangan jadikan purge CDN sebagai panggilan sinkron yang menentukan sukses atau gagal deploy utama. API CDN bisa lambat, rate-limited, atau mengalami gangguan sementara. Lebih aman jika deploy mencatat permintaan purge ke queue, lalu worker memprosesnya dengan retry, deduplikasi, observability, dan kontrol konkurensi.

Contoh alur deploy yang lebih aman

  1. Build menghasilkan aset dengan content hash dan asset-manifest.
  2. Aset baru diunggah ke object storage atau origin.
  3. Manifest rilis baru disimpan secara atomik atau diberi versi.
  4. HTML atau server template mulai merujuk manifest baru.
  5. Deploy membuat job purge untuk URL stabil yang perlu invalidasi.
  6. Worker purge memproses job secara idempotent dan mencatat hasilnya.
  7. Canary atau synthetic test memverifikasi bahwa edge cache sudah menyajikan versi yang diharapkan.

Dengan alur ini, kegagalan purge tidak langsung merusak rilis aset hashed. Dampaknya dibatasi pada URL stabil yang memang membutuhkan invalidasi.

Worker purge yang aman: idempotent dan tidak balapan

Worker purge harus dirancang dengan asumsi job bisa dikirim dua kali, diproses ulang setelah timeout, atau berjalan paralel di beberapa instance. Ini normal pada sistem queue.

Prinsip pentingnya:

  • Idempotency key: setiap request purge memiliki identitas stabil, misalnya gabungan environment, release ID, dan daftar URL yang dinormalisasi.
  • Status persisten: simpan status pending, running, succeeded, atau failed di database.
  • Distributed lock: hanya satu worker boleh memproses purge untuk scope yang sama pada satu waktu.
  • Retry dengan backoff: retry error sementara, tetapi jangan retry tanpa batas tanpa alarm.
  • Deduplikasi URL: normalisasi path, hapus duplikasi, dan batasi ukuran batch sesuai batas provider CDN.

Pseudocode berikut menggambarkan idenya tanpa bergantung pada provider CDN tertentu:

function handlePurgeJob(job):
    purgeKey = hash(job.environment + ':' + job.releaseId + ':' + sort(job.urls))

    existing = purgeRepository.findByKey(purgeKey)
    if existing.status == 'succeeded':
        return

    lockKey = 'cdn-purge:' + job.environment
    lock = distributedLock.acquire(lockKey, ttl = 120 seconds)
    if not lock.acquired:
        queue.release(job, delay = 30 seconds)
        return

    try:
        purgeRepository.markRunning(purgeKey)

        urls = normalizeAndDeduplicate(job.urls)
        result = cdnClient.purge(urls)

        if result.accepted:
            purgeRepository.markSucceeded(purgeKey, providerRequestId = result.requestId)
        else:
            purgeRepository.markFailed(purgeKey, reason = result.error)
            queue.retryWithBackoff(job)
    finally:
        lock.release()

Distributed lock mencegah dua worker mengirim purge yang sama atau saling menimpa status. TTL lock penting agar lock tidak menggantung selamanya jika worker mati. Namun TTL juga harus cukup panjang untuk durasi operasi normal. Jika proses purge bisa lebih lama dari TTL, gunakan mekanisme perpanjangan lock atau pecah job menjadi batch yang lebih kecil.

Kesalahan umum pada worker purge

  • Menganggap queue menjamin tepat satu kali eksekusi. Banyak queue menjamin setidaknya satu kali, sehingga idempotency tetap wajib.
  • Mengunci terlalu luas, misalnya seluruh sistem global, sehingga semua purge dari environment berbeda ikut tertahan.
  • Mengunci terlalu sempit, sehingga dua job untuk release yang sama tetap bisa berjalan bersamaan.
  • Tidak menyimpan request ID dari provider CDN, sehingga sulit melakukan investigasi saat ada laporan stale cache.
  • Melakukan purge wildcard secara berlebihan. Ini praktis, tetapi dapat meningkatkan cache miss dan membebani origin.

Observability: tahu versi apa yang sedang disajikan

Masalah cache sulit ditangani jika Anda tidak bisa menjawab pertanyaan sederhana: user menerima versi yang mana, dari mana, dan melalui cache layer apa?

Tambahkan metadata versi yang aman untuk diekspos:

  • X-Release-Id pada HTML atau respons config.
  • ETag atau hash konten untuk aset yang tidak menggunakan nama hash.
  • Header debug internal dari CDN jika tersedia dan aman digunakan.
  • Log purge berisi release ID, daftar URL, waktu request, status, dan provider request ID.
  • Metric seperti jumlah job purge pending, durasi purge, retry count, dan failure rate.

Untuk aset hashed, versi biasanya sudah terlihat dari URL. Untuk URL stabil seperti /favicon.ico atau /site.webmanifest, tambahkan cara lain untuk mengidentifikasi konten, misalnya ETag yang berasal dari hash file.

ETag: W/'favicon-2f8a9c'
Cache-Control: public, max-age=300

Hindari mengekspos informasi sensitif seperti nama branch internal, nama engineer, token, atau detail infrastruktur pada header publik.

Rollback: jangan hanya mengganti file lama

Rollback aset statis lebih aman jika rilis lama masih tersedia dengan URL hashed yang sama. Karena URL hashed bersifat immutable, Anda tidak perlu menimpa file lama. Cukup kembalikan pointer manifest atau konfigurasi server ke manifest rilis sebelumnya.

Alur rollback yang disarankan:

  1. Pastikan aset rilis sebelumnya belum dihapus dari storage.
  2. Aktifkan kembali manifest versi sebelumnya secara atomik.
  3. Purge hanya URL stabil yang menjadi entry point, misalnya HTML, /site.webmanifest, atau /favicon.ico kompatibilitas.
  4. Jalankan synthetic test untuk memastikan HTML menunjuk aset lama yang masih tersedia.

Jangan melakukan rollback dengan menimpa /assets/app.91df20.js menggunakan isi berbeda. Itu melanggar kontrak immutable dan dapat menghasilkan bug yang sangat sulit dilacak karena URL sama menyajikan konten berbeda di cache layer berbeda.

Cara menguji konsistensi cache

Pengujian cache perlu mencakup origin, CDN, dan perilaku browser. Satu kali refresh di browser developer tidak cukup, apalagi untuk favicon.

1. Verifikasi header dan konten dari CLI

Gunakan request HEAD atau GET untuk memeriksa cache header, ETag, dan indikasi apakah respons datang dari CDN cache. Nama header CDN berbeda-beda, jadi fokus pada pola, bukan nama tertentu.

curl -I https://example.com/assets/favicon.2f8a9c.ico
curl -I https://example.com/favicon.ico
curl -s https://example.com/site.webmanifest

Pastikan aset hashed memiliki cache panjang dan immutable, sedangkan URL stabil memiliki cache yang lebih pendek atau dapat dipurge.

2. Bandingkan versi yang diharapkan dengan versi aktual

Synthetic test dapat mengambil HTML, membaca URL favicon atau manifest, lalu memastikan hash-nya cocok dengan release yang aktif.

function checkRelease(baseUrl, expectedRelease):
    html = http.get(baseUrl).body
    release = extractHeaderOrMeta(html, 'release-id')

    if release != expectedRelease:
        fail('HTML release mismatch')

    faviconUrl = extractFaviconUrl(html)
    favicon = http.get(resolve(baseUrl, faviconUrl))

    if sha256(favicon.body) != expectedFaviconHash:
        fail('Favicon content mismatch')

Jalankan test dari beberapa region jika aplikasi Anda dilayani oleh CDN global. Tujuannya bukan membuktikan semua edge sempurna, tetapi mendeteksi propagasi yang lambat atau purge yang gagal.

3. Uji skenario race condition

Simulasikan dua deploy berdekatan: release A lalu release B. Pastikan worker purge tidak memproses job lama dengan cara yang merusak release baru. Idempotency key dan lock per environment membantu, tetapi sistem tetap perlu menyimpan urutan rilis atau memvalidasi release aktif sebelum melakukan aksi berisiko.

if job.releaseId != releaseRepository.currentRelease(job.environment):
    markSkipped(job, reason = 'stale purge job')
    return

Logika ini berguna untuk purge URL stabil. Jika job lama datang terlambat, worker dapat melewatinya atau membatasi purge pada URL yang aman.

Checklist implementasi praktis

  • Gunakan content hash untuk favicon utama dan aset statis lain yang berubah saat rilis.
  • Terapkan Cache-Control: public, max-age=31536000, immutable hanya untuk URL hashed.
  • Gunakan cache pendek atau redirect terkontrol untuk /favicon.ico kompatibilitas.
  • Simpan manifest versi dan aktifkan manifest baru secara atomik.
  • Proses purge CDN melalui queue, bukan sebagai langkah sinkron yang rapuh.
  • Buat worker purge idempotent, memakai distributed lock, retry terukur, dan status persisten.
  • Catat release ID, ETag, request ID CDN, durasi purge, retry, dan error.
  • Pastikan rollback mengembalikan pointer manifest, bukan menimpa aset immutable.
  • Uji konsistensi dari origin, CDN, beberapa region, dan browser profil bersih.

Kesimpulan

Favicon memang kecil, tetapi perilaku cache-nya bisa berdampak besar jika dijadikan pembawa state, konfigurasi, atau sinyal versi aplikasi. Pendekatan paling aman adalah memperlakukan aset statis sebagai immutable melalui content hash, lalu mengelola URL stabil dengan cache pendek dan purge CDN yang terkontrol.

Untuk operasi produksi, detail seperti queue, worker idempotent, distributed lock, observability, dan rollback bukan tambahan mewah. Itu adalah mekanisme yang membuat perubahan aset kecil tetap dapat diprediksi saat melewati browser, CDN, dan banyak edge cache yang tidak selalu bergerak serempak.