← Machinery Machinery · 06

If nobody is watching
a spinner,
you are overpaying.

Providers sell the same models at roughly half their synchronous price to anyone willing to wait — this page is what a queue must guarantee before that discount is worth taking, and where it does not apply at all.

The trade

Half the price, for work that can wait.

Most of what a production system sends to a model is not urgent. Nightly enrichment, backfills, evaluation sweeps, summarising a corpus, re-scoring records that changed yesterday — the output is read by a person the next morning, or by another program on a schedule. All of it is billed at the price of work somebody is actively waiting for.

Providers offer an alternative: submit the same requests as a file, accept a turnaround measured in hours instead of seconds, and pay roughly half. The price and the window are the provider's, not ours. What stops most teams taking the trade is not the economics; it is the second integration, the second place credentials live, and a job runner you now have to operate and monitor.

 SynchronousBatch
PriceThe provider's standard rateRoughly half — the provider's batch rate
TurnaroundSecondsUp to 24 hours; the provider's window, not a target
IntegrationAn OpenAI-compatible clientThe same client, a different call
CredentialsYours, held onceYours, the same ones
The job runnerNot neededOurs — submission, polling, retries, result storage

The batch surface is the OpenAI-compatible one you already know: upload a file of requests, create a batch, read its status, download the output. Point the official client at the gateway and those objects behave as the SDK documents them.

# Same client, same key — one call changes
from openai import OpenAI
client = OpenAI(base_url="https://api.ai-ctrl.net/v1", api_key="$AI_CTRL_API_KEY")

f = client.files.create(file=open("requests.jsonl", "rb"), purpose="batch")
b = client.batches.create(input_file_id=f.id,
                          endpoint="/v1/chat/completions",
                          completion_window="24h")

Batch pricing applies where the provider publishes it, on the models it publishes it for. Coverage widens as providers are added. more providers soon

A rough test Ask who reads the answer. A dashboard tomorrow morning, a nightly index, an evaluation report, another program on a timer — all deferrable, all paying an urgency premium for nothing. A person holding a browser tab open is not.
Money safety

The failure mode is paying twice.

A batch is the largest single spend an inference API can commit in one call, and the easiest to duplicate. The component that submits it runs as more than one copy, and every copy sees the same queued work. Send the same file twice and the provider does exactly what it was asked: runs it twice, charges twice, returns two identical result sets. Nothing errors. You find out on the invoice.

So taking a job and marking it taken are one indivisible step, not two. One copy wins the claim and holds it for the length of the submission; any other copy reaching for that job finds it held and moves on. The claim is settled before the money is spent, never after — a marker written once the request is in flight says nothing about the request that left a moment earlier.

SENDER A SAME QUEUE SENDER B SAME QUEUE SAME JOB THE CLAIM TAKE THE JOB AND MARK IT — ONE STEP SENT UPSTREAM ONCE THE FIRST CLAIM WINS · ONE CHARGE SECOND SEND REFUSED ANY LATER CLAIM · NO SECOND CHARGE
Duplicate submission is not an error state — the provider accepts it and bills for it. The only defence is that claiming the job and recording the claim cannot come apart.

It is a held claim rather than a plain flag because a sender can die mid-submission, and both naive versions fail in opposite directions. A flag never released strands the batch forever — unsent, unbilled, undone. A flag released the moment a sender looks unhealthy sends the batch again while the first request is still in flight, which is exactly the charge you were avoiding.

The cadence of asking

How often to ask a question whose answer is hours away.

Once submitted, the batch belongs to the provider. Our side does one thing: ask whether it has finished, at a rate matched to how likely the answer is to have changed. The first check comes half a minute after submission, then a minute, five, fifteen, and from there once an hour until it ends.

30 S 1 MIN 5 MIN 15 MIN 1 HOUR SENT CHECK 1 CHECK 5 HOURLY FROM HERE
Spacing is illustrative, not to scale. The early checks catch the batches that finish fast; the widening keeps a long one from being asked the same question two thousand times.

Small batches often come back inside the first few checks, which is why those are close together. A large one will not, and asking every thirty seconds for a day spends request quota to learn nothing. Neither number says when your work finishes. They say when we look.

Why it is not a message queue Work that may take a day cannot be held open on a connection expecting an answer in seconds, so nothing here depends on one. Submission and polling run from durable state: restart the service mid-batch and the next tick resumes where it was, because the record is the state, not a variable inside a running process.
Endings

Every batch ends somewhere you can read.

Three terminal outcomes, and the difference between them is operational rather than cosmetic. Completed — results fetched, stored, yours to download. Failed — the work did not come back, and the provider's own reason travels with the outcome instead of being flattened into a generic message. That includes the case where the provider's window elapsed with the work unfinished: it surfaces as a failure carrying the expiry reason, so the cause is still legible even though the status is shared. Cancelled — you asked us to stop; the cancellation is picked up on the next check and passed upstream, so a batch you no longer want stops costing you as soon as the provider allows, rather than running to completion because nothing was listening.

IN FLIGHT WITH PROVIDER COMPLETED RESULTS FETCHED AND STORED FAILED REJECTED OR ERRORED UPSTREAM CANCELLED YOU ASKED US TO STOP REPORTED EVERY ENDING, WITH ITS REASON PUSHED — NOT POLLED FOR
There is no fourth outcome where the batch simply stops being mentioned. Reaching any of the three is an event you receive, not a state you have to discover.

An elapsed window and a rejected request share a status but not a remedy. One says the work did not fit the time available — split the batch, or accept that this workload wants the synchronous path. The other says the request itself was wrong. The reason field is what tells them apart, which is why it is carried through rather than summarised: retrying on the status alone is how a team re-submits something that will run out of time again for exactly the same reason.

You should not have to ask Reaching a terminal state pushes an event onto the stream you already subscribe to, carrying the batch and its outcome. Reading the status directly still works and always will — it is the backstop, not the intended path.
Where it does not apply

Batch is a scheduling decision, not a discount code.

The saving is real and so is the constraint, and the constraint is the part most descriptions leave out. Run a workload through these four questions in order. The first one that stops you, stops you.

  1. Refuse — someone is waiting for the answer Interactive requests, anything rendering into a UI, anything with a person on the other end. Batch pricing buys patience. If you have none to sell, the synchronous path is the correct answer rather than a compromise.
  2. Refuse — there is a hard deadline inside the window Up to 24 hours means up to. Short batches frequently return far sooner, but that is an observation about provider behaviour, not a commitment anyone can make on the provider's behalf. If missing the window has a consequence, do not schedule against the optimistic case.
  3. Refuse — the model has no batch tier Not every model at every provider is offered at batch pricing. Where it is not, the submission fails with the provider's own reason rather than quietly re-running the work at the standard rate. A silent fallback to full price is a bill you never agreed to.
  4. Act — nobody is watching The output is read tomorrow, or by another program on a timer. Submit it as a batch and let the queue own it until it ends — which it will, in one of four states, whichever way it goes.
The useful question is not can this be cheaper but is anyone waiting for this. For most production systems a larger share of the answer is “no” than the architecture diagram suggests — and that share is billed today as though somebody were watching it arrive.

If your traffic already goes through an OpenAI-compatible client, the deferrable half of it is one call away from a different price. We are onboarding early users now.

Run cheap GPUs without the bill shock.

Request early access →