Batch API de un LLM: paga la mitad por lo que no corre prisa
Batch API de un LLM: envía miles de peticiones asíncronas a Claude u OpenAI y paga un 50 % menos. Límites, código TypeScript y cuándo no compensa esperar.

Te piden reclasificar 50.000 tickets de soporte antiguos con la taxonomía nueva. Escribes un script con Promise.all, lo lanzas contra la API síncrona de tu LLM, sin pasar por su Batch API, y te vas a por un café. Cuando vuelves, la mitad ha fallado con 429, el reintento ha duplicado peticiones y la factura ya no cuadra con lo que estimaste.
Lo peor es que nadie necesitaba esos tickets en dos minutos. Los necesitaban para el informe del jueves. Has pagado precio de tiempo real y has peleado contra el rate limit por un trabajo que podía ir a la Batch API de un LLM y esperar toda la noche.
Para eso existe la Batch API. Anthropic y OpenAI la ofrecen con el mismo trato: tú renuncias a la respuesta inmediata y ellos te cobran la mitad.
En corto: la Batch API de un LLM te deja enviar miles de peticiones de golpe, procesarlas de forma asíncrona en un máximo de 24 horas y pagar un 50 % menos por cada token. Encaja con clasificaciones masivas, evals nocturnas, descripciones de catálogo y backfills. No sirve para nada que tenga a un usuario esperando ni para agentes que encadenan turnos.
¿Qué es la Batch API de un LLM?
Una Batch API es un endpoint de procesamiento asíncrono del proveedor del modelo que acepta muchas peticiones en un solo envío, las procesa cuando tiene capacidad libre y te devuelve todos los resultados juntos, a cambio de un descuento sobre el precio normal por token.
El flujo tiene tres pasos en los dos proveedores. Envías las peticiones, cada una con un custom_id que tú eliges. Consultas el estado hasta que el trabajo termina. Descargas los resultados y los casas con tus datos por ese custom_id, porque el orden de salida no está garantizado.
Cada petición se procesa de forma independiente. Si una falla, las demás siguen. Y dentro de cada petición puedes usar casi todo lo que usarías en una llamada normal: system prompt, visión, tool use, thinking.
Message Batches API de Claude y OpenAI Batch API: límites reales
Los dos grandes proveedores coinciden en el descuento y en la ventana de 24 horas, pero difieren en cómo envías el trabajo y en los límites. Estos son los datos de la documentación de Anthropic y de la guía de OpenAI, consultadas en octubre de 2026:
| Criterio | Anthropic — Message Batches API | OpenAI — Batch API |
|---|---|---|
| Descuento | 50 % sobre todo el uso (input, output y tokens especiales) | 50 % |
| Máximo por envío | 100.000 peticiones o 256 MB | 50.000 peticiones o 200 MB |
| Ventana de proceso | Hasta 24 h; la mayoría termina en menos de 1 h | completion_window: "24h" (único valor) |
| Cómo envías | JSON con array requests en una llamada |
Fichero .jsonl subido antes con la Files API |
| Estados del envío | in_progress, canceling, ended |
validating, in_progress, finalizing, completed, expired, cancelling, cancelled, failed |
| Disponibilidad de resultados | 29 días desde la creación | El fichero de salida se borra 30 días después de completarse |
| Rate limits | Propios; no consumen los de la Messages API | Pool separado; no consumen tus límites por modelo |
| No admite | stream, speed (fast mode), max_tokens: 0 |
Mezclar modelos en un mismo fichero |
| Limitación / riesgo | Sin fallbacks; resultados descargables solo 29 días |
Un solo modelo por fichero; paso previo de subida con la Files API; la salida se borra a los 30 días |
Un detalle que cambia cómo diseñas el reintento: en Anthropic, las peticiones errored, canceled y expired no se facturan. Solo pagas las peticiones succeeded, y un rechazo también llega como succeeded.
Cuánto ahorras con el descuento del 50 % en la API de un LLM
El 50 % es fijo, pero el ahorro absoluto depende del volumen y del modelo. Tres escenarios con supuestos declarados, sin caché, calculados con los precios oficiales de Claude de octubre de 2026:
| Escenario (supuesto) | Modelo y precio estándar | Tokens totales | Coste síncrono | Coste con Batch API |
|---|---|---|---|---|
| 50.000 tickets × 600 tokens de entrada y 10 de salida | Haiku 5.5: 0,10 $ / 0,50 $ por MTok (prompts de hasta 100.000 tokens) | 30 MTok in, 0,5 MTok out | 3,25 $ | 1,625 $ |
| 20.000 fichas de catálogo × 1.500 in / 400 out | Sonnet 5.5: 2 $ / 10 $ por MTok | 30 MTok in, 8 MTok out | 140 $ | 70 $ |
| Evals nocturnas: 2.000 casos × 3.000 in / 1.000 out, 30 noches | Opus 5.5: 4 $ / 20 $ por MTok | 180 MTok in, 60 MTok out | 1.920 $ al mes | 960 $ al mes |
Los tokens de thinking se facturan como salida. En Sonnet 5.5 y Opus 5.5 el thinking va activo por defecto, así que tu salida real puede ser mayor que la del supuesto.
La primera fila es la que nadie enseña. Clasificar 50.000 tickets con un modelo pequeño cuesta poco de cualquier forma: ahorras 1,625 $. Ahí la Batch API compensa por los rate limits, no por el dinero.
La tercera fila es otra historia. Una suite de evals que corre cada noche con un modelo grande es un gasto recurrente, y partirlo por la mitad son casi mil dólares al mes que no tocan a la calidad del resultado. Si todavía no sabes cuántos tokens consume tu pipeline, empieza por medir el consumo de tokens de tu agente antes de hacer cuentas.
Batch API vs llamada síncrona vs bucle local con concurrencia
La alternativa que la mayoría usa sin pensarlo es el bucle local: un script que lanza peticiones normales con un límite de concurrencia. Funciona, pero pagas precio completo y gestionas tú los reintentos.
| Criterio | Llamada síncrona | Batch API | Bucle local con concurrencia |
|---|---|---|---|
| Latencia | Segundos | Minutos a 24 h | Segundos por petición |
| Precio | 100 % | 50 % | 100 % |
| Rate limits | Los del endpoint normal | Pool propio | Los del endpoint normal; los agotas rápido |
| Reintentos | Tuyos | Reenvías solo los fallidos | Tuyos, con backoff |
| Streaming | Sí | No | Sí |
| Infraestructura | Ninguna | Guardar el ID y hacer polling | Cola, backoff y control de concurrencia |
| Limitación / riesgo | Caro a escala y choca con el rate limit | Sin garantía de hora de entrega; las peticiones pueden expirar | Precio completo y un 429 en cascada si calculas mal la concurrencia |
El criterio es simple. Si un humano espera la respuesta, síncrona. Si nadie la espera antes de mañana y son más de unos cientos de peticiones, Batch API. El bucle local queda para lo que necesitas en minutos pero no en segundos.
Message Batches API de Claude en TypeScript
Este es el ejemplo de los tickets con el SDK oficial @anthropic-ai/sdk. El código está comprobado con tsc --noEmit en modo strict contra la versión 0.133.0 del SDK y TypeScript 7.0.2. No se ha ejecutado contra la API, así que no hay salida que enseñar.
Un aviso antes del código: Haiku 5.5 piensa por defecto, y los tokens de thinking cuentan dentro de max_tokens y se facturan como salida. Con max_tokens: 10 y el thinking activo, la respuesta puede cortarse antes de la etiqueta, así que el ejemplo lo apaga y busca el bloque de texto en lugar de coger el primero.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // lee ANTHROPIC_API_KEY del entorno
type Ticket = { id: string; body: string };
const SYSTEM_PROMPT =
"Clasifica el ticket de soporte en una sola palabra: billing, bug, feature o other.";
// 1. Crear el batch: una petición por ticket, identificada por custom_id
export async function submitTickets(tickets: Ticket[]): Promise<string> {
const batch = await client.messages.batches.create({
requests: tickets.map((ticket) => ({
custom_id: `ticket-${ticket.id}`, // ^[a-zA-Z0-9_-]{1,64}$
params: {
model: "claude-haiku-5-5",
max_tokens: 10,
// Haiku 5.5 piensa por defecto y esos tokens cuentan dentro de max_tokens
thinking: { type: "disabled" },
system: [
{
type: "text",
text: SYSTEM_PROMPT,
// caché de 1 h: el batch puede tardar más de 5 min. Solo cachea si el
// prefijo llega a 512 tokens en Haiku 5.5; un prompt así de corto no se cachea
cache_control: { type: "ephemeral", ttl: "1h" },
},
],
messages: [{ role: "user", content: ticket.body }],
},
})),
});
return batch.id; // guárdalo: es lo único que necesitas para volver
}
// 2. Consultar el estado hasta que termine (o lanzarlo desde un cron)
export async function waitForBatch(batchId: string): Promise<void> {
while (true) {
const batch = await client.messages.batches.retrieve(batchId);
if (batch.processing_status === "ended") {
console.log(batch.request_counts); // succeeded, errored, canceled, expired
return;
}
await new Promise((resolve) => setTimeout(resolve, 60_000));
}
}
// 3. Recoger resultados: el orden NO coincide con el de entrada
export async function collectResults(batchId: string) {
const labels = new Map<string, string>();
const retry: string[] = [];
for await (const item of await client.messages.batches.results(batchId)) {
const { result } = item;
switch (result.type) {
case "succeeded": {
// un rechazo llega como succeeded con stop_reason "refusal";
// max_tokens significa que la respuesta se cortó antes de la etiqueta
const stop = result.message.stop_reason;
if (stop === "refusal" || stop === "max_tokens") {
retry.push(item.custom_id);
break;
}
const block = result.message.content.find((b) => b.type === "text");
if (block?.type === "text") labels.set(item.custom_id, block.text.trim());
break;
}
case "errored":
// invalid_request_error: arregla la petición antes de reenviarla
if (result.error.error.type !== "invalid_request_error") retry.push(item.custom_id);
break;
case "expired":
case "canceled":
retry.push(item.custom_id); // no se facturan: reenvíalos en otro batch
break;
}
}
return { labels, retry };
}
Tres decisiones más del código que no son de adorno. La primera: submitTickets devuelve solo el ID. En producción lo guardas en base de datos y separas el envío de la recogida, porque el proceso que crea el batch no debería quedarse vivo horas esperando.
La segunda es el rechazo. Según la documentación de refusals, una petición rechazada vuelve como succeeded con stop_reason: "refusal". Si solo miras result.type, cuentas como éxito algo que no tiene etiqueta.
La tercera es el tipo del error. En el SDK, el objeto de error viene envuelto: el tipo útil está en result.error.error.type, no en result.error.type, que siempre vale "error". TypeScript lo detecta en compilación porque esa comparación no puede ser verdadera. En JavaScript sin tipos pasa a producción, y la condición siempre se cumple: acabarías reintentando también las peticiones inválidas.
Si es la primera vez que tocas la API de Claude desde código, el crash course de la API de Claude cubre la parte síncrona que aquí doy por sabida.
Si quieres montar este tipo de pipelines dentro de un producto real, es justo el terreno del curso Construye con IA.
Batch API y prompt caching: los descuentos se suman, los aciertos no están garantizados
Sí: los multiplicadores de prompt caching se aplican encima del 50 % de la Batch API, pero dentro de un batch los aciertos de caché son best-effort.
Es la pregunta que apareció el mismo día del lanzamiento. En un comentario del hilo de Hacker News del anuncio, un usuario contaba que ya ahorraba "~75%" con prompt caching y preguntaba si la Batch API se acumulaba con la caché. La respuesta, citando la documentación: se puede, pero sin garantía de acierto.
Hoy la página de precios lo confirma: los multiplicadores de caché se aplican encima del descuento de la Batch API. La guía de batches matiza que los aciertos son best-effort y que las tasas observadas van del 30 % al 98 % según el patrón de tráfico.
Para subir esa tasa, pon el contenido compartido idéntico al principio de cada petición y usa el TTL de 1 hora, como en el ejemplo. Ojo con el mínimo: en Haiku 5.5, un prefijo de menos de 512 tokens no se cachea aunque lleve cache_control, y la API no da error. Si quieres entender cómo funciona la caché por debajo, lo explico en el post sobre prompt caching en la API de Claude.
Evals nocturnas con la Batch API
Las evals son el caso más rentable porque son recurrentes, voluminosas y nadie las mira hasta la mañana siguiente. Un job a las 02:00 envía los casos, otro a las 07:00 comprueba que el envío está ended, recoge resultados y escribe el informe.
La condición es que tus evals se puedan puntuar sin intervención humana. Si todavía dependen de que alguien lea las salidas, el descuento no arregla nada. Las evals deterministas para agentes son el punto de partida, y en el ebook gratuito El Developer Agéntico está la arquitectura completa de un agente con su capa de evaluación.
Cuándo NO usar la Batch API
La Batch API tiene un coste que no aparece en la factura: el tiempo. Estos son los casos en los que no compensa.
Cuando hay un usuario esperando. No hay streaming ni hora de entrega garantizada. La mayoría termina en menos de una hora, pero la propia documentación avisa de que, con mucha demanda, más peticiones pueden expirar a las 24 horas.
Agentes con tool use de varios turnos. Cada turno depende del anterior, así que cada turno sería un envío nuevo. Eran Sandler lo probó y lo contó (en Hacker News): con un turno por envío, cada paso tardaba de 90 a 120 segundos, "agent, but with a two-minute polling spinner between every turn".
Su conclusión es que la unidad que compensa agrupar es una flota de agentes, no los turnos de uno solo.
Volumen pequeño. Con unas decenas de peticiones baratas, el ahorro es de céntimos y añades polling, persistencia del ID y una segunda fase de recogida. La complejidad cuesta más que lo que ahorras.
Cuando necesitas fallback automático. El parámetro fallbacks de la API de Claude no funciona en la Message Batches API: una petición que lo incluye vuelve como errored. Si tu flujo depende de saltar a otro modelo tras un rechazo, tienes que reenviar tú los rechazados, como explico en el post sobre refusals y fallbacks en Claude Opus 5.5.
Dos riesgos más que conviene saber antes del primer envío grande. Por el procesamiento concurrente, un batch puede pasarse un poco del límite de gasto configurado en tu workspace. Y un batch enviado no se puede modificar: si descubres un error en el prompt, cancelas y vuelves a enviar (la cancelación no es inmediata). Además, si usas Haiku 5.5, ten en cuenta que no tiene fallback del lado del servidor ni siquiera fuera de los batches.
Por dónde empezar con la Batch API
Busca en tu código un job que ya corra en un cron o en una cola: un backfill, una regeneración de descripciones, la suite de evals. Si su resultado no lo consume nadie en la próxima hora, es candidato.
Mándalo primero con una sola petición por la API normal para validar el formato. Después envía cien con la Batch API, comprueba que casas bien los resultados por custom_id y que tratas los rechazos. Solo entonces escala al volumen completo.
Preguntas frecuentes
¿Cuánto tarda en procesarse un batch?
Hasta 24 horas en Anthropic y en OpenAI. Anthropic indica que la mayoría termina en menos de una hora, pero no lo garantiza. En Anthropic, las peticiones que no se procesan en 24 horas acaban como expired; en OpenAI el batch pasa a expired y las pendientes aparecen en el fichero de errores con batch_expired.
¿La Batch API es más barata en todos los modelos?
En Anthropic, todos los modelos activos la soportan y el descuento es del 50 % en input, output y tokens especiales. En OpenAI, el descuento también es del 50 %, y cada fichero de entrada solo puede usar un modelo.
¿Puedo combinar la Batch API con prompt caching?
Sí. Los descuentos se acumulan, pero los aciertos de caché son best-effort porque las peticiones se procesan en paralelo y sin orden. Usar el TTL de 1 hora aumenta las probabilidades.
¿Qué pasa si una petición del batch falla?
No afecta al resto. Cada resultado llega con su tipo: succeeded, errored, canceled o expired. En Anthropic, las tres últimas no se facturan; reenvías solo esas en un batch nuevo.
¿Los resultados llegan en el mismo orden en que los envié?
No. Ni Anthropic ni OpenAI garantizan el orden. Usa siempre el custom_id para casar cada resultado con su petición original.
¿Sirve la Batch API para agentes de IA?
Para un agente interactivo, no: cada turno de tool use esperaría minutos. Sí sirve para trabajo de agentes que no tiene prisa, como evals nocturnas o tareas independientes de muchos agentes agrupadas en un mismo envío.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Quieres construir con agentes en serio?
Curso «Construye con IA» con Claude Code, de la idea al producto.
Developer con más de 15 años en frontend y creador de Dominicode. Escribe sobre IA aplicada al desarrollo de software.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.