Batch RAG for high-volume ingest and bulk query scenarios. Covers OpenAI Batch API (50% discount, 24h SLA), Anthropic Message Batches API, Voyage AI and Cohere batch embeddings, ingestion-time vs query-time batching, async/Ray parallelism, parallel writes to vector DBs, and rate-limit coordination across workers. USE WHEN: user mentions "batch API", "OpenAI batch", "Anthropic batches", "bulk embedding", "Ray embeddings", "parallel ingest", "batch RAG" DO NOT USE FOR: streaming single-query ...
Scanned 9/8/2026
Install to Claude Code
npx -y skills add claude-dev-suite/claude-dev-suite --skill batch-inference --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Batch Inference?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/claude-dev-suite-batch-inference)More formats (shields.io, HTML) on the badges page.
---
name: batch-inference
disable-model-invocation: true
description: |
Batch RAG for high-volume ingest and bulk query scenarios. Covers OpenAI
Batch API (50% discount, 24h SLA), Anthropic Message Batches API, Voyage
AI and Cohere batch embeddings, ingestion-time vs query-time batching,
async/Ray parallelism, parallel writes to vector DBs, and rate-limit
coordination across workers.
USE WHEN: user mentions "batch API", "OpenAI batch", "Anthropic batches",
"bulk embedding", "Ray embeddings", "parallel ingest", "batch RAG"
DO NOT USE FOR: streaming single-query latency - use `rag-production`;
cost-dashboards - use `cost-allocation`;
LLM gateway routing - use `llm-gateway`
allowed-tools: Read, Grep, Glob, Write, Edit
---
# Batch Inference for RAG
Batch APIs cut cost 50% and unlock much higher throughput than the real-time path. Use them wherever a 24-hour SLA is acceptable: initial ingest, re-embed after model swap, bulk metadata extraction, nightly eval runs, large-scale query replay.
## When to Batch vs Stream
| Workload | Batch? |
|---|---|
| Initial corpus embedding (millions of chunks) | Yes |
| Re-embed after model swap | Yes |
| Nightly eval run on golden set | Yes |
| Extracting entities/summaries/keywords during ingest | Yes |
| User-facing chat query | No |
| Real-time retrieval embedding (single query) | No |
| Sub-100 ms reranker call | No |
## OpenAI Batch API (50% Discount, 24h SLA)
Works for `/v1/chat/completions`, `/v1/embeddings`, `/v1/completions`. Input is a JSONL file where each line is a self-contained request.
```python
from openai import OpenAI
import json, pathlib
client = OpenAI()
# 1. Prepare JSONL
requests = [
{
"custom_id": f"chunk-{i}",
"method": "POST",
"url": "/v1/embeddings",
"body": {"model": "text-embedding-3-small", "input": chunk, "dimensions": 512},
}
for i, chunk in enumerate(chunks)
]
pathlib.Path("batch.jsonl").write_text("\n".join(json.dumps(r) for r in requests))
# 2. Upload file
fobj = client.files.create(file=open("batch.jsonl", "rb"), purpose="batch")
# 3. Create batch job
batch = client.batches.create(
input_file_id=fobj.id,
endpoint="/v1/embeddings",
completion_window="24h",
metadata={"project": "kb_v2_embed"},
)
print(batch.id, batch.status)
```
Poll:
```python
while batch.status in {"validating", "in_progress", "finalizing"}:
time.sleep(60)
batch = client.batches.retrieve(batch.id)
print(batch.status, batch.request_counts)
```
Download + parse:
```python
out = client.files.content(batch.output_file_id).text
for line in out.splitlines():
row = json.loads(line)
vec = row["response"]["body"]["data"][0]["embedding"]
upsert(row["custom_id"], vec)
```
**Limits**: max 50k requests per batch, max 200 MB per file, max 50 queued batches per account. Split big jobs into chunks and upload in parallel.
## Anthropic Message Batches API (50% Discount, 24h SLA)
```python
import anthropic
client = anthropic.Anthropic()
requests = [
{
"custom_id": f"summarize-{i}",
"params": {
"model": "claude-sonnet-4-5",
"max_tokens": 512,
"messages": [{"role": "user", "content": f"Summarize:\n{doc}"}],
},
}
for i, doc in enumerate(docs)
]
batch = client.messages.batches.create(requests=requests)
print(batch.id, batch.processing_status)
# Poll
while batch.processing_status in {"in_progress"}:
time.sleep(60)
batch = client.messages.batches.retrieve(batch.id)
for result in client.messages.batches.results(batch.id):
if result.result.type == "succeeded":
text = result.result.message.content[0].text
store(result.custom_id, text)
```
Limits: 100k requests per batch, 256 MB file size. Works with prompt caching — cache prefixes still apply for 50% stacking with batch's 50%, effectively 75% off the prefix tokens.
## Voyage AI Batch Embeddings
Voyage supports batched calls in real-time (no separate batch endpoint), but the per-call batch size is high (128 inputs):
```python
import voyageai
vo = voyageai.Client()
def embed_batch(texts, model="voyage-3"):
# 128-input batches optimal on API
out = []
for i in range(0, len(texts), 128):
r = vo.embed(texts[i:i+128], model=model, input_type="document")
out.extend(r.embeddings)
return out
```
## Cohere Embed Jobs (Async Bulk)
```python
import cohere
co = cohere.ClientV2()
job = co.embed_jobs.create(
dataset_id="my-dataset-id",
model="embed-v4.0",
input_type="search_document",
)
# Poll status, then download dataset with vectors attached
```
Good for datasets already in Cohere's dataset store; less useful if you're not in that ecosystem.
## Query-Time vs Ingest-Time
- **Ingest-time batch**: huge wins. You have all inputs up front; 50% discount + parallelism; eventual consistency fine.
- **Query-time batch**: only viable if you micro-batch multiple near-simultaneous user queries (50–200 ms window). Usually not worth the complexity for chat UX.
## Parallel Embedding with asyncio
Real-time path where you want pipeline parallelism without hitting a batch API:
```python
import asyncio
from openai import AsyncOpenAI
client = AsyncOpenAI()
sem = asyncio.Semaphore(16) # cap concurrent API calls
async def embed_one(batch):
async with sem:
r = await client.embeddings.create(model="text-embedding-3-small", input=batch)
return [d.embedding for d in r.data]
async def embed_all(chunks, batch_size=64):
tasks = [embed_one(chunks[i:i+batch_size]) for i in range(0, len(chunks), batch_size)]
return [v for sub in await asyncio.gather(*tasks) for v in sub]
vecs = asyncio.run(embed_all(all_chunks))
```
## Ray / Dask for CPU- or Local-Model Embeddings
```python
import ray
ray.init()
@ray.remote(num_gpus=0.25)
class Embedder:
def __init__(self):
from sentence_transformers import SentenceTransformer
self.m = SentenceTransformer("BAAI/bge-large-en-v1.5").half().cuda()
def embed(self, batch):
return self.m.encode(batch, batch_size=64, normalize_embeddings=True).tolist()
workers = [Embedder.remote() for _ in range(4)]
futures = [workers[i % 4].embed.remote(batch) for i, batch in enumerate(batches)]
all_vecs = [v for sub in ray.get(futures) for v in sub]
```
Ray is great for: hundreds of millions of chunks, self-hosted embedders, spot instance clusters. Each worker can shard by `hash(doc_id) % N`.
## Parallel Writes to Vector DBs
Vector DBs all support batch upsert — always use it:
```python
# Pinecone
index.upsert(vectors=[(id, vec, meta) for id, vec, meta in batch], batch_size=100)
# Qdrant
from qdrant_client.models import PointStruct
client.upsert(collection_name="kb",
points=[PointStruct(id=i, vector=v, payload=m) for i, v, m in batch])
# Weaviate v4
with client.batch.dynamic() as b:
for i, v, m in batch:
b.add_object(collection="Chunk", properties=m, uuid=i, vector=v)
```
Throttle concurrent writers to avoid storage-engine contention; usually 4–8 parallel writer tasks saturate most managed services.
## Rate-Limit Coordination Across Workers
Distributed embedding hitting one API key needs a shared rate-limit budget. Options:
```python
# Redis-based token bucket
import redis, time
r = redis.Redis()
def take(tokens, key="openai_tpm", capacity=1_000_000, refill_per_sec=1_000_000/60):
now = time.time()
with r.pipeline() as p:
p.hmget(key, "tokens", "ts"); p.execute()
# ... standard token-bucket refill math, HSET back atomically
```
Simpler: give each worker a slice of the limit. With 1M TPM and 10 workers, cap each at 100k TPM and run independently.
## Error Handling and DLQ
- Validate JSONL before upload (OpenAI rejects the whole file on a single bad line — parse with `json.loads` in a loop first).
- Retry the entire batch on transient failure; retry per-request on `response.error` in the output.
- Persist a DLQ for requests that still fail after retries; rerun after fixing the input.
```python
for line in out.splitlines():
row = json.loads(line)
if row.get("error"):
dlq.append(row)
else:
process(row)
```
## Cost Math
Example: 20M chunks × 500 tokens × `text-embedding-3-small` at $0.02/M tokens.
- Real-time: 10B tokens × $0.02/M = **$200**, plus engineer time on rate-limit headaches.
- Batch: **$100**, queued and done in 24h.
For a re-embed with `-large` at $0.13/M, batch savings are **$650** on the same corpus.
## Anti-Patterns
| Anti-Pattern | Fix |
|---|---|
| Initial corpus embed via real-time API | Use OpenAI/Anthropic Batch for 50% off |
| No `custom_id` — can't map results back | Always set a stable `custom_id` |
| Single JSONL of 10M requests | Split into ≤50k-request files; parallel upload |
| Per-request vector DB upserts | Batch 100–500 per upsert call |
| Uncoordinated rate limits across workers | Redis token bucket or pre-sliced per-worker caps |
| Retrying whole batch on one failure | Retry only `error` rows from output |
| Forgetting cache invalidation after re-embed | Bump index version + flush semantic cache |
## Production Checklist
- [ ] Batch API used for ingest-time embedding and extraction
- [ ] `custom_id` ties every response back to a chunk row
- [ ] Batch files split by 50k-request cap
- [ ] Parallel upload, parallel result download
- [ ] Retry + DLQ for persistent errors
- [ ] Idempotent upserts keyed by `doc_id + chunk_idx`
- [ ] Shared rate-limit budget across workers (Redis bucket or slicing)
- [ ] Cost checkpoint before triggering multi-million-row batches
- [ ] Manifest records which batch job produced which index version
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!