{"slug": "cut-your-claude-api-bill-by-90-with-prompt-caching-and-batches", "title": "Cut Your Claude API Bill by 90% with Prompt Caching and Batches", "summary": "Anthropic's Claude API users can cut their bills by up to 90% by combining prompt caching and the Message Batches API, according to a tutorial by Rachel Goldstein. Prompt caching bills repeated context at 10% of the normal input rate, and the Batches API takes 50% off non-urgent requests, so a cached token inside a batch costs 5% of the list price. The tutorial uses the claude-opus-5 model, priced at $5 per million input tokens and $25 per million output tokens, and provides Python code to measure token costs and implement caching.", "body_md": "# Cut Your Claude API Bill by 90% with Prompt Caching and Batches\n\nMeasure real token costs, then stack prompt caching and the Batches API to slash them.\n\n[Rachel Goldstein](https://sourcefeed.dev/u/rachel_goldstein)\n\n## What you'll build\n\nYou'll take a working [Claude API](https://platform.claude.com/docs/en/) integration, measure exactly what its input tokens cost, then cut that bill twice: [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) bills repeated context at 10% of the normal input rate, and the [Message Batches API](https://platform.claude.com/docs/en/build-with-claude/batch-processing) takes 50% off everything that doesn't need an immediate response. Stacked, a cached token inside a batch costs 5% of the list price.\n\n## Prerequisites\n\n- Python 3.9+ (tested on 3.13)\n0.120.2 (current as of August 2026)`anthropic`\n\nPython SDK- An API key from the\n[Claude Console](https://platform.claude.com/) - Model:\n`claude-opus-5`\n\n— $5 per million input tokens, $25 per million output tokens. Cache pricing verified against the official docs: writes cost 1.25× base input (2× for the 1-hour TTL), reads cost 0.1×.\n\n```\nmkdir claude-costs && cd claude-costs\npython3 -m venv .venv && source .venv/bin/activate\npip install anthropic==0.120.2\nexport ANTHROPIC_API_KEY=\"sk-ant-...\"\n```\n\n## Step 1: Capture a cost baseline\n\nMost real integrations re-send a big static block on every request — product docs, a style guide, few-shot examples. That's the money leak. Save this as `baseline.py`\n\n:\n\n``` python\nimport anthropic\n\nclient = anthropic.Anthropic()\n\n# Stand-in for the static context your app re-sends on every request\nDOC_LINE = (\n    \"Acme Widget API reference, section {}: the flux endpoint accepts JSON \"\n    \"payloads with a `power` field measured in gigawatts and returns a signed \"\n    \"delivery manifest within 30 seconds.\\n\"\n)\nSYSTEM = \"Answer questions using only the reference below.\\n\\n\" + \"\".join(\n    DOC_LINE.format(i) for i in range(200)\n)\n\nresp = client.messages.create(\n    model=\"claude-opus-5\",\n    max_tokens=1024,\n    system=SYSTEM,\n    messages=[{\"role\": \"user\", \"content\": \"What unit is the `power` field measured in?\"}],\n)\nu = resp.usage\nprint(f\"input={u.input_tokens}  cache_write={u.cache_creation_input_tokens}  cache_read={u.cache_read_input_tokens}\")\n```\n\nRun `python baseline.py`\n\n. The system prompt is roughly 8,000 tokens, and `input_tokens`\n\nconfirms you're paying full price for all of them — about $0.04 per request, $40 per 1,000 requests, before output tokens.\n\n## Step 2: Add a cache breakpoint\n\nCaching is a prefix match: the API renders `tools`\n\n→ `system`\n\n→ `messages`\n\nand caches everything up to a `cache_control`\n\nmarker. Any byte change earlier in the prefix invalidates it, so keep stable content first and volatile content (the user's question) after the breakpoint. Save as `cached.py`\n\n:\n\n``` python\nimport anthropic\n\nclient = anthropic.Anthropic()\n\nDOC_LINE = (\n    \"Acme Widget API reference, section {}: the flux endpoint accepts JSON \"\n    \"payloads with a `power` field measured in gigawatts and returns a signed \"\n    \"delivery manifest within 30 seconds.\\n\"\n)\nSYSTEM = \"Answer questions using only the reference below.\\n\\n\" + \"\".join(\n    DOC_LINE.format(i) for i in range(200)\n)\n\ndef ask(question: str) -> None:\n    resp = client.messages.create(\n        model=\"claude-opus-5\",\n        max_tokens=1024,\n        system=[\n            {\n                \"type\": \"text\",\n                \"text\": SYSTEM,\n                \"cache_control\": {\"type\": \"ephemeral\"},  # 5-minute TTL\n            }\n        ],\n        messages=[{\"role\": \"user\", \"content\": question}],\n    )\n    u = resp.usage\n    print(f\"input={u.input_tokens}  cache_write={u.cache_creation_input_tokens}  cache_read={u.cache_read_input_tokens}\")\n\nask(\"What unit is the `power` field measured in?\")\nask(\"How fast is the delivery manifest returned?\")\n```\n\nThe first call writes the cache at 1.25× input price; every identical-prefix call within the 5-minute TTL (each read refreshes it) bills those tokens at 0.1× — the 90% cut. You get up to 4 breakpoints per request, and on `claude-opus-5`\n\na prefix must be at least 512 tokens to cache at all.\n\n## Step 3: Batch the non-urgent work\n\nEvaluations, backfills, and nightly summarization don't need answers in seconds. The Batches API takes any list of Messages requests (up to 100,000 requests or 256 MB per batch), processes them asynchronously — most batches finish within an hour — and bills all usage at 50% of standard prices. Caching works inside batches too; use the 1-hour TTL since processing usually outlives the 5-minute one. Save as `batch.py`\n\n:\n\n``` python\nimport time\n\nimport anthropic\nfrom anthropic.types.message_create_params import MessageCreateParamsNonStreaming\nfrom anthropic.types.messages.batch_create_params import Request\n\nclient = anthropic.Anthropic()\n\nDOC_LINE = (\n    \"Acme Widget API reference, section {}: the flux endpoint accepts JSON \"\n    \"payloads with a `power` field measured in gigawatts and returns a signed \"\n    \"delivery manifest within 30 seconds.\\n\"\n)\nSYSTEM = \"Answer questions using only the reference below.\\n\\n\" + \"\".join(\n    DOC_LINE.format(i) for i in range(200)\n)\n\nshared_system = [\n    {\n        \"type\": \"text\",\n        \"text\": SYSTEM,\n        \"cache_control\": {\"type\": \"ephemeral\", \"ttl\": \"1h\"},\n    }\n]\n\nquestions = [\n    \"What unit is the `power` field measured in?\",\n    \"What does the flux endpoint return?\",\n    \"Summarize the reference in one sentence.\",\n]\n\nbatch = client.messages.batches.create(\n    requests=[\n        Request(\n            custom_id=f\"q-{i}\",\n            params=MessageCreateParamsNonStreaming(\n                model=\"claude-opus-5\",\n                max_tokens=1024,\n                system=shared_system,\n                messages=[{\"role\": \"user\", \"content\": q}],\n            ),\n        )\n        for i, q in enumerate(questions)\n    ]\n)\nprint(f\"Created {batch.id}, status: {batch.processing_status}\")\n\nwhile batch.processing_status != \"ended\":\n    time.sleep(30)\n    batch = client.messages.batches.retrieve(batch.id)\n    print(f\"status: {batch.processing_status}\")\n\nfor result in client.messages.batches.results(batch.id):\n    if result.result.type == \"succeeded\":\n        text = next(b.text for b in result.result.message.content if b.type == \"text\")\n        print(f\"{result.custom_id}: {text}\")\n    else:\n        print(f\"{result.custom_id}: {result.result.type}\")\n```\n\nResults arrive in any order, so always key on `custom_id`\n\n, never on position. Results stay downloadable for 29 days.\n\n## Verify it works\n\n`python cached.py`\n\nshould print (token counts vary by a few):\n\n```\ninput=15  cache_write=8021  cache_read=0\ninput=14  cache_write=0     cache_read=8021\n```\n\nThe second line is the win: 8,021 tokens billed at $0.50/MTok instead of $5. `python batch.py`\n\nshould end with:\n\n```\nCreated msgbatch_013Zva2CMHLNnXjNJJKqJ2EF, status: in_progress\nstatus: ended\nq-0: The `power` field is measured in gigawatts.\nq-1: The flux endpoint returns a signed delivery manifest.\nq-2: ...\n```\n\nAt 1,000 requests against this 8,000-token prompt, input costs drop from ~$40 uncached to ~$4 with caching, and ~$2 cached inside a batch — with output tokens also half price in the batch.\n\n## Troubleshooting\n\n** cache_read_input_tokens is 0 on every request.** Caching fails silently, never with an error. Two usual causes: the prefix is under the 512-token minimum for\n\n`claude-opus-5`\n\n(other models require 1,024–4,096), or a byte differs between requests — a timestamp interpolated into the system prompt, an unsorted `json.dumps`\n\n, a tool list that changes order. Diff the rendered prompts of two consecutive requests.** anthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'invalid x-api-key'}}** — the key isn't reaching the client. Re-run\n\n`export ANTHROPIC_API_KEY=...`\n\nin the same shell as the script, and check for a stale key in `.env`\n\nor your shell profile overriding it.** AttributeError: 'MessageBatchErroredResult' object has no attribute 'message'** — you read\n\n`result.result.message`\n\nwithout checking the result type. Only `succeeded`\n\nresults carry `.message`\n\n; branch on `result.result.type`\n\nfirst (`errored`\n\nresults carry `.error`\n\ninstead).**Batch results come back expired.** The batch hit the 24-hour processing ceiling before those requests ran — common under heavy demand or when a batch trips rate limits. Expired requests aren't billed; collect their\n\n`custom_id`\n\ns and resubmit just those in a new batch.## Next steps\n\nPoint `cached.py`\n\nat your real system prompt and tool definitions — a breakpoint on the last system block caches tools and system together. Audit for silent cache invalidators (timestamps, UUIDs, non-deterministic serialization) before trusting your hit rate. Then look at the [token counting endpoint](https://platform.claude.com/docs/en/build-with-claude/token-counting) to estimate costs pre-flight, and the [rate limits page](https://platform.claude.com/docs/en/api/rate-limits) — batches have their own limits separate from the Messages API.\n\n## Sources & further reading\n\n-\n[Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)— platform.claude.com -\n[Batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing)— platform.claude.com -\n[Pricing](https://platform.claude.com/docs/en/pricing)— platform.claude.com -\n[Anthropic Python SDK](https://github.com/anthropics/anthropic-sdk-python)— github.com\n\n[Rachel Goldstein](https://sourcefeed.dev/u/rachel_goldstein)· Dev Tools Editor\n\nRachel has been embedded in the developer tooling ecosystem for nearly eight years, covering everything from IDE wars and package-manager drama to the quiet rise of AI-assisted coding. She has a soft spot for open-source maintainers and an unhealthy number of terminal emulators installed on a single laptop.\n\n## Discussion 0\n\nNo comments yet\n\nBe the first to weigh in.", "url": "https://wpnews.pro/news/cut-your-claude-api-bill-by-90-with-prompt-caching-and-batches", "canonical_source": "https://sourcefeed.dev/a/cut-your-claude-api-bill-by-90-with-prompt-caching-and-batches", "published_at": "2026-08-01 11:40:16+00:00", "updated_at": "2026-08-01 11:55:32.332099+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-products", "developer-tools"], "entities": ["Anthropic", "Claude API", "Rachel Goldstein", "claude-opus-5", "Message Batches API", "Python"], "alternates": {"html": "https://wpnews.pro/news/cut-your-claude-api-bill-by-90-with-prompt-caching-and-batches", "markdown": "https://wpnews.pro/news/cut-your-claude-api-bill-by-90-with-prompt-caching-and-batches.md", "text": "https://wpnews.pro/news/cut-your-claude-api-bill-by-90-with-prompt-caching-and-batches.txt", "jsonld": "https://wpnews.pro/news/cut-your-claude-api-bill-by-90-with-prompt-caching-and-batches.jsonld"}}