OpenAI Batch API, half price if the job can wait a day

OpenAI's Batch API takes a file of requests, runs them asynchronously, and bills at a 50% cost discount compared with the synchronous APIs. One file carries up to 50,000 requests, and a batch input file can be up to 200 MB, while the completion window can currently only be set to 24h.

So the trade is blunt. You give up latency, keep half the money, and accept that anything unfinished when the day runs out gets cancelled. Below you get the price of your job across every OpenAI model KickLLM holds a validated live rate for, the lifecycle in five calls you can paste, and the queue limits that stall a batch before it starts.

The rules in one table

Price a batch across seven OpenAI models

Type the shape of your job once and read every OpenAI model with a validated live rate. The batch column applies the documented 50% cost discount to the synchronous total, and rows sort by batch cost, cheapest first. Calculations run client side, and no data leaves the browser. The boxes open on a placeholder job, so replace the three figures with yours.

RankModelInput per 1MOutput per 1MSynchronousBatchYou keep

Rates come from the validated field table further down, snapshot 2026-07-27. Per million figures are computed in the browser from those per-token rates.

Expiry is where the two big providers stop agreeing on who pays. On OpenAI, unfinished requests in an expired batch are cancelled, responses to completed requests remain available, and you are charged for tokens consumed by the completed requests. On Anthropic, requests that end as errored or expired are not billed. Same failure, different invoice, which is why the calculator prints the money at stake per slice of the job.

The lifecycle in five calls

Field names, endpoint paths and status values below are the ones OpenAI's batch guide states. Swap the model, the prompt and the file id for yours.

# 1. requests.jsonl, one line per request
{"custom_id": "ticket-0001", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o", "messages": [{"role": "user", "content": "Summarise this ticket."}]}}
{"custom_id": "ticket-0002", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o", "messages": [{"role": "user", "content": "Summarise this ticket."}]}}

# 2. upload the input file through the Files API
POST /v1/files          (multipart form, file=@requests.jsonl)
-> {"id": "file_abc", ...}

# 3. create the batch from that file id
POST /v1/batches
{"input_file_id": "file_abc", "endpoint": "/v1/chat/completions", "completion_window": "24h"}
-> {"id": "batch_xyz", "status": "validating", ...}

# 4. poll until the status stops moving
GET /v1/batches/batch_xyz
status: validating, failed, in_progress, finalizing, completed, expired, cancelling, cancelled

# 5. download both files, match every line back by custom_id
client.files.content(output_file_id)     # results
client.files.content(error_file_id)      # failures
  1. Build the input file. Each line is a JSON object with custom_id (a unique identifier for the request), method (POST), url (the endpoint path) and body (the parameters for that endpoint). Pick custom_id values you can trace back to your own records.
  2. Upload it. You must first upload your input file through the Files API so the batch can reference it.
  3. Create the batch. A batch is created from the uploaded file's id plus the endpoint and completion window, and the completion window can currently only be set to 24h.
  4. Poll it. A batch object moves through validating, failed, in_progress, finalizing, completed, expired, cancelling and cancelled.
  5. Download both files. Completed results come through the Files API using the batch's output_file_id, and errors for failed requests are written to the file referenced by error_file_id. Copy them somewhere durable, because the output file is automatically deleted 30 days after the batch is complete.

Which endpoints take batch requests

The guide lists the supported paths, and a url your account can call synchronously is not automatically one of them. Batch accepts /v1/responses, /v1/chat/completions, /v1/embeddings, /v1/completions, /v1/moderations, /v1/images/generations, /v1/images/edits and /v1/videos.

When a batch fails, expires, or never starts

Most of the pain with batch is not pricing. Something rejects the file, the queue refuses more work, or half the job comes back and you cannot tell what you owe. Each row below names the symptom, the documented cause, and the move.

What you seeWhyWhat to do
Status goes to failed within seconds of creationA batch enters the failed status when its input file has failed the validation processCheck every line parses on its own and names all four fields, since each line must carry custom_id, method, url and body, and confirm the url is one of the endpoints batch accepts
Results carry the message "This request could not be executed before the completion window expired."Unfinished requests in an expired batch return that exact error messageThe finished work is still yours to keep, and you are charged for tokens consumed by the completed requests, so resubmit only the missing custom_id values
A large job will not accept any more queued workEach model has a maximum number of prompt tokens that can be queued for batch processing, and batch queue limits are calculated based on the total number of input tokens queued for a given modelThose per-model caps live on the Platform Settings page rather than in the guide, so read yours there and compare it with the enqueued token figure the calculator above prints. Tokens from a pending batch stop counting against the queue limit once that batch job completes, so draining one job frees headroom for the next
Batch creation starts getting rejected while each file is smallAn organization can create up to 2,000 batches per hourPack more requests per file rather than firing more files, up to the 50,000 request and 200 MB ceiling
Fewer result lines than you submittedFailures are written separately, to the file referenced by error_file_idDownload the error file too and reconcile both against your custom_id list before you call the job done
An output file id that used to work now returns nothingThe output file is automatically deleted 30 days after the batch is completePersist results into your own storage as part of the download step, not as a later chore
Worry that a big batch will throttle your production trafficUsing the Batch API does not consume tokens from your standard per-model rate limitsKeep the synchronous budget for latency-sensitive paths and push bulk work into batch, since the two draw on separate pools

Rate limits and queue limits are two different pools

Worth separating, because the dashboards look similar. Your synchronous ceilings are measured as requests per minute, requests per day, tokens per minute, tokens per day and images per minute. Batch does not draw on those, and instead has its own queue measured in total input tokens queued for a given model. Both sets grow the same way, since OpenAI automatically graduates an organization to the next usage tier as its cumulative API spend goes up.

The same job on Anthropic Message Batches

Anthropic skips the file upload. A Message Batch is created by POSTing a requests array to https://api.anthropic.com/v1/messages/batches, where each entry has a custom_id and a params object holding standard Messages API parameters. The discount matches, because all Message Batches API usage is charged at 50% of the standard API prices.

# create, one call, no file upload
POST https://api.anthropic.com/v1/messages/batches
{"requests": [
  {"custom_id": "ticket-0001",
   "params": {"model": "claude-haiku-4-5", "max_tokens": <int>,
              "messages": [{"role": "user", "content": "Summarise this ticket."}]}}
]}
-> {"processing_status": "in_progress", ...}

# poll until processing_status is "ended", then stream results_url
# one JSON object per line, matched back by custom_id

Two shapes to watch when you port a working OpenAI job across. Anthropic custom_id values must be 1 to 64 characters of alphanumerics, hyphens and the underscore character, matching ^[a-zA-Z0-9_-]{1,64}$, which is stricter than anything the OpenAI guide states. And batch requests reject stream: true, speed (Fast mode), store and previous_thread_event_id, cache_hint and context_hint, and max_tokens: 0 with a validation error, so a request body that works synchronously can still bounce.

Cancelling behaves differently too. An Anthropic batch reads canceling immediately after cancellation, and canceled batches end with status ended and may contain partial results for requests processed before cancellation. Download those partials from the results_url property, streaming the .jsonl rather than pulling it all at once.

Anthropic queue and spend ceilings

The Anthropic side publishes its batch queue numbers directly, where OpenAI puts yours behind the Platform Settings page. The Message Batches API has its own limits shared across models, allowing 200,000 batch requests in the processing queue on Start, 300,000 on Build and 500,000 on Scale, with a maximum of 100,000 batch requests per batch at every tier. Spend has a ceiling as well, since monthly spend caps run $500 on Start, $1,000 on Build and $200,000 on Scale, with no cap on Custom. When you cross a limit, the API returns a 429 describing which rate limit was exceeded along with a retry-after header saying how long to wait.

Caching on top of the batch discount

Anthropic is explicit that the two discounts combine, because prompt caching multipliers stack with other pricing modifiers, including the Batch API discount and data residency. One wrinkle comes with that. Anthropic recommends the 1-hour cache duration with prompt caching for better cache hit rates when batching requests that share context, because batches can take longer than 5 minutes to process, which follows from the default, since the default cache entry is ephemeral with a 5-minute lifetime and the extended 1-hour cache is requested by setting ttl to 1h on the cache_control block. Wiring it up is a request-body change, as caching is enabled by adding a cache_control field, either once at the top level for automatic caching or on individual content blocks for explicit breakpoints.

OpenAI needs no wiring at all. Prompt Caching works automatically for eligible requests, with no code changes required, and caching is available for prompts containing 1024 tokens or more. Lifetime depends on the model generation. Under the in-memory cache policy, cached prefixes generally remain active for 5 to 10 minutes of inactivity, up to a maximum of one hour, while for GPT-5.6 and later a cached prefix remains eligible for reuse for at least 30 minutes. Put the stable part of the prompt first, since the service reads from the longest matching prefix when several breakpoints match cached content. Two more facts worth knowing before you plan around a shared prefix. Caches are not shared between organizations, and only members of the same organization can access caches of identical prompts, and cache writes on GPT-5.6 and later are billed at 1.25x the uncached input token rate.

Log the usage fields from day one, because a cache that quietly stops hitting looks exactly like a price rise. OpenAI reports the read side, where tokens served from cache appear as cached_tokens, inside usage.input_tokens_details for the Responses API and usage.prompt_tokens_details for Chat Completions. Anthropic reports both sides, since the usage object carries cache_creation_input_tokens for tokens written to cache and cache_read_input_tokens for tokens read from it, with input_tokens covering only the tokens after the last cache breakpoint. That split matters for throughput as well as cost, because on most Claude models only uncached input tokens count toward the ITPM rate limit, so input_tokens and cache_creation_input_tokens count while cache_read_input_tokens do not.

How long a prompt has to be before caching applies

Provider and modelsMinimum before caching applies
OpenAI, eligible requests1024 tokens or more
Claude Opus 4.8, Sonnet 5, Sonnet 4.6, Sonnet 4.51,024 tokens, and shorter prompts are simply not cached with no error returned
Gemini 2.5 Flash and 2.5 Pro2048 tokens
Gemini 3.5 Flash and 3.1 Pro Preview4096 tokens

Gemini needs no opt-in for the implicit path, since implicit caching is enabled by default for all Gemini 2.5 and newer models and Google automatically passes on cost savings when a request hits caches. Google's own advice for hit rate matches the OpenAI prefix rule, namely put large and common contents at the beginning of the prompt and send requests with a similar prefix within a short amount of time. Budget for one extra line item there, because Gemini 2.5 Pro context caching adds a cache storage charge billed per million tokens per hour on top of the cached-token rate, and Gemini 2.5 Flash and Flash-Lite carry a storage charge per million tokens per hour as well.

How many reads before a cached prefix pays for itself

Cache writes cost more than a plain input token and reads cost far less, so a prefix earns its keep only if you read it enough times. Anthropic's multipliers relative to the base input rate are 1.25x for a 5-minute cache write, 2x for a 1-hour cache write and 0.1x for a cache read, and the company states the outcome plainly, that caching pays off after just one cache read for the 5-minute duration and after two cache reads for the 1-hour duration. The table recomputes that break-even from the live per-token rates instead of quoting it, and prices your actual prefix. The gpt-5.6-sol row applies the documented 1.25x cache write rate for GPT-5.6 and later to its validated input rate, and leaves the 1-hour column empty because OpenAI's cache lifetime is automatic rather than a tier you buy.

ModelNo caching5 minute cache1 hour cacheReads to break even, 5 minuteReads to break even, 1 hour

The 5 minute column uses each Claude model's validated cache write rate from the feed. The 1 hour column applies the documented 2x multiplier to that model's validated base input rate. OpenRouter also lists a separate 1 hour write price for each Claude model, and it matches that 2x figure exactly. The prefix box opens at the 1,024 token minimum several Claude models share.

The per-token rates behind both calculators

Snapshot 2026-07-27. Every rate is fetched in code from the OpenRouter models API, cross-checked against a second price registry, range checked, and rendered from the validated value. None of it is typed by hand. Units are USD per token because that is what the feeds publish, and the calculators convert to per million.

ModelInputOutputCache readCache write
gpt-4o0.00000250.000010.00000125Not published as a separate rate by the feed
gpt-4o-mini1.5e-76e-77.5e-8Not published as a separate rate by the feed
gpt-5.40.00000250.0000152.5e-7Not published as a separate rate by the feed
gpt-5.4-mini7.5e-70.00000457.5e-8Not published as a separate rate by the feed
gpt-5.4-nano2e-70.000001252e-8Not published as a separate rate by the feed
gpt-5.50.0000050.000035e-7Not published as a separate rate by the feed
gpt-5.6-sol0.0000050.000035e-71.25x the uncached input token rate on GPT-5.6 and later
claude-opus-4.80.0000050.0000255e-70.00000625
claude-sonnet-4.50.0000030.0000153e-70.00000375
claude-haiku-4.50.0000010.0000051e-70.00000125

Four pricing facts sit outside that table and still move a batch budget. OpenAI's pricing page publishes batch rates for gpt-5.4-mini in their own column alongside the standard rates, so you can check the discount against the invoice rather than trusting the guide's headline. Claude Sonnet 5 carries introductory pricing through August 31, 2026, after which standard pricing takes effect, which is a dated cliff worth putting in a calendar. Claude 4.7 and later models use a newer tokenizer that produces approximately 30% more tokens for the same text than the tokenizer used by Sonnet 4.6 and earlier, with the exact increase depending on content and workload, so an unchanged prompt can bill as more tokens after a model upgrade. And for Claude 4.6 and later the full 1M token context window is included at standard pricing, with prompt caching and batch discounts applying at standard rates across the full context window.

Where these numbers come from

Every per-token rate on this page arrives as a validated live field. The build fetches it from the OpenRouter models API, cross-checks it against the community maintained LiteLLM price registry, and holds any value that disagrees across sources or falls outside a sane range for that model. Nothing is typed by hand, and both calculators multiply those rates in your browser, so you can reproduce any figure here from the per-token numbers in the rate table. I'm Michael Lip. I build and operate the KickLLM cost calculators and run 20 plus published Chrome extensions under the Zovo network, several of which call paid LLM APIs in production, which is where the interest in batch discounts and cache multipliers started. No batch run of my own is reported anywhere on this page. Every operational number here is the provider's own, linked beside the sentence that uses it.

Sources