LLM Batch Processing with Anthropic Message Batches API
Process thousands of LLM requests at 50% lower cost using Anthropic's Message Batches API. Complete guide with Python implementation, error handling, polling patterns, and production use cases for DevOps automation.
Real-time Claude API calls cost full price. The Anthropic Message Batches API processes requests asynchronously at 50% reduced cost — ideal for bulk operations like analyzing thousands of logs, generating reports, or processing infrastructure configs overnight.
When to Use Batch vs Real-Time
| Use Case | Real-Time | Batch |
|---|---|---|
| User-facing chatbot | Yes | No |
| PR description on push | Yes | No |
| Nightly log analysis (10,000 entries) | No | Yes |
| Weekly cost report generation | No | Yes |
| Bulk Terraform plan reviews | No | Yes |
| Infrastructure audit (500 configs) | No | Yes |
Batch API has up to 24-hour processing time. If you need results in seconds, use real-time. If you can wait hours and want 50% off, use batch.
Setup
pip install anthropicBasic Batch Request
import anthropic
import json
import time
from pathlib import Path
client = anthropic.Anthropic()
def create_log_analysis_batch(log_entries: list[dict]) -> str:
"""Create a batch request to analyze multiple log entries."""
requests = []
for i, entry in enumerate(log_entries):
requests.append({
"custom_id": f"log-{i}-{entry.get('timestamp', 'unknown')}",
"params": {
"model": "claude-haiku-4-5", # Cheaper model for batch
"max_tokens": 500,
"messages": [{
"role": "user",
"content": f"""Classify this log entry:
Log: {entry.get('message', '')}
Level: {entry.get('level', 'unknown')}
Service: {entry.get('service', 'unknown')}
Timestamp: {entry.get('timestamp', 'unknown')}
Return JSON: {{"category": "error|warning|info|debug", "root_cause": "brief description", "action_required": true|false, "priority": "critical|high|medium|low"}}"""
}]
}
})
batch = client.beta.messages.batches.create(requests=requests)
print(f"Batch created: {batch.id}")
print(f"Requests: {batch.request_counts.processing} processing")
return batch.idPolling for Completion
def wait_for_batch(batch_id: str, poll_interval: int = 60) -> object:
"""Poll until batch completes. Typical wait: 1-24 hours."""
print(f"Waiting for batch {batch_id}...")
while True:
batch = client.beta.messages.batches.retrieve(batch_id)
counts = batch.request_counts
total = counts.processing + counts.succeeded + counts.errored + counts.canceled + counts.expired
done = counts.succeeded + counts.errored + counts.canceled + counts.expired
print(f"Progress: {done}/{total} | Succeeded: {counts.succeeded} | Errors: {counts.errored}")
if batch.processing_status == "ended":
print(f"Batch completed!")
return batch
time.sleep(poll_interval)
def get_batch_results(batch_id: str) -> dict[str, dict]:
"""Retrieve all results from a completed batch."""
results = {}
for result in client.beta.messages.batches.results(batch_id):
custom_id = result.custom_id
if result.result.type == "succeeded":
response_text = result.result.message.content[0].text
try:
# Parse JSON response
parsed = json.loads(response_text.strip())
results[custom_id] = {"status": "success", "data": parsed}
except json.JSONDecodeError:
results[custom_id] = {"status": "success", "data": {"raw": response_text}}
elif result.result.type == "errored":
error = result.result.error
results[custom_id] = {
"status": "error",
"error_type": error.type,
"error": str(error)
}
elif result.result.type == "expired":
results[custom_id] = {"status": "expired"}
return resultsProduction Use Case: Nightly Infrastructure Audit
import boto3
from datetime import datetime
def audit_security_groups_batch():
"""Analyze all AWS Security Groups overnight for policy violations."""
ec2 = boto3.client("ec2", region_name="ap-south-1")
# Get all security groups
sgs = ec2.describe_security_groups()["SecurityGroups"]
print(f"Auditing {len(sgs)} security groups...")
# Create batch request — each SG analyzed by Claude
requests = []
for sg in sgs:
sg_summary = json.dumps({
"GroupId": sg["GroupId"],
"GroupName": sg["GroupName"],
"Description": sg["Description"],
"InboundRules": sg.get("IpPermissions", [])[:10],
"OutboundRules": sg.get("IpPermissionsEgress", [])[:5]
}, indent=2)
requests.append({
"custom_id": sg["GroupId"],
"params": {
"model": "claude-haiku-4-5",
"max_tokens": 800,
"messages": [{
"role": "user",
"content": f"""You are a cloud security auditor. Analyze this AWS Security Group:
{sg_summary}
Identify security issues and return JSON:
{{
"risk_level": "critical|high|medium|low|info",
"issues": [
{{"rule": "description", "risk": "what this enables", "fix": "how to fix"}}
],
"compliant": true|false,
"summary": "one sentence"
}}
Flag: open ports to 0.0.0.0/0 (especially SSH 22, RDP 3389, all traffic), overly permissive rules, and unnecessary exposure."""
}]
}
})
# Submit batch
batch = client.beta.messages.batches.create(requests=requests)
batch_id = batch.id
# Save batch ID for later retrieval (process async)
Path("audit_batch_id.txt").write_text(batch_id)
print(f"Batch submitted: {batch_id}")
print("Results available in ~1-4 hours. Run retrieve_audit_results.py tomorrow.")
return batch_id
def retrieve_audit_results(batch_id: str):
"""Run this the next morning to get results."""
batch = client.beta.messages.batches.retrieve(batch_id)
if batch.processing_status != "ended":
print(f"Still processing: {batch.request_counts.processing} remaining")
return
results = get_batch_results(batch_id)
# Generate report
critical = []
high = []
medium = []
for sg_id, result in results.items():
if result["status"] != "success":
continue
data = result["data"]
risk = data.get("risk_level", "unknown")
if risk == "critical":
critical.append({"sg": sg_id, **data})
elif risk == "high":
high.append({"sg": sg_id, **data})
elif risk == "medium":
medium.append({"sg": sg_id, **data})
# Print report
print(f"\n{'='*60}")
print(f"SECURITY GROUP AUDIT REPORT — {datetime.utcnow().strftime('%Y-%m-%d')}")
print(f"{'='*60}")
print(f"Analyzed: {len(results)} security groups")
print(f"Critical: {len(critical)} | High: {len(high)} | Medium: {len(medium)}")
if critical:
print(f"\n🔴 CRITICAL ISSUES ({len(critical)} security groups):")
for item in critical:
print(f"\n {item['sg']}: {item.get('summary', '')}")
for issue in item.get("issues", [])[:2]:
print(f" - {issue.get('rule', '')}: {issue.get('fix', '')}")
# Save full report
with open("security_audit_report.json", "w") as f:
json.dump({"critical": critical, "high": high, "medium": medium}, f, indent=2)
print(f"\nFull report saved to security_audit_report.json")
# Run overnight
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "retrieve":
batch_id = Path("audit_batch_id.txt").read_text().strip()
retrieve_audit_results(batch_id)
else:
audit_security_groups_batch()Cost Calculation
def estimate_batch_cost(num_requests: int, avg_input_tokens: int = 500, avg_output_tokens: int = 300) -> dict:
"""Estimate batch vs real-time cost."""
# claude-haiku-4-5 pricing (batch is 50% off real-time)
realtime_input_cost_per_mtok = 1.0 # $1/M tokens
realtime_output_cost_per_mtok = 5.0 # $5/M tokens
batch_discount = 0.5
total_input_tokens = num_requests * avg_input_tokens
total_output_tokens = num_requests * avg_output_tokens
realtime_cost = (
(total_input_tokens / 1_000_000 * realtime_input_cost_per_mtok) +
(total_output_tokens / 1_000_000 * realtime_output_cost_per_mtok)
)
batch_cost = realtime_cost * batch_discount
return {
"requests": num_requests,
"realtime_cost_usd": round(realtime_cost, 4),
"batch_cost_usd": round(batch_cost, 4),
"savings_usd": round(realtime_cost - batch_cost, 4),
"savings_pct": "50%"
}
# Example
print(estimate_batch_cost(10000))
# {'requests': 10000, 'realtime_cost_usd': 2.0, 'batch_cost_usd': 1.0, 'savings_usd': 1.0, 'savings_pct': '50%'}Batch Scheduling with Cron
# Run audit submission every night at 11 PM
0 23 * * * cd /opt/infra-tools && python audit.py >> /var/log/audit.log 2>&1
# Run result retrieval every morning at 7 AM
0 7 * * * cd /opt/infra-tools && python audit.py retrieve >> /var/log/audit.log 2>&1Any bulk LLM task that can tolerate a few hours of delay should use the Batch API. 50% cost reduction adds up fast at scale.
More LLMOps cost optimization? Read our LLM token budget and cost control patterns and LLM prompt caching for cost reduction.
Today I Fixed
Short real fixes from production — posted daily
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.
Related Articles
Build an AI AWS Cost Anomaly Detector with Claude API and Cost Explorer
Step-by-step tutorial to build an AI-powered AWS cost anomaly detector using Claude API and AWS Cost Explorer. Automatically identify unusual spending patterns, find the responsible service, and get plain-English explanations with fix recommendations.
Build an AI Cloud Cost Anomaly Detector with Claude API + AWS Cost Explorer
Cloud costs spike without warning. Build a Python bot using AWS Cost Explorer + Claude API that detects anomalies using Z-score analysis and explains the spike in plain English.
Build an AI Cloud Cost Optimization Agent with Claude and LangGraph
Build a multi-step AI agent using Claude API and LangGraph that analyzes your AWS costs, identifies waste, and autonomously applies rightsizing recommendations — cutting cloud bills by 20-40% with minimal human involvement.