← Kembali ke Blog

Pangkas Biaya LLM 90% dengan Prompt Caching Claude (Panduan Praktis)

Chatbot kamu ngirim system prompt yang sama persis 4,000 token di setiap turn. Pipeline RAG kamu ngirim konteks yang retrieved ulang untuk setiap pertanyaan lanjutan. Agent kamu ngirim ulang 20 definisi tool yang sama di setiap langkah. Semua token itu ditagih, setiap saat, dan kebanyakan dari mereka tidak berubah sejak percakapan dimulai.

Prompt caching adalah solusi yang membosankan dan jelas. Tandai prefix yang stabil sekali, bayar biaya tulis kecil, lalu baca ulang di 10% harga input untuk setiap request berikutnya yang cocok. Pada system prompt 5,000 token selama 100 turn, angkanya berubah dari $1.50 ke $0.18 di Claude Sonnet 4.5. Itu penghematan nyata yang bisa kamu luncurkan minggu ini.

Artikel ini tunjukin mekanisme, jebakan, dan implementasi Python yang jalan untuk tiga bentuk umum: chat multi-turn, retrieval RAG, dan agent yang pakai tool.

Prerequisites

  • Python 3.10+
  • pip install anthropic (SDK versi 0.116.x per tulisan ini)
  • ANTHROPIC_API_KEY di environment
  • Opsional: pip install openai buat bagian perbandingan

Cara Kerja Pricing

Yang bikin banyak orang ketipu: tidak ada "mode cache" terpisah. Caching cuma modifier harga pada input token. Setiap input token masuk salah satu dari tiga kategori, dan response kasih tahu kamu yang mana.

Kategori Anthropic (TTL 5 menit) Anthropic (TTL 1 jam) OpenAI (auto)
Cache write 1.25x base input 2.0x base input gratis (tanpa surcharge)
Cache read 0.1x base input 0.1x base input 0.5x base input
Input biasa 1.0x base input 1.0x base input 1.0x base input

Sumber: Anthropic prompt caching pricing dan OpenAI prompt caching guide.

Titik impas adalah request kedua. Kalau kamu cuma kirim prompt sekali, surcharge tulis 1.25x bikin kamu bayar lebih mahal dari baseline tanpa cache. Mulai dari request kedua yang cocok, kamu sudah untung. Semakin panjang prefix, semakin besar penghematan per request.

Untuk Claude Sonnet 4.5 sekarang, angkanya seperti ini (per juta token):

  • Base input: $3.00
  • 5-menit cache write: $3.75
  • 1-jam cache write: $6.00
  • Cache read: $0.30
  • Output: $15.00

Untuk prefix 5,000 token yang di-cache selama 100 request, total biaya di Sonnet 4.5:

  • Tanpa cache: 5,000 x 100 / 1,000,000 x $3 = $1.50
  • Dengan cache: (5,000 write di $3.75) + (5,000 x 99 read di $0.30) = $0.0188 + $0.1485 = $0.1673

Itu pengurangan 89%. Angka persisnya tergantung berapa cache read yang kamu dapat, tapi urutan besarnya yang penting.

Dua Rasa Caching Anthropic

Anthropic luncurkan caching otomatis dan eksplisit. Pilih berdasarkan seberapa dinamis prompt kamu.

Caching Otomatis (Chat Multi-Turn)

Taruh satu cache_control di level atas request. API mindahin breakpoint cache ke cacheable block terakhir di setiap panggilan, jadi cache tumbuh seiring percakapan. Ini pilihan yang tepat kalau yang berubah cuma message list.

import anthropic

client = anthropic.Anthropic()

def chat(messages: list[dict]) -> str:
    response = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        # Satu baris. System prompt dan message awal otomatis ke-cache.
        cache_control={"type": "ephemeral"},
        system="Kamu agen customer support untuk fintech Indonesia. "
               "Balas dalam Bahasa Indonesia kecuali user nulis dalam bahasa Inggris. "
               "Singkat. Jangan bikin-bikin saldo rekening.",  # ~50 token
        messages=messages,
    )
    print(response.usage)  # Cek cache_creation_input_tokens dan cache_read_input_tokens
    return response.content[0].text

Jalanin fungsi dua kali dengan system prompt yang sama dan message list yang tumbuh. Di panggilan kedua, blok usage akan nunjukin cache_read_input_tokens yang mencakup system prompt dan message awal yang cocok dengan request sebelumnya.

Caching Eksplisit (Bagian Stabil, Bagian Berubah)

Kalau bagian prompt kamu berubah di kecepatan yang beda, caching otomatis patah. RAG adalah contoh yang paling jelas: system prompt dan definisi tool stabil, konteks yang retrieved berubah per query, dan pesan user berubah per turn. Tiga irama yang berbeda.

Tandai setiap bagian dengan cache_control sendiri:

import anthropic

client = anthropic.Anthropic()

SYSTEM = [
    {
        "type": "text",
        "text": "Kamu jawab pertanyaan tentang skema Postgres. Singkat aja. Pakai SQL kalau perlu.",
        "cache_control": {"type": "ephemeral"},  # Breakpoint 1
    }
]

TOOLS = [
    {
        "name": "query_database",
        "description": "Jalanin query SQL read-only ke warehouse.",
        "input_schema": {
            "type": "object",
            "properties": {
                "sql": {"type": "string", "description": "Statement SELECT"},
            },
            "required": ["sql"],
        },
        # Tool cache sebagai blok sendiri.
        "cache_control": {"type": "ephemeral"},  # Breakpoint 2
    }
]

def ask(question: str, retrieved_chunks: list[str]) -> str:
    context = "\n\n".join(retrieved_chunks)
    response = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        system=SYSTEM,
        tools=TOOLS,
        messages=[{
            "role": "user",
            "content": (
                f"Pakai konteks di bawah buat jawab.\n\n"
                f"<context>\n{context}\n</context>\n\n"
                f"Pertanyaan: {question}"
            ),
            "cache_control": {"type": "ephemeral"},  # Breakpoint 3
        }],
    )
    return response.content[0].text

Tiga breakpoint, tiga entry cache independen. System prompt dan tool tetap ke-cache di setiap request. Konteks yang retrieved dapat entry baru saat chunk berubah. Pesan user sendiri ke-cache supaya turn berikutnya bisa baca ulang.

Jebakan Lookback

Cache Anthropic pakai lookback window 20 blok. Kalau breakpoint kamu bergerak lebih dari 20 blok dari write terakhir, cache miss terjadi secara diam-diam. Kamu bayar harga input penuh dan mungkin tidak sadar kalau kamu tidak cek objek usage.

Ini yang ngegigit percakapan multi-turn. Turn 1 nulis di blok 10. Turn 2 nambah 5 blok dan nulis di blok 15. Turn 3 nambah 30 blok. Breakpoint sekarang di blok 45. Lookback mundur 20 posisi, gak ketemu apa-apa, dan kamu bayar fresh write di 1.25x untuk seluruh prefix.

Solusinya adalah nambah breakpoint kedua, yang intermediate, lebih awal di percakapan supaya entry tersedia lebih dekat ke tempat yang berikutnya akan dibutuhkan. Maksimal 4 breakpoint per request.

# Di agent yang jalan lama, taruh breakpoint "waypoint".
# Entry ini tetap hidup bahkan kalau breakpoint utama bergerak melewatinya.
{
    "role": "user",
    "content": "[checkpoint: masih di task yang sama, ringkasin state sekarang satu baris]",
    "cache_control": {"type": "ephemeral"},
}

Kapan Pilih TTL 5 Menit vs 1 Jam

Defaultnya 5 menit. Cocok buat chat dan flow request sinkron. Pilih 1 jam kalau traffic kamu bursty tapi prefix tetap valid lebih lama.

cache_control={"type": "ephemeral", "ttl": "1h"}

Write 1 jam biaya 2x base (dua kali write 5 menit), tapi read-nya sama 0.1x. Kalau kamu benar-benar mempertahankan cache hidup selama satu jam penuh, biaya per request turun jadi sekitar 10% dari base. Kalau kamu evict cache setiap 6 menit dengan mengubah prefix, premi write 1 jam adalah duit yang terbuang.

Aturan simpel: kalau aplikasi kamu secara natural bisa baca ulang prefix yang sama setiap beberapa menit, 5 menit cukup. Kalau kamu bikin batch job yang pakai ulang system prompt di 1,000 request selama satu jam, 1 jam menang. Kalau belum tahu, mulai dari 5 menit dan ukur.

Prewarming Cache (Menang Latency)

Cache baru tersedia setelah response pertama dimulai. Kalau kamu punya jalur cold-start yang diketahui (laporan harian, scheduled job, user pertama hari itu), prewarm dengan request zero-token:

def prewarm_cache() -> None:
    """Panggil di startup atau cron. Biaya cuma surcharge cache write."""
    client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=0,
        system=[
            {
                "type": "text",
                "text": "Kamu senior staff engineer yang review pull request...",
                "cache_control": {"type": "ephemeral"},
            }
        ],
        messages=[{"role": "user", "content": "warmup"}],
    )
    # stop_reason bakal "max_tokens" dan content bakal [].
    # Entry cache sekarang sudah siap untuk request beneran berikutnya.

Request beneran pertama setelah ini dapat cache hit, yang juga motong time-to-first-token. Anthropic laporkan sampai 85% pengurangan latency untuk cache read pada prompt panjang. Ini penting buat UI streaming.

Batas Minimum Token

Caching gak gratis untuk dipasang. Tiap platform enforce minimum:

  • Anthropic Claude: 1,024 token untuk Claude Sonnet 4.5, 2,048 token untuk Opus 4.x
  • OpenAI gpt-4o dan yang lebih baru: 1,024 token
  • Google Gemini: beberapa ribu token (beda per model)

Kalau prefix kamu di bawah threshold, API diam-diam skip caching dan kamu bayar harga input biasa. Tidak ada error. Selalu cek cache_read_input_tokens di response buat konfirmasi caching benar-benar jalan.

Bagaimana OpenAI dan Google Bandingkan

Prompt caching OpenAI sepenuhnya otomatis. Tidak ada parameter cache_control, tidak ada breakpoint. Request dengan prefix yang cocok (di-hash dari 256 token pertama) ke-cache selama 5 sampai 10 menit. Harganya lebih simpel: 0.5x untuk cache read, tidak ada surcharge write. Kekurangannya kontrol yang lebih sedikit. Kamu tidak bisa pin satu section, tidak bisa perpanjang TTL, dan kamu tidak dapat laporan usage sejelas itu.

Implicit caching Google Gemini kerja mirip: tidak ada marker eksplisit, prefix matching otomatis, read gratis, dan default TTL yang jauh lebih panjang. Caching eksplisit juga tersedia via resource cachedContent kalau kamu butuh kontrol.

Kalau kamu mulai proyek baru dan tidak butuh kontrol presisi, jalur otomatis OpenAI adalah opsi paling rendah friksi. Kalau kamu punya prompt kompleks dengan bagian yang berubah di kecepatan yang beda, breakpoint eksplisit Anthropic kasih kamu kontrolnya.

Verifikasi Cache Beneran Hit

Satu kebiasaan paling penting: log objek usage di setiap request. Kegagalan caching terjadi secara diam-diam.

def log_cache_stats(usage):
    cached = usage.cache_read_input_tokens or 0
    written = usage.cache_creation_input_tokens or 0
    fresh = usage.input_tokens or 0
    total = cached + written + fresh
    if total == 0:
        return
    hit_rate = cached / total
    print(f"cache hit rate: {hit_rate:.1%} ({cached} read, {written} written, {fresh} fresh)")
    if hit_rate < 0.5 and total > 2000:
        print("WARNING: cache hit rate rendah, cek penempatan breakpoint")

Setup yang sehat dengan breakpoint eksplisit harus hit di atas 80% pada cached prefix setelah request pertama. Kalau kamu lihat 0%, penyebab yang paling umum: breakpoint ditaruh di blok yang berubah setiap request (timestamp di prefix), hash prefix beda karena urutan tool, atau prefix di bawah minimum token.

Contoh RAG yang Jalan

Gabungin semua bagian untuk pipeline RAG:

import anthropic

client = anthropic.Anthropic()

# Stabil di semua query: instruksi sistem + definisi tool.
SYSTEM_PROMPT = [
    {
        "type": "text",
        "text": (
            "Kamu jawab pertanyaan tentang kebijakan perusahaan pakai konteks yang diberikan. "
            "Kutip chunk sumber kalau bisa. Kalau jawabannya gak ada di konteks, "
            "bilang 'tidak ditemukan di konteks yang diberikan' dan jangan tebak."
        ),
        "cache_control": {"type": "ephemeral"},
    }
]

TOOL_DEFINITIONS = [
    {
        "name": "search_policies",
        "description": "Cari chunk yang relevan di database kebijakan.",
        "input_schema": {
            "type": "object",
            "properties": {"query": {"type": "string"}},
            "required": ["query"],
        },
        "cache_control": {"type": "ephemeral"},
    }
]

# Per-query: konteks yang retrieved + pertanyaan user.
def answer(question: str, chunks: list[str]) -> str:
    context_text = "\n\n".join(f"[{i+1}] {c}" for i, c in enumerate(chunks))
    response = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=800,
        system=SYSTEM_PROMPT,
        tools=TOOL_DEFINITIONS,
        messages=[{
            "role": "user",
            "content": (
                f"Konteks:\n{context_text}\n\nPertanyaan: {question}"
            ),
            # Blok ini berubah per query, tapi dia nge-cache konteks turn sebelumnya
            # supaya pertanyaan lanjutan berikutnya bisa baca ulang.
            "cache_control": {"type": "ephemeral"},
        }],
    )
    log_cache_stats(response.usage)
    return response.content[0].text

Jalanin answer("Apa kebijakan cuti?", [chunk1, chunk2, chunk3]) sekali. System prompt dan definisi tool ke-cache. Jalanin lagi dengan system prompt dan tool yang sama tapi chunk yang beda. System dan tool datang dari cache. Jalanin lagi dengan chunk yang sama dan pertanyaan lanjutan. Chunk datang dari cache.

Kesalahan Umum

Taruh breakpoint di pesan user yang ada timestamp-nya. Hash menyertakan timestamp, jadi prefix gak pernah cocok. Pindahin breakpoint ke blok stabil terakhir.

Nambah breakpoint lebih dari yang dibutuhkan. Empat itu maksimum, tapi setiap breakpoint yang kamu tambah gak naikin biaya kamu. Dia cuma kasih kamu lebih banyak kontrol. Pake kalau bagian memang berubah di kecepatan yang beda, bukan "untuk jaga-jaga."

Lupa kalau cache itu per-region dan per-workspace. Cache yang dibuat di environment staging gak akan tersedia di production. Kalau kamu jalanin workload yang sama di beberapa region, tiap region punya cache sendiri.

Asumsi cache bertahan ganti model. Pindah dari Sonnet ke Opus evict cache. Model baru punya namespace cache sendiri.

Abaikan biaya output token. Caching cuma bantu input. Kalau prompt kamu kecil tapi output besar (long-form generation, loop agentic dengan tool call yang verbose), penghematan input cuma rounding error di tagihan. Profile dulu.

Lanjut ke Mana

Kalau kamu mau gabungin caching sama pola agent yang banyak tool dari artikel sebelumnya, tambahin cache_control ke system prompt dan array tools. Definisi tool jarang berubah, jadi dia cache dengan baik. Message history dan tool result adalah bagian yang bergerak.

Referensi

Butuh Bantuan Implementasi?

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

Konsultasi Gratis