DEV Community

Cover image for API Caching dengan ETag dan Cache-Control: Meminimalkan Payload via Permintaan Kondisional
Walse
Walse

Posted on Originally published at apidog.com

API Caching dengan ETag dan Cache-Control: Meminimalkan Payload via Permintaan Kondisional

Caching HTTP untuk API: Cache-Control, ETag, dan 304 Not Modified

API Anda mungkin mengirimkan JSON yang sama ribuan kali sehari. Klien meminta GET /v1/products/42, menerima 18 KB, lalu meminta ulang lima menit kemudian dengan hasil yang sama. Anda tetap membayar bandwidth, serialisasi, dan pembacaan database. HTTP menyediakan solusinya: Cache-Control menentukan berapa lama respons tetap sesuai (fresh), sedangkan ETag memberi klien sidik jari untuk memeriksa perubahan. Bersama-sama, keduanya mengubah permintaan berulang menjadi respons 304 Not Modified tanpa badan, sekaligus melindungi penulisan dari pembaruan yang hilang. Jika Anda sudah membaca panduan caching respons API di React, artikel ini membahas sisi servernya: tiga lapisan caching HTTP, alur 304, perbedaan no-cache dan no-store, implementasi Express, serta cara memverifikasinya di Apidog.

Coba Apidog hari ini

Tiga Lapisan Caching HTTP

Caching HTTP untuk API terdiri dari tiga keputusan terpisah. Masalah muncul ketika tim mencampurnya.

1. Kesesuaian (freshness)

Berapa lama klien dapat menggunakan respons yang tersimpan tanpa menghubungi server?

Cache-Control: max-age=60
Enter fullscreen mode Exit fullscreen mode

Selama 60 detik, klien menggunakan salinan lokal tanpa lalu lintas jaringan. Ini adalah cache hit termurah, tetapi klien tidak dapat mendeteksi perubahan sampai masa berlaku habis.

2. Validasi

Setelah respons kedaluwarsa, klien tidak harus mengunduhnya lagi. Klien cukup bertanya apakah sumber daya berubah dengan mengirimkan sidik jari sebelumnya.

Jika tidak berubah, server mengembalikan 304 Not Modified tanpa badan. ETag dan If-None-Match lebih presisi, sedangkan Last-Modified dan If-Modified-Since menggunakan stempel waktu dengan granularitas satu detik.

3. Pembatalan validasi (invalidation)

Ketika data berubah, bagaimana salinan kedaluwarsa dibuang?

  • Cache pribadi klien kedaluwarsa melalui max-age.
  • Cache bersama dan CDN memerlukan penghapusan eksplisit atau TTL singkat.
  • stale-while-revalidate membatasi berapa lama salinan kedaluwarsa boleh digunakan.

Kesesuaian menghemat paling banyak permintaan, validasi menangani permintaan yang melewati masa berlaku, dan pembatalan validasi menjaga salinan tetap benar. Sebagian besar API membutuhkan ketiganya.

Cara Kerja Perjalanan Bolak-balik 304 Not Modified

Berikut siklus lengkap untuk endpoint produk.

Permintaan pertama

Klien belum memiliki salinan cache:

GET /v1/products/42 HTTP/1.1
Host: api.example.com
Enter fullscreen mode Exit fullscreen mode

Respons pertama

Server mengembalikan badan dan metadata caching:

HTTP/1.1 200 OK
Cache-Control: private, max-age=60
ETag: "33a64df551425fcc55e4d42a148795d9f2"
Content-Type: application/json
Content-Length: 18432
Enter fullscreen mode Exit fullscreen mode

Klien menyimpan badan dan nilai ETag. Selama 60 detik berikutnya, klien tidak menghubungi server.

Permintaan kedua setelah 60 detik

Salinan kedaluwarsa, sehingga klien melakukan validasi ulang:

GET /v1/products/42 HTTP/1.1
Host: api.example.com
If-None-Match: "33a64df551425fcc55e4d42a148795d9f2"
Enter fullscreen mode Exit fullscreen mode

Respons kedua ketika sumber daya tidak berubah

Server membandingkan ETag yang masuk dengan nilai saat ini. Karena cocok, server mengembalikan:

HTTP/1.1 304 Not Modified
Cache-Control: private, max-age=60
ETag: "33a64df551425fcc55e4d42a148795d9f2"
Enter fullscreen mode Exit fullscreen mode

Respons 304 tidak memiliki badan. Alih-alih mengirim ulang 18 KB, server hanya mengirim beberapa ratus byte header. Klien menandai salinan lokal sebagai fresh selama 60 detik berikutnya dan menyajikannya.

Jika produk berubah, server mengembalikan 200 OK dengan badan dan ETag baru. Baca penjelasan 304 Not Modified untuk detail status ini. Singkatnya, 304 adalah instruksi cache, bukan kesalahan.

Permintaan GET kondisional tetap membutuhkan perjalanan bolak-balik, autentikasi, dan perhitungan ETag. Yang dihilangkan adalah transfer payload dan penguraian ulang di sisi klien. Untuk endpoint daftar besar yang sering di-polling oleh perangkat seluler, pendekatan ini dapat memangkas egress API sebesar 60–90 persen.

Direktif Cache-Control Penting untuk API

Untuk API JSON, lima direktif berikut paling penting.

no-store vs no-cache

no-store berarti respons tidak boleh disimpan di cache mana pun. Gunakan untuk data yang benar-benar sensitif, seperti:

  • Token
  • Data perbankan
  • PII yang tidak boleh dipertahankan

no-cache berarti cache boleh menyimpan respons, tetapi harus melakukan validasi ulang ke origin sebelum digunakan kembali. Jika dipasangkan dengan ETag, no-cache tetap menghasilkan penghematan melalui respons 304, sekaligus memastikan klien tidak menampilkan data kedaluwarsa.

Menggunakan no-store pada semua endpoint “demi keamanan” menonaktifkan permintaan kondisional dan memaksa transfer payload penuh pada setiap panggilan.

private

private menandai respons agar hanya dapat di-cache oleh klien pengguna akhir, bukan cache bersama atau CDN.

Respons yang berbeda untuk setiap pengguna—terutama sebagian besar API terautentikasi—harus menyertakan private. Tanpanya, proxy yang salah konfigurasi dapat menyajikan data akun satu pengguna kepada pengguna lain.

max-age

max-age menentukan masa berlaku freshness dalam detik. Untuk API, angka kecil seperti 30–300 detik biasanya cukup. Tujuannya bukan menghilangkan permintaan selama sehari, melainkan menyerap ledakan trafik dan loop polling.

stale-while-revalidate

Direktif ini memberikan kompromi yang pragmatis:

Cache-Control: max-age=60, stale-while-revalidate=300
Enter fullscreen mode Exit fullscreen mode

Cache boleh menyajikan salinan yang sudah kedaluwarsa hingga lima menit tambahan sambil memperbaruinya di latar belakang. Pengguna mendapat respons instan, sementara origin diperbarui sesaat kemudian. CDN seperti Cloudflare dan Fastly, serta peramban modern, mendukung pola ini.

Default yang masuk akal untuk endpoint baca terautentikasi:

Cache-Control: private, max-age=60, stale-while-revalidate=120
ETag: "9f8b2c41aa73e0d5"
Enter fullscreen mode Exit fullscreen mode

Spesifikasi lengkapnya tersedia di RFC 9111, yang menggantikan RFC 7234 sebagai referensi caching HTTP.

ETag Kuat vs Lemah

ETag memiliki dua jenis. Awalan W/ menandai ETag lemah.

ETag: "33a64df551425fcc"
ETag: W/"33a64df551425fcc"
Enter fullscreen mode Exit fullscreen mode

ETag kuat

ETag kuat menjamin kesetaraan byte demi byte. Dua respons dengan ETag kuat yang sama identik. Karena itu, ETag kuat aman untuk permintaan rentang byte dan diperlukan untuk kontrol konkurensi dengan If-Match.

ETag lemah

ETag lemah menjamin kesetaraan semantik. Byte respons dapat berbeda—misalnya urutan bidang berubah atau stempel waktu diperbarui—selama maknanya tetap sama. Cache tetap dapat menggunakan salinan tersebut.

Middleware kompresi dapat menyebabkan masalah. Nginx dan beberapa framework mengubah ETag kuat menjadi ETag lemah ketika melakukan gzip secara on-the-fly, karena byte terkompresi tidak lagi sama dengan byte asli.

Jika pemeriksaan konkurensi gagal secara misterius di balik proxy, periksa apakah awalan W/ ditambahkan oleh middleware.

Gunakan ETag kuat yang dihitung dari badan tidak terkompresi sebagai default. Gunakan ETag lemah hanya ketika Anda memang menyajikan beberapa representasi dari data yang sama.

Membuat ETag: Hash Badan vs Kolom Versi

Dua strategi umum memiliki trade-off berbeda.

Hash badan respons

Serialisasikan respons, buat hash, lalu bungkus hasilnya dengan tanda kutip:

  • MD5 atau SHA-1 cukup untuk sidik jari; ini bukan batas keamanan.
  • Tidak membutuhkan perubahan skema.
  • Akurat secara konstruksi.

Kekurangannya, server harus membangun respons lengkap pada setiap permintaan, termasuk permintaan yang berakhir dengan 304. Bandwidth berkurang, tetapi komputasi dan beban database belum tentu.

Kolom versi atau updated_at

Ambil ETag dari data yang murah diakses:

ETag: "42-v17"
Enter fullscreen mode Exit fullscreen mode

Nilai tersebut dapat berasal dari penghitung versi baris atau hash updated_at. Permintaan kondisional kini hanya membutuhkan satu pencarian terindeks, bukan serialisasi penuh.

Kekurangannya, versi harus diperbarui setiap kali ada perubahan yang memengaruhi respons, termasuk perubahan pada tabel yang di-join. Jika satu perubahan terlewat, server dapat mengembalikan 304 untuk data yang sebenarnya sudah kedaluwarsa—bug caching yang sulit terlihat.

Mulailah dengan hash badan karena benar secara default. Pindahkan endpoint yang sangat sibuk ke ETag berbasis versi setelah profiling menunjukkan serialisasi sebagai bottleneck.

ETag untuk Konkurensi Optimistis: If-Match dan 412

Sidik jari yang menghemat bandwidth saat membaca juga dapat mencegah pembaruan yang hilang saat menulis.

Misalnya, dua admin memuat produk 42 secara bersamaan. Admin A mengubah harga dan menyimpannya. Tiga puluh detik kemudian, Admin B menyimpan koreksi typo berdasarkan data lama dan tanpa sengaja menimpa perubahan harga A.

Solusinya adalah membuat setiap pembaruan bergantung pada versi terakhir yang dilihat klien:

PUT /v1/products/42 HTTP/1.1
If-Match: "33a64df551425fcc55e4d42a148795d9f2"
Content-Type: application/json
Enter fullscreen mode Exit fullscreen mode

Server membandingkan If-Match dengan ETag sumber daya saat ini:

  • Cocok: terapkan pembaruan dan kembalikan 200 dengan ETag baru.
  • Tidak cocok: kembalikan 412 Precondition Failed dan jangan ubah data.
  • Header tidak ada: API yang ketat dapat mengembalikan 428 Precondition Required.

Klien kemudian mengambil versi terbaru, menerapkan ulang perubahan pada data yang fresh, dan mencoba lagi. Dengan begitu, korupsi data diam-diam berubah menjadi status HTTP eksplisit yang dapat ditangani.

Perilaku CDN dan Proxy

Cache bersama berada di antara origin dan klien, tetapi tetap membaca header yang sama.

  • private mengecualikan respons dari caching CDN. s-maxage=600 dapat menetapkan TTL khusus CDN yang berbeda dari max-age untuk peramban.
  • Sebagian besar CDN melakukan validasi ulang ke origin menggunakan permintaan kondisional. Jika origin menjawab dengan 304, CDN memperbarui metadata salinan tanpa mengunduh badan lagi.
  • Pastikan framework mengirim Vary dengan benar. Jika satu URL dapat mengembalikan JSON dan CSV, gunakan Vary: Accept agar cache bersama tidak memberikan CSV kepada klien yang meminta JSON.
  • Perhatikan proxy yang melemahkan ETag karena kompresi.

Contoh Express: Mengembalikan ETag dan Menangani If-None-Match

Express mengatur ETag lemah secara otomatis. Penanganan manual berikut menghasilkan ETag kuat dan menyediakan jalur penulisan dengan 412:

import crypto from "node:crypto";
import express from "express";

const app = express();
app.use(express.json());

function etagFor(payload) {
  const hash = crypto.createHash("sha1")
    .update(JSON.stringify(payload))
    .digest("hex");
  return `"${hash}"`;
}

app.get("/v1/products/:id", async (req, res) => {
  const product = await db.products.find(req.params.id);
  const etag = etagFor(product);

  res.set("Cache-Control", "private, max-age=60, stale-while-revalidate=120");
  res.set("ETag", etag);

  if (req.get("If-None-Match") === etag) {
    return res.status(304).end();   // fingerprint matches: no body
  }
  res.json(product);
});

app.put("/v1/products/:id", async (req, res) => {
  const product = await db.products.find(req.params.id);
  const currentEtag = etagFor(product);
  const ifMatch = req.get("If-Match");

  if (!ifMatch) {
    return res.status(428).json({ error: "If-Match header required" });
  }
  if (ifMatch !== currentEtag) {
    return res.status(412).json({ error: "Resource changed since you fetched it" });
  }

  const updated = await db.products.update(req.params.id, req.body);
  res.set("ETag", etagFor(updated));
  res.json(updated);
});
Enter fullscreen mode Exit fullscreen mode

Cabang 304 tetap mengirimkan header Cache-Control dan ETag. Sesuai RFC 9111, respons 304 memperbarui metadata respons yang tersimpan. Kirim ulang header yang diperlukan klien untuk menjaga salinannya tetap fresh.

Memverifikasi Caching di Apidog

Kode dapat terlihat benar, tetapi middleware dan proxy mungkin mengubah perilaku caching. Uji pada tingkat HTTP, bukan hanya pada tingkat kode.

Di Apidog, pemeriksaan manualnya dapat dilakukan dalam sekitar satu menit:

  1. Kirim GET /v1/products/42. Buka header respons dan pastikan ETag serta Cache-Control tersedia. Pastikan nilai ETag dikutip, lalu salin nilainya.
  2. Tambahkan header berikut pada permintaan yang sama dan kirim ulang:
   If-None-Match: "33a64df551425fcc55e4d42a148795d9f2"
Enter fullscreen mode Exit fullscreen mode

Anda harus menerima 304 dengan badan kosong. Jika masih menerima 200, lapisan validasi belum membandingkan sidik jari.

  1. Ubah data produk, kirim ulang permintaan, dan pastikan server mengembalikan 200 dengan ETag baru.

Agar pemeriksaan ini berjalan pada setiap deploy, ubah menjadi skenario pengujian:

  1. Permintaan pertama mengekstrak ETag dari header respons ke variabel.
  2. Permintaan kedua mengirim variabel tersebut sebagai If-None-Match dan memeriksa status 304 serta badan kosong.
  3. Permintaan ketiga mengirim PUT dengan nilai If-Match yang sengaja kedaluwarsa, misalnya "deadbeefcafe1234", lalu memastikan statusnya 412.

Panduan API assertions menjelaskan sintaks assertion untuk status dan header.

Jalankan skenario tersebut di CI. Perubahan middleware yang diam-diam menghilangkan ETag akan menghasilkan pipeline gagal, bukan tagihan bandwidth yang membengkak. Unduh Apidog secara gratis dan uji endpoint Anda sendiri.

FAQ

Apa perbedaan no-cache dan no-store?

no-store melarang caching sepenuhnya. Tidak ada data yang ditulis ke disk atau memori, sehingga setiap permintaan mengunduh respons lengkap.

no-cache mengizinkan penyimpanan, tetapi memaksa validasi ulang sebelum setiap penggunaan kembali. Jika dipasangkan dengan ETag, respons 304 dan penghematan payload tetap dapat digunakan.

Gunakan no-store hanya untuk data sensitif. Memakainya pada semua endpoint adalah salah satu kesalahan Cache-Control paling mahal untuk API.

Apakah ETag berfungsi dengan POST?

Biasanya tidak, dan itu memang disengaja. ETag menggambarkan keadaan sumber daya pada URL, sedangkan POST biasanya membuat sesuatu yang baru, bukan membaca keadaan stabil. Cache juga umumnya tidak menyimpan respons POST.

Header kondisional yang penting untuk penulisan adalah If-Match pada PUT, PATCH, dan DELETE. Jika Anda ingin menyimpan respons POST, pertimbangkan apakah operasi tersebut seharusnya menjadi GET.

Apakah respons 304 membuat API lebih cepat?

304 membuat transfer data lebih kecil, tetapi tidak otomatis mengurangi semua pekerjaan server. Server tetap menerima permintaan, menjalankan autentikasi, dan menghitung ETag saat ini.

Penghematan CPU origin bergantung pada seberapa murah sidik jari tersebut diperoleh. Keuntungan terbesar biasanya terlihat pada bandwidth, baterai seluler, dan waktu rendering di jaringan lambat.

Ukur sebelum dan sesudah. Panduan pengujian kinerja API menjelaskan cara menguji latensi dan throughput.

Sebaiknya menggunakan ETag atau Last-Modified?

Jika memungkinkan, kirim keduanya.

ETag lebih presisi karena menangkap perubahan subdetik dan perubahan konten yang mungkin dilewatkan oleh stempel waktu. Jika keduanya dikirim, If-None-Match lebih diutamakan daripada If-Modified-Since.

Last-Modified tetap berguna sebagai fallback untuk klien lama dan sebagai heuristik yang digunakan beberapa cache. Jika hanya dapat mengirim satu, pilih ETag.

Referensi

Top comments (0)