OpenAI Batch and Anthropic Message Batches look almost identical from a distance. Both offer roughly a 50% discount on input and output tokens versus the synchronous endpoint. Both target a 24-hour completion window. Both accept a JSONL-shaped workload with a per-request custom_id for stable row identity.
The differences live in shape, limits, and operational quirks — and those are exactly the things you care about when you're routing across both. This is a comparison written from the perspective of an operator who has to ingest, dispatch, monitor, and normalize results from each.
Shape
The biggest structural difference is how you hand the work over.
OpenAI is a two-step process: first you upload the JSONL to the Files API, then you create a batch that references the resulting input_file_id.
# 1. Upload the JSONL file
curl https://api.openai.com/v1/files \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-F purpose=batch \
-F file=@requests.jsonl
# -> { "id": "file_abc123", ... }
# 2. Create the batch referencing the file
curl https://api.openai.com/v1/batches \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input_file_id": "file_abc123",
"endpoint": "/v1/chat/completions",
"completion_window": "24h"
}'Anthropic is one step: you POST the requests array inline.
curl https://api.anthropic.com/v1/messages/batches \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{
"requests": [
{
"custom_id": "row-001",
"params": {
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"messages": [
{ "role": "user", "content": "Classify this." }
]
}
}
]
}'The operational implication is real. For OpenAI, the ingestion path is upload-bound: a 200 MB file means a large multipart upload and an extra round-trip before the batch even starts validating. For Anthropic, the ingestion path is parse-bound: a 256 MB JSON body is a chunky parse on both ends, and you need to stream the request to stay under proxy limits.
Limits
Per-batch ceilings differ in both axes — file size and request count.
- OpenAI max file size
- 200 MB
- OpenAI max requests per batch
- 50,000
- Anthropic max payload size
- 256 MB
- Anthropic max requests per batch
- 100,000
- Discount (both)
- 50% on input + output tokens
- Completion window (both)
- 24 hours
If a job is on the boundary — say, 70,000 rows — OpenAI forces a split into two batches, while Anthropic takes it as a single batch. That changes how you track progress: two batch IDs to poll vs one. It also changes how partial failures look: a failed OpenAI half-batch is a 35,000-row failure unit; a failed Anthropic batch is a 70,000-row failure unit.
Status lifecycle
Status names matter when you normalize. A routing layer has to map provider-specific statuses to a stable internal state machine, and the two providers don't agree on either the names or the granularity.
- OpenAI batch statuses
validating→in_progress→finalizing→completed(alsofailed,expired,cancelled)- Anthropic batch statuses
in_progress→ended(alsocanceling)- Anthropic per-request statuses
succeeded,errored,canceled,expired
OpenAI exposes more batch-level granularity (you can tell validating from in_progress from finalizing), but a single completed state doesn't tell you whether every row succeeded. Anthropic uses a flat batch status — ended is the terminal state regardless of outcome — and pushes the per-row outcome into request-level fields you read from the results.
Result delivery
Results come back as JSONL in both cases, but the delivery mechanism is different.
- OpenAI returns an
output_file_id. You download it via the Files API and stream the JSONL. - Anthropic returns a
results_url. You GET that URL and stream the JSONL directly.
If you have a single artifact pipeline that needs to deliver results to a customer-owned R2 or S3 bucket, this difference shows up. The OpenAI path has an extra hop — download from Files, re-stream to the artifact target. The Anthropic path is a direct proxy. Neither is a problem at small volume; both add latency and bytes at large volume.
Things that look the same but aren't
Both APIs support custom_id on each row. Both let you set a stable identifier you can use to match results back to inputs. The semantics, though, are slightly different.
- Uniqueness scope — OpenAI requires
custom_idto be unique within the file. Anthropic requires it unique within the batch request. For mixed-model jobs that get split across providers, you have to decide whether to namespace IDs per lane or carry them through verbatim. - Error handling — OpenAI emits failed rows into a separate
error_file_id, sibling to the output file. Anthropic interleaves errored results into the same.jsonlwith a per-rowresult.typeoferrored. Two reconciliation strategies for the same outcome. - Retry behavior — neither provider retries failed rows automatically. If a row fails because of a provider-side transient (rate limit overflow, capacity, an internal 500), it stays failed unless you resubmit it. The routing layer is where retries live.
- Expiry — both expire incomplete batches at the 24-hour mark. Anything not produced by then is lost; the per-row status becomes
expired. Worth budgeting capacity assuming the SLA is firm rather than soft.
When to pick which (or both)
The honest answer is: model availability is usually the answer. If you need GPT-4o, you're going to OpenAI. If you need Claude Sonnet 4.5, you're going to Anthropic. Most production batch jobs are model-specific, and the comparison is a non-question.
Where it gets interesting is on overlapping coverage — when the same model is reachable through more than one path. Anthropic-direct vs Anthropic-on-Bedrock vs Anthropic-on-Vertex differ on cost, latency, regional availability, and exact API shape. Vision pricing differs across these in ways that matter for image-heavy jobs; input image tokens are not billed the same way across providers, and a batch of 50,000 images per row will show 10–30% spread depending on which path you route.
On overlapping coverage, the routing question becomes: which lane is cheapest right now for this exact model and input shape? That's where running both makes sense — not because you arbitrage one against the other, but because cheapest-lane is a moving target.
How Batchrouter handles both
We expose **one customer batch_id** regardless of how many provider lanes the job actually touches. If you submit a mixed-model JSONL, the router splits it into model-specific internal lanes, dispatches each, and re-assembles results under the original ID. You get a single receipt that itemizes provider subtotals, native lane discounts, control-plane fees per lane, and the final usage charge.
On the wire, the API is OpenAI-Batch-compatible — same JSONL shape, same custom_id semantics, same upload-then-create pattern — so existing OpenAI Batch code drops in without rewrites. Native lanes for both OpenAI and Anthropic carry an additional 10% discount on the provider rate, which we publish on the lane descriptor.
If you want the full API surface, see [/docs/api/batches](/docs/api/batches). Provider-specific notes are at [/providers/openai](/providers/openai) and [/providers/anthropic](/providers/anthropic) — including current native lane discounts, supported endpoints, and the model coverage matrix.