Cache purge-nya sudah dijalankan, dan staging tetap menyajikan build lama. Kombinasi itu yang bikin saya berhenti sebentar. Kalau alat pembersihnya sudah dipakai dan hasilnya nihil, kemungkinannya tinggal dua: yang dibersihkan bukan lapisan yang basi, atau alatnya tidak pernah benar-benar membersihkan apa pun. Malam itu jawabannya ternyata dua-duanya sekaligus.
Yang terukur malam itu
Malam 1 Mei 2026 saya mendorong tiga commit persiapan produksi, dan Railway menyelesaikan auto-deploy-nya tanpa keluhan. Lalu saya ambil header staging apa adanya:
curl -sI https://staging.clientsite.com/cf-cache-status: HIT
age: 1082HIT berarti respons itu tidak pernah menyentuh origin. age memberi tahu berapa lama salinan tersebut sudah duduk di edge, dalam detik. Dibagi 60, 1082 detik adalah 18,03 menit. Jadi selama lebih dari 18 menit setelah deploy, staging masih menyajikan build lama, padahal GitHub Action pembersih cache sudah dijalankan.
Origin segar, yang basi ada di depannya
Langkah berikutnya memisahkan dua kemungkinan tadi. Kalau saya bisa menembus Cloudflare dan mendapat build baru, artinya origin sudah benar dan yang menahan versi lama adalah edge. Cara paling murah menembusnya adalah query string yang belum pernah masuk cache key mana pun:
STAMP=$(date +%s)
curl -sI "https://staging.clientsite.com/?_cb=$STAMP" | grep -i cf-cache-status
curl -s "https://staging.clientsite.com/?_cb=$STAMP" | grep -o 'penanda-build-lama\|penanda-build-baru'Hasilnya cf-cache-status: MISS dan build baru. Saya tidak berhenti di tampilan halaman, saya grep HTML mentahnya: penanda komponen banner dari build lama sudah hilang, dan penanda halaman baru sudah muncul. Origin dan Railway menyajikan yang segar. Cloudflare yang menahan yang basi.
Klaim bypass yang tidak cocok dengan header
Bagian ini perlu saya tulis apa adanya karena arah kerja saya sempat salah gara-gara satu kalimat. Kontak DevOps di sisi klien lebih dulu menyatakan bahwa cache staging sudah di-bypass. Kalau memang begitu, HIT dengan age setinggi tadi tidak mungkin muncul di homepage.
Jadi cache Cloudflare untuk subdomain staging itu tidak sepenuhnya di-bypass, bertentangan dengan pernyataan sebelumnya. Dugaan saya waktu itu, dan sampai sekarang statusnya tetap dugaan karena dasbor Cloudflare-nya bukan saya yang pegang, yang di-bypass kemungkinan route API atau path tertentu, sementara homepage dan halaman HTML tetap tercache di edge. Bypass penuh akan mencegah HIT yang saya ukur.
Alat pembersihnya sendiri keluar dengan kode 1
Sebab keduanya baru kelihatan setelah saya membuka berkas workflow-nya pada 1 Mei 2026 dan membaca urutan panggilan API-nya. Isinya dua langkah:
1. GET /zones?name=$DOMAIN -> ambil ZONE_ID
2. POST /zones/$ZONE_ID/purge_cache -> body {"purge_everything": true}Langkah pertama yang menentukan segalanya. Zone Cloudflare terdaftar terhadap domain root atau apex, bukan terhadap subdomain. Jadi begitu yang dioper ke name= adalah subdomain staging, yang balik adalah array kosong:
$DOMAIN = staging.clientsite.com -> array kosong -> ZONE_ID=null -> exit 1
$DOMAIN = clientsite.com -> zone ketemu -> purge jalanZONE_ID jadi null, workflow keluar dengan kode 1, dan log-nya menulis "Zone ID not found for domain: ...". Ini bukan kegagalan senyap. Pesannya terlihat di log GitHub Actions, cuma memang tab Actions bukan tempat yang orang datangi setelah menekan tombol yang kelihatannya berhasil dipicu.
Yang bikin kesalahan ini gampang dilakukan justru karena intuisinya masuk akal. Kalau yang mau dibersihkan cache staging, wajar rasanya mengetik alamat staging. Padahal purge-nya beroperasi di level zone, dan subdomain di bawah zone ikut ter-flush sebagai bagian dari purge zone root. Jadi input yang benar adalah domain root, dan mengetik subdomain membuat pipeline gagal atau tidak melakukan apa-apa.
Aturan jalan yang saya pakai setelah itu
Sisi klien mengundang saya ke repo GitHub yang memuat pipeline pembersih cache itu, dan akses push serta Actions saya terima 1 Mei 2026. Workflow-nya ada di .github/workflows/purge-cache.yml, dipicu manual lewat workflow_dispatch dari tab Actions. Jalannya lurus: buka repo, masuk tab Actions, pilih workflow purge cache, tekan Run workflow, isi domain root, jalankan.
Untuk flush rutin setelah deploy, saya jalankan sendiri action itu dengan domain root. Tidak perlu eskalasi ke sisi klien kecuali action-nya sendiri yang error. Dan selama bypass belum diperluas ke route HTML, purge manual setelah tiap deploy memang wajib, bukan opsional.
Tiga langkah sebelum menuduh
Dari kejadian ini saya menetapkan urutan pemeriksaan yang cuma tiga langkah. Pertama, curl -sI ke path yang dikeluhkan, lalu baca cf-cache-status. Kalau HIT dengan age tinggi, Cloudflare yang jadi masalah. Kedua, uji bypass dengan ?_cb=$(date +%s). Kalau konten baru muncul, origin baik-baik saja dan yang basi cuma edge, jadi tinggal jalankan purge. Ketiga, kalau uji bypass ikut menampilkan konten lama, berarti origin Railway-nya yang belum ter-deploy, jadi tunggu build-nya selesai atau cek dasbor Railway.
Yang saya ubah dari kebiasaan lama ada di urutan curiganya. Waktu ada laporan "sudah saya edit tapi staging masih lama", cache HIT Cloudflare justru tersangka paling mungkin untuk halaman HTML, bukan tersangka terakhir. Verifikasi dulu sebelum mencoretnya.
Tersangka lain tetap saya catat, dengan status kurang mungkin tapi mungkin: singleton Sanity yang belum di-seed ulang, redirect 301 yang tercache di browser, dan cache ISR Next.js yang tidak direvalidasi karena revalidateTag tidak dipasang di server action yang mengubah datanya.
Harga dari purge_everything
Bagian yang paling tidak nyaman dari solusi ini adalah bentuk purge-nya. purge_everything: true menghapus seluruh cache zone, setiap host dan setiap path, bukan cuma yang staging. Selama produksi belum berbagi zone itu, dampaknya terbatas, dan begitu produksi ikut masuk, ini jadi berbahaya.
Ada satu baris caveat di catatan saya sendiri yang masih berasumsi cache staging sudah di-bypass sehingga workflow ini praktis no-op, dan baris itu bertentangan dengan pengukuran yang saya tulis di bagian lain catatan yang sama. Saya biarkan di sini sebagai pengingat bahwa asumsi lama tidak otomatis ikut terkoreksi begitu buktinya berubah.
Perkiraan saya untuk nanti: begitu produksi rilis di domain root yang sama, menjalankan workflow ini untuk memperbaiki masalah staging akan ikut membuang cache produksi. Yang menyusul adalah banjir cache-miss ke origin Railway, dengan kemungkinan pelambatan singkat atau kena rate limit kalau produksi sudah punya trafik berarti.
Tiga jalur mitigasi yang saya daftar untuk dipertimbangkan sebelum produksi rilis. Pertama, workflow kedua yang purge per host atau per prefix saja, misalnya body {"hosts":["staging.clientsite.com"]} atau {"prefixes":[...]}, sehingga opsi nuklirnya tetap terpisah. Kedua, memisahkan staging ke zone Cloudflare yang berbeda sama sekali. Ketiga, dan ini yang paling minimal, memperlakukan workflow ini sebagai aksi yang berdampak ke produksi begitu produksi hidup, jadi tidak dijalankan sambil lalu.
Untuk produksinya sendiri, arah yang saya ambil bukan purge, tapi TTL pendek yang dikirim dari aplikasi. Route yang sensitif harga mengirim Cache-Control: public, s-maxage=60, stale-while-revalidate=300 dari middleware atau konfigurasi route Next.js, dan Cloudflare menghormatinya tanpa konfigurasi tambahan. Sebagian route bahkan sudah rilis dengan s-maxage=15.
Yang saya bawa pulang
Dua sebab bertumpuk, dan dari luar keduanya kelihatan seperti sebab yang sama. Lapisan yang basi ternyata bukan lapisan yang saya kira sudah di-bypass, dan alat yang seharusnya membersihkannya berhenti di panggilan API pertama karena diberi nama host yang tidak pernah terdaftar sebagai zone.
Kalau sebuah pipeline purge kelihatan sudah dijalankan tapi tidak ada yang berubah, buka log-nya sebelum menyalahkan cache. Kegagalannya sering sudah tertulis di sana dengan kalimat lengkap. Dan kalau pipeline itu menerima nama domain sebagai input, ingat bahwa Cloudflare mendaftarkan zone di apex. Yang perlu diketik adalah domain root, dan subdomain yang ingin dibersihkan akan ikut terbawa dengan sendirinya.
Catatan projeknya sendiri saya perbarui malam itu juga, setelah insiden ketidakcocokan deploy dan cache mengoreksi klaim bypass yang saya bawa sebelumnya.