← Kembali ke Blog

Prompt Caching di Production: Pangkas Biaya Input LLM sampai 90%

Kalau aplikasimu mengirim system prompt panjang atau dokumen hasil retrieval yang sama di tiap request, kamu membayar model untuk membaca ulang prefix itu dari nol setiap kali. Bot support yang menembak 10.000 request sehari ke blok instruksi 15.000 token menghitung ulang 150 juta token per hari untuk teks yang tidak pernah berubah.

Prompt caching menyelesaikan itu. Di Anthropic sifatnya opt-in lewat penanda yang kamu sisipkan di request. Di OpenAI jalan otomatis. Di Gemini ada dua mode, otomatis dan manual. Panduan ini membahas penanda di Anthropic, cara memastikan cache benar-benar hit, dan cara memilih masa hidup cache yang pas dengan trafficmu. Perintahnya bisa langsung di-copy-paste dan pengali harga diambil dari dokumentasi resmi tiap provider.

Prasyarat

  • Python 3.10 atau lebih baru
  • Anthropic SDK (pip install anthropic)
  • API key Anthropic di ANTHROPIC_API_KEY
  • Prompt dengan prefix panjang yang stabil. Prefix harus melewati batas minimum cacheable model, yang berkisar 512 sampai 4.096 token tergantung model. Di bawah itu, tidak ada yang di-cache dan kamu tidak dapat error apa pun.

Langkah 1: Baseline tanpa cache

Kirim prompt yang sama dua kali dan perhatikan blok usage.

import anthropic

client = anthropic.Anthropic()
SYSTEM = open("system_prompt.md").read()  # panjang dan stabil

for _ in range(2):
    resp = client.messages.create(
        model="claude-opus-5",
        max_tokens=256,
        system=SYSTEM,
        messages=[{"role": "user", "content": "Summarize section 4."}],
    )
    print(resp.usage)

Dua panggilan itu menagih seluruh system prompt di harga input dasar. Tidak ada yang di-cache, karena kamu belum menandai apa pun sebagai cacheable.

Langkah 2: Tambahkan cache breakpoint

Bungkus system prompt dalam content block dan pasang cache_control. Semua konten sampai dan termasuk blok yang ditandai menjadi prefix yang di-cache.

resp = client.messages.create(
    model="claude-opus-5",
    max_tokens=256,
    system=[
        {
            "type": "text",
            "text": SYSTEM,
            "cache_control": {"type": "ephemeral"},
        }
    ],
    messages=[{"role": "user", "content": "Summarize section 4."}],
)
print(resp.usage)

Request pertama membayar biaya tulis. Setiap request setelahnya, selama masa hidup cache masih berlaku, membayar harga baca.

Langkah 3: Pastikan cache benar-benar hit

Cache miss tidak pernah melempar error. Diam-diam biayanya jadi harga penuh. Blok usage satu-satunya tempat kamu bisa lihat faktanya, dan dia memecah input jadi tiga counter:

  • input_tokens: total jumlah input
  • cache_creation_input_tokens: token yang ditulis ke cache, ditagih 1.25x harga dasar untuk masa hidup 5 menit atau 2x harga dasar untuk 1 jam
  • cache_read_input_tokens: token yang dibaca dari cache, ditagih 0.1x harga dasar

Biaya satu request, dalam satuan harga input dasar, jadi seperti ini:

u = resp.usage
uncached = u.input_tokens - u.cache_read_input_tokens - u.cache_creation_input_tokens
units = uncached * 1.0 + u.cache_creation_input_tokens * 1.25 + u.cache_read_input_tokens * 0.10
print(f"{units:.1f} satuan input dasar")

Di cache yang bekerja, kamu lihat cache_creation_input_tokens di panggilan pertama dan cache_read_input_tokens di panggilan berikutnya. Kalau keduanya tetap nol di request yang identik berulang, berarti prefix-mu berubah di suatu tempat, dan Langkah 4 penyebabnya.

Langkah 4: Jangan taruh konten yang berubah di dalam blok yang di-cache

Cache itu pencocokan prefix. Dia menghitung hash request dengan urutan tools, lalu system, lalu messages, sampai breakpoint-mu. Satu byte beda di mana pun sebelum penanda menghasilkan hash berbeda dan tulis dengan harga penuh.

Kesalahan klasiknya adalah timestamp atau request id yang diselipkan ke system prompt:

# Salah. Timestamp berubah tiap request, jadi prefix tidak pernah cocok.
system=[{
    "type": "text",
    "text": f"Current time: {now()}\\n{SYSTEM}",
    "cache_control": {"type": "ephemeral"},
}]

Pindahkan breakpoint ke blok terakhir yang tetap sama di antara request yang mau berbagi cache:

# Benar. Cache blok yang stabil, taruh nilai yang berubah di luarnya.
system=[
    {"type": "text", "text": SYSTEM, "cache_control": {"type": "ephemeral"}},
    {"type": "text", "text": f"Current time: {now()}"},  # tidak di-cache
]

Kamu bisa memasang sampai empat breakpoint saat bagian yang berbeda berubah dengan kecepatan berbeda. Definisi tool jarang berubah, tapi konteks hasil retrieval bisa refresh harian, jadi beri tool breakpoint sendiri dan biarkan konteks harian di luarnya.

Langkah 5: Sesuaikan masa hidup cache dengan trafficmu

Default {"type": "ephemeral"} hidup 5 menit dan refresh di tiap hit. Alternatifnya, "ttl": "1h", hidup 1 jam dengan biaya tulis dua kali lipat.

Tulis 5 menit itu 1.25x harga dasar dan baca 0.1x, jadi balik modal setelah satu kali baca. Tulis 1 jam itu 2x harga dasar, jadi butuh sekitar dua kali baca sebelum mengalahkan tanpa cache. Pilih berdasarkan seberapa sering prefix yang sama kena hit:

Traffic ke satu prefix Masa hidup
Beberapa hit per menit Default 5 menit
Satu hit tiap beberapa menit Default 5 menit
Satu hit tiap 10-30 menit 1 jam
Satu hit tiap jam atau lebih jarang Mungkin tidak worth it

Satu jebakan: masa hidup dihitung dari awal request, bukan dari akhirnya. Kalau respons butuh 4 menit untuk streaming, request berikutnya dengan prefix yang sama harus datang sekitar 1 menit kemudian supaya masih hit cache 5 menit.

Langkah 6: Untuk chat, biarkan API mengatur breakpoint

Di percakapan multi-turn, history bertambah tiap giliran. Daripada memindahkan penanda secara manual, cukup pasang satu cache_control di level atas dan biarkan API mendorong breakpoint ke blok cacheable terakhir.

resp = client.messages.create(
    model="claude-opus-5",
    max_tokens=256,
    cache_control={"type": "ephemeral"},  # level atas: caching otomatis
    system="You are a support agent for Acme. Follow these guidelines...",
    messages=history,  # bertambah tiap giliran
)

Satu batasan penting di sini. API mencari ke belakang maksimal 20 content block dari sebuah breakpoint untuk menemukan cache write sebelumnya. Giliran yang menambah lebih dari 20 blok tool-use dan tool-result bisa melewatkan entry giliran sebelumnya dan membayar ulang biaya tulis. Tambahkan breakpoint kedua yang lebih dekat kalau giliran agent-mu seberat itu.

Langkah 7: OpenAI dan Gemini

Caching di OpenAI otomatis dan gratis. Prompt 1.024 token atau lebih mendapat diskon tanpa perubahan kode, dan dokumentasinya mengklaim latensi turun sampai 80% dan biaya input turun sampai 90%. Kamu memastikannya dengan membaca usage.prompt_tokens_details.cached_tokens. Parameter opsional prompt_cache_key membantu mengarahkan request yang berbagi prefix panjang ke mesin yang sama.

resp = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": "Summarize section 4."},
    ],
)
print(resp.usage.prompt_tokens_details.cached_tokens)

Gemini punya dua mode. Implicit caching aktif default untuk model 2.5 ke atas dan bersifat best-effort, jadi tidak ada jaminan. Explicit caching langkah manual tapi menjamin diskon. Buat cache sekali, lalu referensikan. TTL default-nya satu jam, dan minimumnya 2.048 token di keluarga 2.5 dan 4.096 di keluarga 3.x.

from google import genai
from google.genai import types

client = genai.Client()

cache = client.caches.create(
    model="gemini-2.5-flash",
    config=types.CreateCachedContentConfig(
        contents=[types.Content(role="user", parts=[types.Part(text=SYSTEM)])],
        ttl="3600s",
    ),
)

resp = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="Summarize the key points in three bullets",
    config=types.GenerateContentConfig(cached_content=cache.name),
)
print(resp.usage_metadata.cached_content_token_count)

Kapan caching untung, kapan tidak

Situasi Cache? Alasan
System prompt panjang yang stabil, volume request tinggi Ya Sekali tulis, lalu baca 0.1x
RAG dengan set dokumen tetap yang sering dipanggil Ya Prefix hasil retrieval mendominasi biaya
Prompt lebih pendek dari minimum model Tidak Prefix diam-diam tidak di-cache
Konten yang berubah tiap request Tidak Hash tidak pernah cocok, kamu cuma bayar biaya tulis
Traffic terlalu jarang sampai TTL habis sebelum hit berikutnya Tidak Kamu bayar 1.25x tiap request dan tidak pernah baca

Kesimpulan

Prompt caching mengubah prefix yang stabil jadi biaya sekali bayar. Urutan yang penting: taruh konten stabil di depan, tandai blok identik terakhir sebagai breakpoint, pilih TTL yang cocok dengan traffic nyata, lalu baca counter usage untuk memastikan hit. Cache yang tidak pernah hit lebih buruk daripada tanpa cache, karena kamu membayar biaya tulis tiap kali dan menyebutnya penghematan.

Langkah lanjut: catat cache_read_input_tokens sebagai metrik hit-rate, dan jalankan ulang rumus biaya di atas pada data traffic seminggu sebelum menganggap fiturnya selesai.

Referensi

Butuh Bantuan Implementasi?

Saya membantu tim mendesain dan membangun infrastruktur cloud scalable, pipeline DevOps, dan sistem production-grade.

Konsultasi Gratis