35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Anthropic Prompt Caching in Rails: Claude API-kosten Verlagen met de Anthropic Ruby SDK

Anthropic Prompt Caching in Rails: Claude API-kosten Verlagen met de Anthropic Ruby SDK

Roger Heykoop
Ruby on Rails, AI
Anthropic prompt caching in Rails: verlaag Claude API-kosten tot 90% met de Anthropic Ruby SDK. Productiepatronen, valkuilen en echte cijfers.

Een SaaS-klant belde me op een vrijdagmiddag omdat hun Anthropic-rekening die maand al op tweeënveertigduizend dollar stond — en het was pas de achttiende. Ze hadden zes weken eerder een Claude-aangedreven supportassistent uitgerold, het verkeer was verviervoudigd, en elk verzoek stuurde dezelfde systeemprompt en kennisbasis-aanloop van achttienduizend tokens mee. Die middag zetten we Anthropic prompt caching aan. De volgende rekening kwam onder de vijfduizend uit.

Na negentien jaar Rails heb ik veel “dit moet goedkoper kunnen”-engineering gedaan, en Anthropic prompt caching is veruit de optimalisatie met de hoogste hefboomwerking die ik ken voor productie-LLM-apps. Als je de Anthropic Ruby SDK gebruikt en niet caching aanzet, sta je geld te verbranden. Dit is het productiehandboek.

Wat Anthropic Prompt Caching Eigenlijk Is

Elke Claude API-call heeft een input-prompt — systeemboodschap, tooldefinities, gespreksgeschiedenis, opgehaalde documenten — en een output-completion. Het overgrote deel van die input is identiek tussen verzoeken. De systeemprompt verandert niet tussen gebruikers. De toolschema’s veranderen niet tussen calls. Het beleidsdocument van dertig pagina’s dat je in context propt, verandert een week lang niet.

Anthropic prompt caching laat je delen van je input markeren als cachebaar. Het eerste verzoek dat die delen bevat, betaalt een write-premie van 1.25x de basisinput-prijs. Elk volgend verzoek dat de cache raakt, betaalt nog maar 0.1x de basisinput-prijs voor die tokens. Dat is 90% korting op gecachete input.

De standaard cachelevensduur is vijf minuten vanaf de laatste hit, ververst bij elke read. Er is ook een caching-optie van één uur voor prompts waarvan je weet dat ze warm blijven. Voor een chatbot met stabiel verkeer dekt een TTL van vijf minuten in feite elk verzoek na het eerste.

Het mechanisme is één veld op een content block:

{
  type: "text",
  text: long_system_prompt,
  cache_control: { type: "ephemeral" }
}

Anthropic hasht het cumulatieve prefix tot en met elke cache_control-marker. Komt de hash overeen met een levend cache-record, dan krijg je een cache hit. Zo niet, dan schrijf je een nieuw record. De cache-key bevat het model, dus van model wisselen invalideert alles.

Wanneer Prompt Caching Loont (en Wanneer Niet)

De rekensom is simpel. Cache writes kosten 1.25x base, cache reads 0.1x base. Een gecachet prefix is dus rond twee hergebruiken break-even en levert echt geld op vanaf drie. Alles wat je binnen vijf minuten meer dan drie keer hergebruikt, is een kandidaat.

In een Rails-app zijn dit de werklasten die ervan profiteren:

  • Klantgerichte chatbots met een vaste systeemprompt en tooldefinities
  • RAG-endpoints die per sessie een groot referentiedocument of opgehaalde context vastpinnen
  • Code review- of analysebots die een repository-snapshot meesturen
  • Multi-turn assistenten waarbij de gespreksgeschiedenis met elke beurt groeit
  • Batch classificatie- of extractiejobs die dezelfde instructies delen over duizenden items

Waar het niet loont: one-shot calls met elke keer een unieke prompt, heel korte prompts (de minimale cachebare grootte is 1024 tokens voor Sonnet/Opus en 2048 voor Haiku), en elke workflow waarin het prefix per call wezenlijk verandert. Zit je onder het minimum, dan doet caching niets — je cache_control-markers worden in stilte genegeerd.

Anthropic Prompt Caching Opzetten in Rails met de Anthropic Ruby SDK

De officiële Anthropic Ruby SDK ondersteunt prompt caching native. Voeg toe aan je Gemfile:

# Gemfile
gem "anthropic", "~> 1.0"

Configureer de client met credentials uit Rails credentials, niet met environment variables in productiecode:

# config/initializers/anthropic.rb
ANTHROPIC = Anthropic::Client.new(
  api_key: Rails.application.credentials.dig(:anthropic, :api_key)
)

Ik heb de bredere patronen voor Claude-integratie in Rails — streaming, tools, RAG — eerder behandeld. Voor context: de Rails RAG met Claude en pgvector-gids en de LLM function calling in Rails-post sluiten goed aan op dit stuk.

Productiepatroon 1: De Systeemprompt Cachen

De zet met de hoogste hefboom. Wikkel je systeemprompt in een gecachet block:

class Support::AssistantService
  SYSTEM_PROMPT = File.read(Rails.root.join("config/prompts/support_v3.md")).freeze

  def reply(conversation:, user_message:)
    ANTHROPIC.messages.create(
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      system: [
        {
          type: "text",
          text: SYSTEM_PROMPT,
          cache_control: { type: "ephemeral" }
        }
      ],
      messages: conversation_messages(conversation, user_message)
    )
  end

  private

  def conversation_messages(conversation, user_message)
    conversation.turns.map { |t| { role: t.role, content: t.content } } +
      [{ role: "user", content: user_message }]
  end
end

Twee niet-vanzelfsprekende punten. Eén: het system-veld accepteert een array van content blocks, niet zomaar een string — je moet de array-vorm gebruiken om cache_control mee te sturen. Twee: bevries de prompt bij het laden. Lees je het bestand binnen reply, dan wordt elke Capistrano-achtige file-mtime-eigenaardigheid of whitespace-verschil een cache miss.

Voor een bot die duizend verzoeken per uur bedient met een systeemprompt van achttienduizend tokens, brengt deze ene wijziging de inputkosten op die prompt terug van 18.000 tokens × $0,003 = $0,054 per verzoek naar $0,0054 per verzoek. Over een miljoen verzoeken is dat $48.000 verschil.

Productiepatroon 2: Lange Documenten Cachen in RAG

Retrieval-augmented generation-pipelines halen vaak dezelfde chunks herhaaldelijk op binnen een sessie — een gebruiker die drie vragen stelt over hetzelfde contract, krijgt drie identieke retrievals. Cache de opgehaalde bundel:

class Knowledge::AssistantService
  def answer(query:, session_id:)
    chunks = Embeddings::Retriever.new(query).top(8)
    context = chunks.map { |c| "[#{c.id}] #{c.content}" }.join("\n\n")

    ANTHROPIC.messages.create(
      model: "claude-sonnet-4-6",
      max_tokens: 800,
      system: [
        { type: "text", text: BASE_SYSTEM_PROMPT, cache_control: { type: "ephemeral" } },
        { type: "text", text: context,           cache_control: { type: "ephemeral" } }
      ],
      messages: [{ role: "user", content: query }]
    )
  end
end

Twee cache_control-markers betekent twee cache breakpoints. Anthropic ondersteunt er tot vier. De basisprompt blijft gecachet over de hele app. De opgehaalde context blijft gecachet over een sessie. De gebruikersvraag wordt niet gecachet — die verandert elke call.

Kanttekening: als je retrieval te goed is en bij elke vraag andere chunks teruggeeft, krijg je geen cache hit op het contextblok. In de praktijk zie ik dat sessies rond onderwerpen clusteren, dus retrievals overlappen sterk. Meet, niet aannemen.

Productiepatroon 3: Multi-Turn Gesprekken

Voor een chatbot die per beurt context laat groeien, is de truc om de laatste turn als cache breakpoint te markeren. Elke vorige turn wordt zo onderdeel van het gecachete prefix bij de volgende call:

def reply(conversation:, user_message:)
  history = conversation.turns.map { |t| { role: t.role, content: t.content } }
  history.last[:cache_control] = { type: "ephemeral" } if history.any?

  ANTHROPIC.messages.create(
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    system: [{ type: "text", text: SYSTEM_PROMPT, cache_control: { type: "ephemeral" } }],
    messages: history + [{ role: "user", content: user_message }]
  )
end

Na turn één is de systeemprompt gecachet. Na turn twee de systeemprompt plus turn één. Na turn drie dat alles plus turn twee. Vanaf turn tien doet de cache echt werk — je betaalt vol tarief alleen voor het laatste gebruikersbericht en het modelantwoord. Dat is waarom lange supportgesprekken progressief goedkoper worden in plaats van progressief duurder.

Productiepatroon 4: Tooldefinities

Gebruik je tool use, dan kunnen je toolschema’s honderden tot duizenden tokens beslaan. Ze veranderen vrijwel nooit tussen verzoeken. Cache ze:

TOOLS = JSON.parse(File.read(Rails.root.join("config/prompts/tools.json"))).freeze

ANTHROPIC.messages.create(
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  tools: TOOLS,
  system: [{ type: "text", text: SYSTEM_PROMPT, cache_control: { type: "ephemeral" } }],
  messages: messages
)

De cache_control-marker op de systeemprompt dekt alles wat in de canonieke verzoekvolgorde daarvoor komt, dus inclusief de tools-array. Eén marker op de systeemprompt cachet beide. Heb je veel tools of genereer je tooldefinities dynamisch, normaliseer ze dan — zelfde JSON-output, zelfde key-volgorde — anders verschilt je hash per call en raak je de cache nooit.

Ik ging dieper in op het inbouwen van Claude-tools in Rails in de LLM function calling-gids; die post is een goede metgezel als je caching op een bestaande tool-gebruikende assistent bouwt.

Cache Hit Rate Meten in Productie

Wat je niet meet, optimaliseer je niet. De Anthropic API geeft cache-statistieken terug bij elk antwoord:

response = ANTHROPIC.messages.create(...)

response.usage.input_tokens               # ongecachete input deze call
response.usage.cache_creation_input_tokens # tokens naar cache geschreven
response.usage.cache_read_input_tokens     # tokens vanuit cache geserveerd
response.usage.output_tokens

Push deze bij elke call naar je metrics-pipeline:

class Llm::Metered
  def self.call(label:, **kwargs)
    response = ANTHROPIC.messages.create(**kwargs)
    u = response.usage

    StatsD.increment("llm.calls", tags: ["label:#{label}"])
    StatsD.histogram("llm.cache_read_tokens",     u.cache_read_input_tokens,     tags: ["label:#{label}"])
    StatsD.histogram("llm.cache_creation_tokens", u.cache_creation_input_tokens, tags: ["label:#{label}"])
    StatsD.histogram("llm.input_tokens",          u.input_tokens,                tags: ["label:#{label}"])
    StatsD.histogram("llm.output_tokens",         u.output_tokens,               tags: ["label:#{label}"])

    response
  end
end

De metric die je op een dashboard wilt is cache_read / (cache_read + cache_creation + input). Een gezonde productiechatbot zit binnen een paar minuten warm verkeer op 80–95%. Zit je structureel onder de 50%, dan verandert je cache-key te vaak — meestal omdat er iets dynamisch in het gecachete prefix is geslopen.

Valkuilen Waar Niemand Je Voor Waarschuwt

Vijf valkuilen die ik of mijn klanten zijn tegengekomen sinds prompt caching GA werd.

Whitespace- en volgordewijzigingen invalideren de cache. Een Markdown-bestand lezen met File.read geeft je een verse string met de regeleindes die het bestand nu eenmaal heeft. Trim trailing whitespace bij het laden. Bevries. Bouw je de systeemprompt door interpolatie, zorg dan dat de geïnterpoleerde waarden ook gecachet zijn of weggestript — "You are #{Time.current}" is een 100% miss rate.

Cache-markers worden in stilte genegeerd onder het minimum. Is je systeemprompt 800 tokens, dan gebeurt er geen caching, en dat valt mogelijk weken niet op omdat de API geen fout teruggeeft. Controleer altijd cache_creation_input_tokens op de eerste call na een deploy. Is die nul, dan zit je onder de drempel.

Van model wisselen invalideert alles. Een cache-record is gekeyd op model. Een rollout van Sonnet 4.6 ter vervanging van Sonnet 4 betekent een stormloop van cache writes voor de eerste vijf minuten. Doe model-rollouts bij voorkeur tijdens daluren als je hier scherp op wilt zijn.

Beta headers kunnen veranderen. Sommige geavanceerde caching-modi, zoals de TTL van één uur, hadden tijdelijk een beta header nodig (anthropic-beta: extended-cache-ttl-2025-04-11 of vergelijkbaar — check de actuele docs). De Ruby SDK laat je extra headers meegeven; pin de versie en lees de changelog bij elke upgrade.

Cache hits tellen niet hetzelfde mee voor rate limits als verse tokens. Dat is een feature, geen bug — je effectieve doorvoer gaat omhoog. Maar het betekent ook dat autoscaling-logica op basis van input-tokens je belasting onderschat. Track cache reads apart als je workers schaalt op tokenvolume. Net zoals ik in de Puma-tuning-gids pleitte voor het apart meten van concurrency, wil je hier een aparte metric.

Wanneer Anthropic Prompt Caching Niet de Juiste Keuze Is

Drie gevallen waarin het de verkeerde tool is.

Echt unieke prompts. Een code-search-tool die per vraag een andere systeemprompt opbouwt, heeft geen gedeeld prefix. Sla caching over en focus op outputtoken-reductie.

Heel weinig verkeer. Maak je twaalf calls per dag, verspreid over kantooruren, dan is je cache elke keer koud. De 1.25x write-premie wordt een permanente belasting.

Latency-gevoelige losse calls. Cache writes zijn iets langzamer dan ongecachete calls. Voor een one-shot-extractie in een nachtelijke job heb je er geen baat bij. Voor een synchrone, gebruikergerichte chatbot wordt de latency-tik op de eerste call uitgesmeerd over duizenden snelle hits — dat is het waard.

Veelgestelde Vragen

Hoeveel kan Anthropic prompt caching de Claude API-kosten verlagen?

In productie-Rails-apps met een stabiele systeemprompt en stabiel verkeer zie ik 70–90% reductie in inputtoken-kosten. De variatie hangt af van prompt-lengte en verkeerspatroon: langere gecachete prefixes en stabieler verkeer geven grotere besparing. Outputtokens worden niet gecachet en veranderen niet.

Wat is de minimale grootte voor Anthropic prompt caching?

Het minimum is 1024 tokens voor Claude Sonnet- en Opus-modellen, en 2048 tokens voor Haiku. Daaronder wordt er stil niet gecachet, ook al stuur je cache_control mee. Controleer cache_creation_input_tokens op de eerste call om te bevestigen dat caching actief is.

Werkt prompt caching met streaming responses in Rails?

Ja. Caching geldt voor het input-prefix, dat is hetzelfde of je nu streamt of niet. De cache-statistieken komen mee in het laatste usage-event van de stream. Gebruik je ActionController::Live om Claude-antwoorden te streamen, zie dan de streaming LLM responses-gids — caching laagt er schoon overheen.

Hoe lang blijft de Anthropic prompt cache geldig?

De standaard ephemeral cache houdt vijf minuten stand vanaf de laatste read en wordt bij elke hit ververst. Er is ook een TTL-optie van één uur voor prompts waarvan je weet dat ze langer warm blijven; die kost iets meer om te schrijven, maar verlengt het hergebruikvenster. Voor de meeste chatbots is de standaard van vijf minuten ruim voldoende, omdat stabiel verkeer hem continu warm houdt.


Na negentien jaar Rails heb ik geleerd dat de beste optimalisaties die zijn die één configuratielijn veranderen en een grafiek opleveren die met een orde van grootte daalt. Anthropic prompt caching is er zo eentje. Draai je Claude in productie via de Anthropic Ruby SDK, markeer dan vandaag je prefixes en kijk morgen hoe de rekening daalt.

Hulp nodig met het integreren van Claude in een Rails-systeem of het auditen van je LLM-kosten? TTB Software levert productiewaardige AI-infrastructuur op Rails. We doen dit al negentien jaar.

#anthropic-prompt-caching #claude-api-cost-reduction #anthropic-ruby-sdk #rails-llm-integration #prompt-caching-rails #claude-sdk-rails #ruby-on-rails
R

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation