Apogée Consult
Retour au blog
Jules Ginhac
Jules GinhacCo-Founder & ingénieur IA

Je suis Jules Ginhac, Co-Founder & ingénieur IA chez Apogée Consult à Lyon. Je conçois et déploie des architectures IA génératives (RAG, agents, LLMOps) pour des PME, startups et organisations publiques.

Batching API Anthropic : gain réel mesuré sur nos workloads d'extraction

  • llm-comparison

On a testé l'API Message Batches d'Anthropic sur nos jobs d'extraction. Résultat : 46 % de réduction de coût sur un workload de 12 000 requêtes par nuit.

Batching API Anthropic : gain réel mesuré sur nos workloads d'extraction

L'API Message Batches d'Anthropic offre 50 % de réduction tarifaire en échange d'un délai de traitement allant jusqu'à 24 heures. Sur le papier, c'est attractif. En pratique, le bénéfice réel dépend entièrement de la nature du workload. Voici ce que nos mesures donnent.

Le contexte : 12 000 extractions par nuit

Nous avons un job nocturne qui extrait des données structurées depuis des fichiers texte, contrats, fiches produit, rapports PDF préalablement OCRisés. Chaque nuit, entre 10 000 et 14 000 requêtes sont envoyées à Claude Sonnet.

Avant le passage au batch, ce job tournait en parallèle avec un pool de workers asynchrones limités à 50 requêtes simultanées pour respecter les rate limits. Durée totale : environ 40 minutes. Coût : autour de 18 € par nuit.

Le délai de 24 heures n'était pas un problème ici. Les extractions alimentent un tableau de bord consulté en journée.

Architecture du batch

L'API Message Batches n'est pas un simple wrapper. Elle exige un format spécifique : chaque requête est un objet custom_id + params. Le batch est soumis en une seule requête HTTP, traité côté Anthropic, et les résultats sont récupérés via polling ou webhook.

import anthropic
import json

client = anthropic.Anthropic()

def build_batch_requests(documents: list[dict]) -> list[dict]:
    requests = []
    for doc in documents:
        requests.append({
            "custom_id": doc["id"],
            "params": {
                "model": "claude-sonnet-4-5",
                "max_tokens": 512,
                "messages": [
                    {
                        "role": "user",
                        "content": f"Extrais les champs suivants en JSON : {doc['text']}"
                    }
                ]
            }
        })
    return requests

def submit_batch(requests: list[dict]) -> str:
    batch = client.messages.batches.create(requests=requests)
    return batch.id

def poll_until_done(batch_id: str) -> list:
    import time
    while True:
        batch = client.messages.batches.retrieve(batch_id)
        if batch.processing_status == "ended":
            break
        time.sleep(60)

    results = []
    for result in client.messages.batches.results(batch_id):
        results.append({
            "id": result.custom_id,
            "output": result.result.message.content[0].text
            if result.result.type == "succeeded"
            else None,
            "error": result.result.error.message
            if result.result.type == "errored"
            else None
        })
    return results

Un point important : la limite actuelle est de 100 000 requêtes par batch et 256 Mo au total. Nos 12 000 requêtes tiennent largement dans un seul batch.

Résultats mesurés sur 30 nuits

MétriqueAvant batchAprès batch
Coût moyen par nuit18,20 €9,80 €
Durée de traitement38 min2h à 6h
Taux d'erreur0,8 %1,1 %
Requêtes en échec définitif0,2 %0,3 %

La réduction effective est de 46 %, légèrement sous les 50 % annoncés. L'écart vient des tokens de prompt mis en cache différemment : en mode synchrone, le cache de contexte système était mutualisé sur plusieurs requêtes consécutives ; en batch, Anthropic ne garantit pas cet ordonnancement.

Ce que le batch change dans votre code

Le principal changement architectural : votre job ne se termine plus en une seule exécution. Il faut séparer la soumission du batch de la récupération des résultats.

Nous utilisons un simple état persisté en base :

# Soumission, lancée à 23h
batch_id = submit_batch(requests)
db.save_batch_state(batch_id, status="pending", submitted_at=now())

# Récupération, tâche séparée, lancée toutes les 30 min
def collect_results():
    pending = db.get_pending_batches()
    for batch in pending:
        status = client.messages.batches.retrieve(batch.id)
        if status.processing_status == "ended":
            results = list(client.messages.batches.results(batch.id))
            db.store_results(batch.id, results)
            db.update_batch_state(batch.id, status="done")

C'est plus de surface de code à maintenir. La contrepartie est que le job de soumission est presque instantané, et que les erreurs partielles sont isolées par custom_id plutôt que de faire tomber tout le worker.

Cas où le batch ne s'applique pas

La frontière est simple : si l'utilisateur attend une réponse, le batch est inutilisable. Mais même pour des jobs asynchrones, le batch peut être contre-productif si :

  • Votre SLA nocturne est inférieur à 2 heures, le délai n'est pas garanti.
  • Vous avez des dépendances entre requêtes, le batch ne permet pas d'enchaîner des appels.
  • Votre volume est trop faible pour que l'économie de 50 % justifie la complexité supplémentaire.

À moins de 500 requêtes par job, la différence de coût est souvent inférieure à 1 €. Le coût de la complexité additionnelle dépasse alors le gain.

Une limite documentée à surveiller

Anthropic peut rejeter un batch si un seul document dépasse 32 000 tokens de prompt. Nous avons eu deux rejets complets à cause de documents mal préprocessés. La solution : valider la taille de chaque requête avant la soumission et isoler les outliers dans un appel synchrone séparé.

Le batch API est aujourd'hui en disponibilité générale. Les webhooks de complétion sont en beta. Si votre volume nocturne dépasse quelques milliers de requêtes et que votre tolérance au délai est suffisante, le gain est réel et mesurable.

La question qui reste ouverte : est-ce qu'Anthropic maintiendra cette décote à 50 % à mesure que la demande augmente, ou est-ce une fenêtre temporaire pour pousser l'adoption ?

Disponible pour de nouveaux projets

Un projet à concrétiser ?
Parlons-en, sans engagement.

Un échange de 30 minutes pour cadrer votre besoin, qualifier la faisabilité et vous proposer une trajectoire claire.

1// kick-off : réponse sous 24h
2const project = await apogee.scope({
3 type: 'web | mobile | IA',
4 timeline: '6 à 16 semaines',
5 approach: 'sur-mesure'
6})
7// → cadrage offert
Anthropic Batch API : gain réel sur nos workloads | Apogée Consult