Ce qui se passe
Sur 30 jours, 1 842 requêtes réparties en 96 lots sont parties dans l'API
Batch d'Anthropic. 112 n'ont jamais produit de résultat
exploitable — non pas 112 plantages, mais 112 lignes restées à NULL en base,
sans une erreur dans les journaux. Soit 6,1 % de perte silencieuse sur une
chaîne réputée verte.
Le décompte exact :
result.type |
Nombre |
|---|---|
succeeded |
1 747 — dont 17 tronquées (stop_reason: "max_tokens") |
errored |
71 — 52 overloaded_error, 14 invalid_request_error, 5 api_error |
expired |
24, au plafond de 24 heures |
canceled |
0 |
Pourquoi ça compte
Deux faits différents portaient le même nom dans son code.
batch.processing_status == "ended" signifie qu'Anthropic a cessé de travailler
sur le lot — y compris sur ce qu'il a abandonné. Le sort de chaque requête
vit ailleurs : dans batch.request_counts, et dans le champ result.type de
chaque ligne du flux de résultats.
Le défaut le plus coûteux n'est pourtant pas là. Les résultats reviennent dans
un ordre arbitraire. Les apparier par position avec la liste d'entrée, via un
zip, fonctionne tant que tout réussit ; dès qu'une requête manque, la liste
raccourcit et tout ce qui suit se décale d'un cran. Le rapport d'une personne
part alors dans le compte d'une autre. Deux utilisateurs l'ont reçu — le défaut a été
repéré en lisant un compte rendu qui félicitait quelqu'un pour un projet
Kubernetes jamais mentionné pendant l'entretien. Aucune exception, aucune
alerte : une sortie fausse, joliment rendue.
Dernier piège du même ordre : une réponse succeeded dont le stop_reason vaut
max_tokens contient du JSON coupé en deux. Le parseur lève, le except trop
large avale, la ligne reste nulle.
Comment s'y prendre
Indexer les résultats par custom_id, et traiter les quatre types sans branche
par défaut :
by_id = {entry.custom_id: entry for entry in
client.messages.batches.results(batch.id)} # jamais l'ordre
for session in pending_sessions:
entry = by_id.get(session.id)
if entry is None:
mark_retry(session.id, "missing_from_results")
continue
match entry.result.type:
case "succeeded":
msg = entry.result.message
if msg.stop_reason == "max_tokens":
mark_retry(session.id, "truncated")
else:
save_report(session.id, msg.content[0].text)
case "errored":
mark_retry(session.id, entry.result.error.type)
case "expired" | "canceled":
mark_retry(session.id, entry.result.type)Aucun chemin ne laisse disparaître une requête. Quatre règles complètent le
dispositif : écrire la ligne en base avant de soumettre, vérifier
stop_reason sur les succès, retomber sur l'API synchrone après deux tentatives
en lot, et alerter sur l'écart entre lignes soumises et lignes résolues —
le seul indicateur qui aurait attrapé les 112. Sur les 30 jours suivants,
environ 2 100 requêtes sont passées sans une seule ligne non résolue.
Dernier point, sur le choix du format : la latence médiane est de 9 minutes, le p90 de 51 minutes, le p99 de 6 h 20. Batch sert au travail que personne n'attend devant son écran. Le compte rendu d'entretien, attendu juste après la session, y avait été placé à tort : il est repassé sur l'API synchrone, et seul le scoring nocturne est resté en lot. La facture, elle, est passée de 209 à 104 dollars.
Source : Anthropic Batch API: 112 of My 1,842 Jobs Never Came Back, DEV Community, 20 septembre 2026. Le billet indique que son auteur ou autrice développe la plateforme citée.