Skip to content
Back to skills

Batch Inference

ASecurity

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 ...

  • 31 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 8, 2026
ai-agentspythongoapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add claude-dev-suite/claude-dev-suite --skill batch-inference --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Batch Inference?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Batch Inference
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/claude-dev-suite-batch-inference/badge)](https://www.skillsdirectory.com/skills/claude-dev-suite-batch-inference)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…