Anthropic Batch API in 2026: the constraints nobody mentions until you hit them
The Batch API's default prompt cache expires in 5 minutes while the batch itself runs for up to an hour, so the two cost levers quietly cancel each other out. Six documented constraints, and the single assumption underneath all of them. Field log, September 2026.
Updated on September 16, 2026
On this page
Quick answer
I spent 16 September 2026 reading how the Anthropic Batch API actually behaves, after watching a classification job get moved onto it for the discount and produce a bill that did not come close to halving.
Every guide about this API opens with the same number: batched requests cost 50 percent of standard prices. That number is real and it is not the interesting part. The interesting part is that a batch is slow and asynchronous, and several other things in your stack quietly assume it is neither. Almost every surprise below is that one assumption breaking somewhere new.
Three are worth the read even if you never submit a batch.
The default prompt cache expires before your batch finishes. An ephemeral cache entry lives 5 minutes. Most batches take up to an hour. Put a large shared prefix in a batch with default caching and the cache is cold long before the run is done, so you spend the whole batch paying to write a cache nothing reads.
max_tokens: 0 is rejected inside a batch, and the stated reason is the same clock. The cache pre-warming trick you would reach for synchronously is structurally unavailable here, because the entry would expire before anything used it.
Your workspace spend limit is not a hard ceiling for batches. Anthropic's own documentation says batches may go slightly over it. A cost control that a cost optimisation can overshoot is worth knowing about before month end, not after.
The first one is the one I would not have guessed, and it is the one that quietly cancels out the discount people came for.
The moment
A colleague moved a nightly document classification job from synchronous calls to batches, expecting the bill to roughly halve. It fell by about a fifth. Nothing errored, the job ran fine, the results were correct, and the invoice just refused to behave.
The job shared a long system prompt across every request, which is exactly the shape prompt caching exists for. It had been caching happily for months on the synchronous path, where the next request landed seconds after the last one.
On the batch path, nothing changed in the prompt. Only the clock did.
Everything below is read from Anthropic's batch processing reference and its prompt caching reference, both read on 16 September 2026, plus the official Python SDK shape. I have flagged throughout what I checked and what I am repeating.
The one sentence that explains the rest
A batch is a set of independent requests that the platform will run whenever it gets to them, and it promises you an answer within 24 hours rather than within seconds.
Read that back slowly, because four separate constraints fall straight out of it, and none of them is arbitrary.
Finding 1: the cache clock is shorter than the batch clock
This is the finding that explains my colleague's invoice, and it is the one I have not seen carried in any of the third party write ups that rank for this API.
Two documented numbers sit in tension:
Scroll to see more
| Clock | Documented value |
|---|---|
| Default ephemeral prompt cache lifetime | 5 minutes |
| Typical batch completion | most within 1 hour |
| Hard batch deadline | 24 hours from creation |
The cache lifetime is also measured from the start of the request that writes or reads the entry, not from the end of its response. So the 5 minutes is not 5 minutes of your compute, it is 5 minutes of wall clock that begins before you have an answer.
Now put a shared 40,000 token system prompt into a 5,000 request batch. The first request writes the cache entry. If the platform is working through your batch over 40 minutes, the entries written in minute one are long gone by minute seven, and requests after that write a fresh entry rather than read a warm one.
The reason that hurts rather than merely disappointing is the direction of the pricing. As of September 2026 a cache write is charged at a multiplier above base input, and a cache read at a fraction of it:
Scroll to see more
| Operation | Multiplier on base input |
|---|---|
| 5 minute cache write | 1.25x |
| 1 hour cache write | 2x |
| Cache read | 0.1x |
A cache you never read is not neutral. It is a 1.25x surcharge on the part of the prompt you were trying to make cheap, repeated every time the entry lapses.
The documented fix is to make the cache clock match the batch clock by asking for the longer lifetime explicitly:
{
"type": "text",
"text": "your large shared system prompt",
"cache_control": { "type": "ephemeral", "ttl": "1h" }
}
That write costs 2x instead of 1.25x, which looks worse until you notice it is a one time cost that the rest of the batch then reads at 0.1x instead of re writing at 1.25x.
The part that makes this worth doing rather than merely worth knowing: Anthropic's caching reference states that these multipliers stack with other pricing modifiers including the Batch API discount. The two cost levers are not alternatives. They compose, and by default they fight.
Anthropic does document this. It is a single tip box on the batch page, phrased as a suggestion to "consider" the longer duration. I think it deserves to be the loudest sentence on the page, because getting it wrong does not fail, it just silently costs more while looking like it worked.
Finding 2: the same clock again, and this time it is a hard rejection
max_tokens: 0 is the documented trick for pre warming a cache synchronously: send a request that writes the entry and generates nothing.
Inside a batch it is rejected outright as a validation error. The documented reason is worth quoting closely, because it is the same mechanism as finding 1: an ephemeral cache entry written during batch processing would likely expire before the follow up request runs.
So this is not an arbitrary API restriction. It is the platform declining to let you build something that its own timing guarantees would break. Two different surfaces, one clock.
Finding 3: three parameters are rejected, and they are all latency features
Including any of these in a batched request returns a validation error, as of September 2026:
Scroll to see more
| Parameter | Documented reason |
|---|---|
stream: true | Batch results come back as a single file, not a stream |
speed (fast mode) | Fast mode tunes synchronous latency, which does not apply to asynchronous processing |
max_tokens: 0 | The cache entry would expire before anything read it |
Notice what they have in common. Every one of them is a feature about when you get tokens rather than which tokens you get. A batch has opinions about when, so the batch wins.
That is a genuinely useful filter to carry: if a parameter exists to make a response arrive sooner or more smoothly, do not expect it to survive batching. Conversely, the things that change what the model produces do survive. Vision, tool use including server tools, system messages, multi turn conversations and extended thinking are all documented as batchable, and requests of different shapes can be mixed freely in one batch because each is processed independently.
Finding 4: your spend limit is not a ceiling here
Quoting the documentation closely, because this one deserves to be read in the original: because of high throughput and concurrent processing, batches may go slightly over your workspace's configured spend limit.
I want to be careful about what I am and am not saying. "Slightly" is not quantified anywhere I could find, and I did not test it, so I cannot tell you the size of the overshoot. What I can tell you is the direction, and that the mechanism is plausible on its face: thousands of requests running concurrently cannot each check a shared budget counter before starting without serialising the thing that makes batching fast.
The practical read is that a workspace spend limit is a good backstop for interactive traffic and a soft one for batch traffic. If you are using the limit as your only guard against a runaway job, batch submission is the wrong place to rely on it. Size the batch instead.
Finding 5: rate limits apply inside the batch, not just to your submission
This is the one I suspect is behind most of the public frustration with this API, and it took reading the limitations section twice to notice.
Rate limits apply both to Batches API HTTP requests and to the number of requests within a batch waiting to be processed. The documentation then adds that processing may be slowed down based on current demand and your request volume, and that in that case you may see more requests expiring after 24 hours.
Read that as a system rather than as two sentences. Submitting successfully does not mean your work is queued for guaranteed execution. It means it is queued subject to limits, and the documented failure mode under load is not an error at submission time. It is expiry, a day later, on some fraction of a batch you already believe you sent.
That reframes expiry from an edge case into a capacity outcome. It is also why the 24 hour deadline deserves respect even though most batches finish inside an hour: the deadline is not the expected duration, it is the failure boundary, and load pushes work toward it.
Worth knowing about the deadline specifically: expires_at is stamped when the batch is created, not when processing begins. The clock starts at submission.
Finding 6: the size ceiling is two ceilings
A batch is limited to either 100,000 requests or 256 MB, whichever is reached first.
Most people size a batch by request count, because that is the number they control in a loop. The MB ceiling is the one that surprises, and it is easy to hit well under 100,000 requests if each carries a large shared prefix, an image, or a long document. The failure is at submission, which is at least the honest place for it.
A batch of 5,000 requests each carrying a 40,000 token system prompt is nowhere near the request ceiling and can be comfortably over the size one. Those are the same batches most likely to be built by someone thinking about caching, which is a slightly unfair overlap.
A pre flight worth 20 lines
Given findings 2 through 6, most of what goes wrong with a batch is knowable before you submit it. This is the check I would now run first, written against the official Python SDK:
import json
REJECTED = ("stream", "speed")
MAX_REQUESTS = 100_000
MAX_BYTES = 256 * 1024 * 1024
def preflight(requests):
problems = []
blob = json.dumps(requests).encode("utf-8")
# a non empty slice past the ceiling means we are over it
if requests[MAX_REQUESTS:]:
problems.append(f"{len(requests)} requests, ceiling is {MAX_REQUESTS}")
if blob[MAX_BYTES:]:
problems.append(f"{len(blob) / 1024 / 1024:.1f} MB, ceiling is 256 MB")
ids = [r["custom_id"] for r in requests]
if len(set(ids)) != len(ids):
problems.append("custom_id values are not unique")
for r in requests:
params = r["params"]
for key in REJECTED:
if key in params:
problems.append(f"{r['custom_id']}: {key} is rejected in batches")
if params.get("max_tokens", 1) == 0:
problems.append(f"{r['custom_id']}: max_tokens 0 is rejected in batches")
return problems
The size check is the one that earns its keep, because it is the only ceiling here you cannot estimate by eye. Both ceiling tests are written as slices rather than comparisons, which reads oddly for about two seconds and then reads fine: a non empty slice past the ceiling is exactly the condition you care about.
On cache TTL, the honest advice is a rule of thumb rather than a lint: if a batch shares a prefix worth caching and you expect it to take longer than about 5 minutes, which is most batches worth submitting, ask for the 1 hour lifetime. I have not built a linter for that because the condition depends on a runtime you do not know at submission.
So when is this actually the right tool
The decision is less about cost than the framing suggests. It is about whether anything downstream is waiting.
Batching is the right call for work that is genuinely independent per item, latency tolerant in the real sense that nobody is watching a spinner, and large enough that the ceilings matter more than the round trip. Bulk classification, evaluation runs over a fixed dataset and offline enrichment all fit cleanly.
It is the wrong call the moment something is waiting on the result, and it is a poor fit for work where the items are not independent, because a batch gives you no ordering and no way to feed one result into the next request.
If you want the recipe level treatment of submitting, polling and reconciling results by custom_id, my colleagues over at PromptAttic wrote the batch recipes that ship, which covers the happy path in more detail than I do here. This piece is deliberately the other half: the constraints that decide whether the happy path is available to you at all.
Two related things I have written up separately, since both turn out to matter here. The cost column in ccusage is what I would use to notice a cache that is not being read, because the symptom is an input token cost that will not come down. And the auto compact field log covers the same cache lifetime question from the interactive side, where the 5 minute default is usually the right one.
For comparison, OpenAI runs an equivalent batch endpoint with the same broad shape, a 24 hour window and a discount, so the general pattern is not Anthropic specific. The cache interaction described in finding 1 is specific to how Anthropic prices and times its cache, and I have not checked whether the equivalent tension exists there.
What I did not verify
This is a documentation reading, not an instrumented benchmark, and the split matters more than usual on a piece about cost.
What I read directly and quote above: Anthropic's batch processing and prompt caching references, both on 16 September 2026, and the official Python SDK's batch surface.
What I did not measure: I ran no controlled cost comparison of a batch with default caching against the same batch with a 1 hour TTL. That is the experiment that would turn finding 1 from a documented mechanism into a number, and it is the number I actually want. The reasoning above is arithmetic over published multipliers plus two published clocks, which is a strong argument and is not a measurement. If you have run it, I would genuinely like to see the result.
I also could not open three community threads that Google surfaces prominently for this API: a Reddit thread titled "Claude Batch API is Officially Over", a Medium write up reporting a submission refused for exceeding the payload limit, and a Hacker News thread about a tool for routing work to the Batch API. All three rate limited or blocked me. I am relying on their indexed summaries only, I have not read the full arguments, and I have deliberately not characterised what those authors concluded beyond noting that the friction exists and that the payload limit in the Medium summary is consistent with the documented 256 MB ceiling.
The quantity I am least sure about is the spend limit overshoot, where the documentation says "slightly" and gives no figure.
If you only take one thing: a batch is a promise about when, so before you submit, check every other part of your request that also has an opinion about when. The cache is the one that will not tell you it lost.
Postscript: I have now spent longer reading about a cache that expires in 5 minutes than that cache has ever existed for, which is a sentence I am choosing to find funny rather than sad.
Written by
Dani ReyesDani Reyes writes DevMoment field notes on AI dev workflow, tested on real work rather than demos.
Frequently asked questions
Why did my Anthropic batch cost more than half the synchronous price?
The most common cause in 2026 is prompt caching going cold mid batch. An ephemeral cache entry lives 5 minutes by default and that clock starts at the beginning of the request that writes it, while most batches take up to an hour to work through. If your requests share a large prefix, entries written early have lapsed long before the later requests run, so those requests write a fresh entry at a multiplier above base input instead of reading a warm one at a fraction of it. Asking for the 1 hour cache lifetime on the shared block makes the cache clock match the batch clock.
Can I use prompt caching with the Anthropic Batch API?
Yes, and Anthropic's caching reference states that caching multipliers stack with the Batch API discount, so the two cost mechanisms compose rather than being alternatives. The catch is the lifetime. Set the longer duration explicitly on the cached block with cache_control of type ephemeral and ttl 1h, otherwise the default 5 minute entry will usually expire before a batch finishes.
Why is max_tokens 0 rejected inside a batch?
Because the cache entry it would write is unlikely to survive long enough to be useful. max_tokens 0 is the documented way to pre warm a cache synchronously, where a follow up request lands seconds later. Anthropic's batch documentation states that an ephemeral cache entry written during batch processing would likely expire before the follow up request runs, so the parameter returns a validation error in a batch. It sits alongside stream and speed, the other two rejected parameters, and all three are features about when a response arrives rather than what it contains.
What happens if an Anthropic batch does not finish within 24 hours?
Requests that have not completed expire, and the expires_at timestamp is stamped when the batch is created rather than when processing begins. Expiry is not purely theoretical: the documentation notes that rate limits apply to requests waiting inside a batch as well as to your HTTP calls, that processing may slow based on current demand and your request volume, and that in that case you may see more requests expiring after 24 hours. Results for a batch remain downloadable for 29 days after creation.
Keep reading
ccusage in 2026: what the Cost column actually measures
The Cost column in ccusage is a counterfactual: what your tokens would have cost at pay-per-use list prices, computed locally. On a Claude subscription it is not your bill, and ccusage's own introduction and Anthropic's own docs both say so. A field log on what the number measures, where the caveat is written, and where it is missing.
Claude Code auto compact: what it costs and what you lose (2026)
Claude Code auto compact fires on a per-model token boundary, not a percentage. What it costs is set by prompt cache warmth rather than context size, and a path-scoped rule does not come back afterwards until Claude reads a matching file again.
Claude Code telemetry: what redaction does not cover (2026)
Claude Code's OpenTelemetry export redacts prompts and responses by default. It does not redact who you are. Four identity attributes ship on every datapoint with no switch to turn them off, and one environment variable quietly controls two streams.