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 Message Batches in Rails: 50% Lagere Claude API Kosten met Asynchrone Batch Processing

Anthropic Message Batches in Rails: 50% Lagere Claude API Kosten met Asynchrone Batch Processing

Roger Heykoop
Ruby on Rails, AI
Anthropic Message Batches in Rails: 50% lagere Claude API kosten met asynchrone batch processing, Solid Queue polling en idempotente result handling.

Een founder waar ik mee werk runt een content classification pipeline die zo’n veertigduizend documenten per dag door Claude duwt. Ze betaalden iets minder dan twaalfduizend dollar per week. De CFO wilde weten of er een goedkopere manier was voordat hij het budget verlengde. Die was er: Anthropic Message Batches. We hebben de pipeline op een vrijdagmiddag verhuisd, de rekening van de week erna was net onder de zesduizend, en de enige verandering die iemand opmerkte was dat resultaten twintig minuten na upload binnenkwamen in plaats van twee seconden na elke request.

Na negentien jaar Rails heb ik veel “stuur dit naar een third-party API op de achtergrond en sla het resultaat op” pipelines gebouwd, en Anthropic Message Batches is de schoonste versie van dat patroon die ik voor LLM workloads heb gezien. Als je bulk classificatie, samenvattingen, extractie of evaluaties tegen Claude draait, en je hebt geen synchroon antwoord nodig, dan is de Anthropic Message Batches API de hefboom die je moet trekken. Dit is het productie-playbook.

Wat Anthropic Message Batches Eigenlijk Zijn

De Claude API heeft twee delivery modes. De synchrone Messages API is waar de meeste Rails apps mee beginnen — je doet POST /v1/messages, je krijgt binnen een paar seconden een response, je schrijft die naar de database. Dat is prima voor chat, lage-volume agents, en alles wat user-facing is waar een mens op zit te wachten.

Anthropic Message Batches is de async neef. Je upload een JSONL-vormige batch van maximaal tienduizend messages of 256 MB payload in één request. Anthropic bevestigt de batch, verwerkt hem binnen vierentwintig uur (meestal veel sneller — minuten voor kleine batches), en stelt dan een results URL beschikbaar met één JSONL regel per request. Elke call binnen de batch kost vijftig procent van de synchrone prijs, inclusief gecachte tokens. De 50% korting stapelt op prompt caching, dus een batched call die een warme cache raakt kost vijf procent van de synchrone, ongecachte basisprijs.

Er zijn drie failure modes waar je omheen moet ontwerpen: individuele requests binnen een batch kunnen falen terwijl de batch als geheel slaagt, de batch kan gecanceld worden, en de batch kan verlopen als Anthropic hem niet binnen het venster kan afmaken. Geen van deze is exotisch — dit zijn dezelfde operationele zorgen als bij elke andere async pipeline — maar Rails apps beginnen meestal met synchrone calls, en het mentale model moet verschuiven.

De Anthropic Ruby SDK exposeert batches als client.messages.batches. Je maakt een batch aan, je polt op status, je streamt resultaten als hij klaar is:

client = Anthropic::Client.new

batch = client.messages.batches.create(
  requests: [
    {
      custom_id: "doc-1",
      params: {
        model: "claude-sonnet-4-6",
        max_tokens: 1024,
        messages: [{ role: "user", content: "Classify: ..." }]
      }
    }
  ]
)

batch.id           # => "msgbatch_01..."
batch.processing_status  # => "in_progress"

De custom_id is het enige dat jij in de batch envelope controleert, en het is het allerbelangrijkste detail van het ontwerp. Het is hoe je resultaten reconcilieert met de oorspronkelijke Rails records.

Wanneer Anthropic Message Batches Lonen

De rekensom op Anthropic Message Batches is eerlijk gezegd makkelijker dan op prompt caching. Halve prijs, geen break-even, geen warm-up. De enige vraag is of jouw workload async delivery tolereert.

Dit zijn de Rails workloads waar het een duidelijke winst is. Nachtelijke verrijking van records die overdag binnenkwamen — classificeren, taggen, metadata embedden, of samenvatten. Historische data herverwerken na een prompt-wijziging. Bulk evaluaties en red-team runs tegen een nieuwe model release. Alt-text, beschrijvingen of seo blurbs genereren voor een content library. Alles wat eindigt met “…en dan zet je het resultaat op het record.”

De plekken waar het niet past zijn de voor de hand liggende. User-facing chat waar een mens op zit te wachten. Tool-using agents die op model-output moeten reageren en de volgende call moeten bepalen. Streaming responses. Workloads waar het upstream systeem het antwoord nodig heeft om een synchrone beslissing te nemen. Voor die gevallen blijf je op de gewone Messages API en leun je op prompt caching voor de kosten.

Waar het interessant wordt is het middenstuk. Een “stuur een job in, krijg een mailtje als hij klaar is” workflow binnen een SaaS app past perfect op Anthropic Message Batches. Een “verwerk deze CSV met leads door Claude en schrijf terug naar HubSpot” import idem dito. Als de gebruiker minuten kan wachten, zou je moeten batchen.

De Anthropic Message Batches Pipeline Bouwen in Rails

Dit is de productie-vorm waar ik steeds naar terugkeer. Eén Rails model voor de batch envelope, één voor elke individuele request, een Solid Queue job voor submission, een polling job voor status, en een idempotente result handler. Niets exotisch.

# db/migrate/20260430000001_create_claude_batches.rb
class CreateClaudeBatches < ActiveRecord::Migration[8.0]
  def change
    create_table :claude_batches do |t|
      t.string  :anthropic_id, index: { unique: true }
      t.string  :status, null: false, default: "pending"
      t.integer :request_count, null: false, default: 0
      t.integer :succeeded_count, null: false, default: 0
      t.integer :errored_count, null: false, default: 0
      t.datetime :submitted_at
      t.datetime :ended_at
      t.timestamps
    end

    create_table :claude_batch_requests do |t|
      t.references :claude_batch, null: false, foreign_key: true
      t.references :subject, polymorphic: true, null: false
      t.string  :custom_id, null: false
      t.jsonb   :params, null: false, default: {}
      t.string  :result_status
      t.jsonb   :result_payload
      t.timestamps

      t.index [:claude_batch_id, :custom_id], unique: true
    end
  end
end

De polymorfe subject is het Rails record waar de request over gaat — een Document, een Lead, een Product, wat dan ook. De custom_id is wat we naar Anthropic sturen, en die maken we deterministisch zodat retries veilig zijn.

class ClaudeBatchRequest < ApplicationRecord
  belongs_to :claude_batch
  belongs_to :subject, polymorphic: true

  before_validation :assign_custom_id, on: :create

  private

  def assign_custom_id
    self.custom_id ||= "#{subject_type.underscore}-#{subject_id}-#{SecureRandom.hex(4)}"
  end
end

De submission job bouwt de JSONL payload, verstuurt hem, en bewaart het Anthropic batch id. Ik houd de body-constructie in een plain Ruby object in plaats van in de job zelf — makkelijker te testen, makkelijker om later van model te wisselen.

class ClaudeBatchSubmitter
  def initialize(claude_batch)
    @claude_batch = claude_batch
    @client = Anthropic::Client.new
  end

  def call
    requests = @claude_batch.claude_batch_requests.map do |req|
      { custom_id: req.custom_id, params: req.params }
    end

    response = @client.messages.batches.create(requests: requests)

    @claude_batch.update!(
      anthropic_id: response.id,
      status: "in_progress",
      request_count: requests.size,
      submitted_at: Time.current
    )

    ClaudeBatchPollJob.set(wait: 30.seconds).perform_later(@claude_batch.id)
  end
end

Polling is het stuk dat iedereen wil over-engineeren. De Anthropic API stuurt geen webhooks voor batches, dus je moet pollen. Solid Queue maakt dit goedkoop omdat een job opnieuw enqueuen met wait: één insert in de database is. Ik pol de eerste vijf minuten elke dertig seconden, dan terug naar één minuut, dan naar vijf.

class ClaudeBatchPollJob < ApplicationJob
  queue_as :claude_batches

  def perform(claude_batch_id)
    batch = ClaudeBatch.find(claude_batch_id)
    return if batch.status == "ended"

    response = Anthropic::Client.new.messages.batches.retrieve(batch.anthropic_id)

    case response.processing_status
    when "in_progress"
      reschedule(batch)
    when "ended"
      ClaudeBatchResultIngestJob.perform_later(batch.id)
    when "canceling", "canceled", "expired"
      batch.update!(status: response.processing_status, ended_at: Time.current)
    end
  end

  private

  def reschedule(batch)
    age = Time.current - batch.submitted_at
    delay = case age
            when 0..5.minutes then 30.seconds
            when 5.minutes..30.minutes then 1.minute
            else 5.minutes
            end
    self.class.set(wait: delay).perform_later(batch.id)
  end
end

Result ingestion is waar idempotentie ertoe doet. Anthropic exposeert resultaten als een streaming JSONL endpoint. Je leest regel voor regel, zoekt de request op via custom_id, en schrijft de uitkomst weg. Als de job halverwege omvalt en opnieuw draait, houden de unieke index op [claude_batch_id, custom_id] plus een if request.result_status.nil? guard je veilig.

class ClaudeBatchResultIngestJob < ApplicationJob
  queue_as :claude_batches

  def perform(claude_batch_id)
    batch = ClaudeBatch.find(claude_batch_id)
    client = Anthropic::Client.new

    client.messages.batches.results(batch.anthropic_id).each do |entry|
      ingest_one(batch, entry)
    end

    batch.update!(
      status: "ended",
      ended_at: Time.current,
      succeeded_count: batch.claude_batch_requests.where(result_status: "succeeded").count,
      errored_count: batch.claude_batch_requests.where.not(result_status: "succeeded").count
    )
  end

  private

  def ingest_one(batch, entry)
    request = batch.claude_batch_requests.find_by(custom_id: entry.custom_id)
    return unless request
    return if request.result_status.present?

    request.update!(
      result_status: entry.result.type,
      result_payload: entry.result.to_h
    )

    ClaudeBatchRequestProcessor.new(request).call if entry.result.type == "succeeded"
  end
end

De downstream processor is application-specifiek — schrijf de classificatie naar het Document, hang de embedding eraan, stuur de notificatie. Houd het saai. De hele winst van Anthropic Message Batches is dat je kosten uit het synchrone pad haalt; geef die winst niet weer terug door de result handler ingewikkeld te maken.

Anthropic Message Batches en Prompt Caching Samen

Dit is het stuk dat de meeste teams missen. Anthropic Message Batches pricing stapelt op prompt caching. Gecachte input tokens binnen een batch worden gefactureerd tegen 0,05x van de synchrone ongecachte rate — de helft van de helft van de basis-input. Als je batch een system prompt of een grote preamble deelt over duizenden requests, structureer hem dan zo dat het prefix identiek is en zet cache_control: { type: "ephemeral" } op het laatste gedeelde blok.

shared_system = [
  { type: "text", text: long_system_prompt,
    cache_control: { type: "ephemeral" } }
]

requests = documents.map do |doc|
  {
    custom_id: "doc-#{doc.id}",
    params: {
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      system: shared_system,
      messages: [{ role: "user", content: "Classify: #{doc.body}" }]
    }
  }
end

De volgorde is belangrijk. Anthropic hasht het prefix tot aan elke cache-marker, dus alles wat tussen requests verschilt moet na de marker komen. Als je de document body in de gecachte system zet, heb je net van elke request een cache miss gemaakt en de korting verbrand.

Voor een dagelijkse run van veertigduizend documenten met een achtduizend-token gedeelde system prompt is het verschil tussen gecachte batches en ongecachte batches ruwweg een orde van grootte op de input-rekening. Ik heb de cache mechanics in detail behandeld in Anthropic Prompt Caching in Rails — dezelfde patronen gelden binnen batches met de extra 50% korting er bovenop.

Operationele Valkuilen bij Anthropic Message Batches

Vijf dingen hebben me in productie gebeten. Geen ervan is subtiel zodra je weet waarop te letten.

Batch size limits doen ertoe. Je kunt geen honderdduizend requests in één batch proppen. Het plafond is tienduizend requests of 256 MB. Daarboven moet je splitsen, en het splitsen moet deterministisch zijn zodat retries niet dubbel verwerken. Ik shard op subject_id % batch_count en sla de shard op de ClaudeBatch row op.

Rate limits op batch creation staan los van synchrone rate limits. Je kunt nog ruim genoeg synchrone tokens over hebben en toch geknepen worden bij batch submissions. Wikkel de create call in retry-with-backoff en behandel 429s als signaal om de submissie af te remmen, niet om de job te laten falen.

Individuele request errors laten de batch niet falen. Een batch kan landen met vijfduizend succeeded en vijfduizend errored en Anthropic rapporteert de batch zelf netjes als ended. Je moet elk resultaat inspecteren. De meest voorkomende per-request error die ik zie is overloaded_error — meestal veilig om de mislukkingen met een delay opnieuw te batchen.

De 24-uurs expiry is een echte expiry. Als Anthropic je batch niet binnen het venster afmaakt, komen de requests die niet zijn voltooid terug als expired en betaal je daar niet voor. Maar je krijgt ook geen resultaat. Plan altijd voor partial completion en zorg voor een re-submission pad.

Cost reporting loopt achter. De per-batch kosten op het Anthropic dashboard verschijnen later dan synchrone uitgaven. Bepaal je besparingen niet op basis van het live dashboard op de dag dat je shipt — wacht een week, kijk naar de factuur.

Veelgestelde Vragen

Hoeveel bespaart de Anthropic Message Batches API daadwerkelijk ten opzichte van synchrone Claude API calls?

Anthropic Message Batches worden gefactureerd tegen vijftig procent van de synchrone Messages API tarieven, voor zowel input als output tokens. De korting geldt voor elk model en stapelt op prompt caching, dus een batched request die een warme cache raakt kost ongeveer vijf procent van de synchrone, ongecachte basisprijs. Voor workloads die al prompt caching gebruiken is batching de grootste resterende kosten-hefboom.

Wat is de maximale batch grootte voor Anthropic Message Batches?

Een enkele Anthropic Message Batches submission is geplafonneerd op tienduizend requests of 256 MB payload, wat je het eerst raakt. Daarboven moet je in meerdere batches splitsen. Het verwerkingsvenster is tot vierentwintig uur vanaf creatie, hoewel kleine batches doorgaans in minuten klaar zijn.

Hoe handel ik individuele request failures af binnen een Anthropic Message Batches result set?

Inspecteer de result.type van elke entry in de gestreamde JSONL response. Mogelijke waarden zijn succeeded, errored, canceled en expired. De batch zelf wordt als ended gemarkeerd zelfs als individuele requests falen, dus je moet over elke regel itereren en per request beslissen of je opnieuw inzendt. De meest voorkomende transient failure is overloaded_error, die veilig met een backoff opnieuw te batchen is.

Kan ik Anthropic Message Batches tegelijk met prompt caching en de Anthropic Ruby SDK gebruiken?

Ja. Voeg cache_control: { type: "ephemeral" } toe op het gedeelde prefix van elke request binnen de batch, exact zoals je dat voor synchrone calls zou doen. Gecachte input tokens binnen een batch worden gefactureerd tegen 0,05x de synchrone ongecachte rate. De Anthropic Ruby SDK geeft het cache control veld ongewijzigd door, dus het patroon is identiek aan niet-batched code.

Wanneer moet ik Anthropic Message Batches niet gebruiken?

Alles wat user-facing is waar een mens op het antwoord wacht, elke agentic loop waarin de volgende call afhangt van de model-output, en elke streaming workload. Blijf voor die gevallen op de synchrone Messages API en gebruik prompt caching voor de kosten. Batching is voor “stuur tienduizend jobs in, kom later terug, schrijf de resultaten naar de database” werk — geen interactief verkeer.


Hulp nodig met het verlagen van LLM-kosten in productie Rails? TTB Software is gespecialiseerd in betrouwbare AI-integraties voor Rails apps — wij bouwen de batch pipelines, caching en async infrastructuur die Claude betaalbaar maakt op schaal. We doen Rails al negentien jaar.

#anthropic-message-batches #claude-api-batch-processing #anthropic-ruby-sdk #rails-llm-cost-reduction #claude-batch-api #rails-async-llm #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