Symptom
A scheduled bulk job (summaries, re-classification, evaluation runs) uses the Azure OpenAI Batch API and one of these happens:
- The batch goes to
faileda few minutes after submission and the output file is empty. - The batch stays in
validatingfor a long time, or fails withtoken_limit_exceeded. - The batch reaches
completed, but many lines in the output have errors or somecustom_ids are missing. - The batch is still
in_progressthe next morning and the downstream job is waiting.
Each of these has a different cause. Start by reading the job object.
Quick checks
1. Read the batch status and its errors
import os
from openai import OpenAI
client = OpenAI(
base_url=f"https://{os.environ['AOAI_RESOURCE']}.openai.azure.com/openai/v1/",
api_key=os.environ["AOAI_KEY"],
)
job = client.batches.retrieve("batch_abc123")
print(job.status, job.request_counts)
print(job.errors) # validation errors when status is failed
print(job.output_file_id, job.error_file_id)
The same thing over REST:
curl -s "https://$AOAI_RESOURCE.openai.azure.com/openai/v1/batches/batch_abc123" \
-H "api-key: $AOAI_KEY" | jq '{status, request_counts, errors}'
2. Know what each status means
| Status | Meaning | What to do |
|---|---|---|
validating | The input file is being checked | Wait, but alert if it stays here much longer than usual |
failed | The input file failed validation | Read errors, fix the file, resubmit |
in_progress | Requests are running | Watch request_counts move |
finalizing | Done, results being written | Wait |
completed | Output and error files are ready | Merge results, check the error file |
expired | Did not finish within the window | Collect partial results, resubmit the rest |
cancelling / cancelled | Cancel requested / done | Partial results may be in the output file |
3. Confirm the deployment type
Batch requests only work against a batch deployment. A Standard or Global Standard deployment of the same model will not accept them.
az cognitiveservices account deployment list \
-g rg-ai-prod -n aoai-prod \
--query "[].{name:name, model:properties.model.name, version:properties.model.version, sku:sku.name}" -o table
Look for a SKU of GlobalBatch or DataZoneBatch (the portal shows "Global Batch" and "Data Zone Batch").
Input file errors (status failed)
Validation errors show up in job.errors.data[].code. The common ones:
| Error code | Cause | Fix |
|---|---|---|
invalid_json_line | A line is not valid JSON (often a stray newline inside a string, or a BOM) | Write each line with json.dumps(), not string concatenation; save as UTF-8 without BOM |
model_not_found | model is the model name, not the batch deployment name | Use the deployment name, e.g. gpt-4.1-mini-batch |
model_mismatch | Lines in one file point at different deployments | One deployment per file |
url_mismatch | Lines use different url values, or the url doesn't match the endpoint | Use the same url on every line, e.g. /v1/chat/completions |
duplicate_custom_id | Two lines share a custom_id | Use your record key and dedupe before writing |
too_many_tasks | More requests than the per-file limit | Split the file |
empty_file | Nothing in the file | Guard against empty extracts upstream |
invalid_request | Line schema is wrong, or the deployment is not a batch SKU | Check the line shape and the deployment SKU |
input_no_permissions / input_modified | Blob input (bring-your-own-storage) not readable, or changed after submit | Fix storage RBAC/network access; don't overwrite input blobs while a job runs |
Validate the file before you upload it. Most of the codes above can be caught locally:
import json, sys
from collections import Counter
ids, models, urls, bad = Counter(), set(), set(), []
with open(sys.argv[1], encoding="utf-8") as f:
for n, line in enumerate(f, 1):
try:
r = json.loads(line)
except json.JSONDecodeError as e:
bad.append((n, str(e))); continue
ids[r.get("custom_id")] += 1
models.add(r["body"].get("model"))
urls.add(r.get("url"))
dupes = [k for k, v in ids.items() if v > 1]
print(f"lines={sum(ids.values())} bad_json={len(bad)} dupes={len(dupes)} models={models} urls={urls}")
if bad or dupes or len(models) != 1 or len(urls) != 1:
sys.exit(1)
Run it as a gate in the pipeline, before client.files.create().
Tip
Submit a 20–50 line file to a new deployment first. A format mistake in a 100,000-line file costs you the whole validation wait before you find out.
token_limit_exceeded and long validating times
Batch quota is measured in enqueued tokens: the tokens in every batch file that has not yet reached a terminal state, for that deployment. A new file is rejected if it would push the total over the limit, even if the file itself is small.
Checks:
- List what is still queued or running on the deployment:
for b in client.batches.list(limit=100):
if b.status in ("validating", "in_progress", "finalizing"):
print(b.id, b.status, b.created_at, b.request_counts)
- Check the deployment's batch quota in the Foundry portal (Quota page) or with the usage list:
az cognitiveservices usage list --location swedencentral -o table
Usage names vary by model and deployment type; look for the batch entry for your model.
Fixes:
- Stagger submissions. Submit the next file only when the previous one reaches
completed, or when the enqueued total leaves room for it. - Retry with backoff when submission fails with
token_limit_exceeded, instead of failing the pipeline:
import time
def create_with_backoff(file_id, retries=8, delay=60):
for _ in range(retries):
job = client.batches.create(input_file_id=file_id,
endpoint="/chat/completions",
completion_window="24h")
while job.status == "validating":
time.sleep(30)
job = client.batches.retrieve(job.id)
codes = [e.code for e in (job.errors.data if job.errors else [])]
if job.status == "failed" and "token_limit_exceeded" in codes:
time.sleep(delay); delay *= 2; continue
return job
raise RuntimeError("enqueued token quota still exhausted")
- Enable dynamic quota on batch deployments where available; Microsoft recommends it to reduce quota failures.
- Request more quota if the steady-state workload needs it. Don't guess numbers; check the current batch quota for your subscription and model.
- Trim requests. Set a sensible
max_tokensper line and remove repeated boilerplate from prompts. Enqueued tokens include prompt tokens, so long prompts fill the queue quickly.
Completed, but results are missing or failed
completed means the job finished, not that every request succeeded.
import json
ok, failed = {}, []
for line in client.files.content(job.output_file_id).text.splitlines():
r = json.loads(line)
if r.get("error") or r["response"]["status_code"] != 200:
failed.append(r["custom_id"]); continue
ok[r["custom_id"]] = r["response"]["body"]["choices"][0]["message"]["content"]
if job.error_file_id:
for line in client.files.content(job.error_file_id).text.splitlines():
failed.append(json.loads(line)["custom_id"])
missing = set(all_input_ids) - set(ok) - set(failed)
print(len(ok), len(failed), len(missing))
| Pattern in failed lines | Likely cause | Fix |
|---|---|---|
| Content filter errors | Prompt or output blocked by the content filter | Review those records; adjust the filter policy if they are legitimate |
context_length_exceeded / 400 | A record is too long for the model | Truncate or chunk those records |
| 5xx on a few lines | Transient service error | Resubmit only those custom_ids |
| Output in a different order | Normal; order is not guaranteed | Always join on custom_id |
Resubmit failed and missing IDs as a new, small batch. Keep writes idempotent (upsert by custom_id) so reruns never duplicate data.
Job runs past 24 hours
completion_window must be 24h, but that is a target. Large jobs at busy times can take longer. Decide what the pipeline does when the deadline passes:
- Downstream jobs should check for
completedinstead of assuming the batch is done by a fixed time. - If the result is needed now, cancel and take what is finished:
curl -s -X POST "https://$AOAI_RESOURCE.openai.azure.com/openai/v1/batches/batch_abc123/cancel" \
-H "api-key: $AOAI_KEY"
The job stays in cancelling for a few minutes. Then the output file holds the partial results. Merge them, and send the remaining IDs to a synchronous fallback or the next night's batch.
Warning
Cancelling throws away the queue position of every unfinished request. Do it only when a late result costs more than a rerun.
Prevent it next time
- Validate every JSONL file in the pipeline (JSON, unique IDs, one model, one URL) before upload.
- Point
modelat the batch deployment name, held in config, not hard-coded per script. - Track enqueued work per deployment and submit files in order, with backoff on
token_limit_exceeded. - Alert on: batch
failed,validatinglonger than your normal baseline, failed-line ratio over a threshold, and nocompletedby the business deadline. - Store each job's ID, input file ID and counts with the pipeline run, so a rerun can pick up only what's missing.
- Keep a synchronous fallback path, rate-limited, for small urgent remainders.