23 views
A coding agent sends the same large block of text on every turn: tool definitions, a system prompt with coding rules, maybe a style guide. The model processes all of it every time, and you pay for it every time. Meanwhile your nightly eval suite calls the model thousands of times in a loop, at interactive prices, for results nobody reads until morning.
Amazon Bedrock has a feature for each problem. Prompt caching lets repeated prompt prefixes be read from a cache, reducing input token cost and latency for interactive calls. Batch inference runs large sets of prompts asynchronously from files in S3. This guide explains both, shows the code, and covers the caching pitfall teams worry about most: agents following stale coding rules after a big repository change. It merges our earlier posts on caching economics and caching pitfalls into one guide.
TL;DR
- Put stable content first (tools, then system rules) and add a
cachePointafter it. Dynamic content (user turns, retrieved chunks) goes after the checkpoint.- Any change to the prefix is a cache miss, and changing
toolsinvalidates thesystemandmessagescaches after it. Bedrock can’t serve an old prompt to a new request.- The default TTL is 5 minutes and resets on each hit; some models support
"ttl": "1h". Claude models allow up to 4 checkpoints and need a minimum prefix size.- Read
cacheReadInputTokensandcacheWriteInputTokensin the response.inputTokenscounts only uncached tokens.- Batch inference takes JSONL from S3 (InvokeModel or Converse format) and writes results back to S3. It doesn’t support prompt caching or tool calling.
How prompt caching works
Bedrock offers two kinds of prompt caching, and support varies by model and API:- Implicit caching: Bedrock and the model automatically try to reuse eligible prefixes. No request changes are needed, but it’s best effort.
- Explicit caching: you mark where the reusable prefix ends with cache checkpoints (
cachePointin Converse,cache_controlin Anthropic’s InvokeModel format).
- Exact prefix match. Prefixes must stay identical between requests; any change causes a miss.
- Order is tools, then system, then messages. The sections are chained, so changing tools invalidates everything after them.
- Minimum size. Each checkpoint needs a minimum number of tokens in the prefix before it (for Claude models in the current table, between 512 and 4,096 depending on the model). Below the minimum, the request still succeeds but nothing is cached.
- TTL. Many models use a 5-minute TTL that resets with every hit; some support a 1-hour TTL. Check the model card.
- No guarantee. Even with checkpoints, a hit isn’t guaranteed. Check the usage fields to see what happened.
- Billing. Cache reads are billed at the model’s cache-read rate. Cache writes can cost more than normal input, depending on the model. Output tokens are never discounted.
Step 1: Add cache checkpoints to agent calls
For a coding agent, the stable prefix is usually the tool definitions and the system prompt with your coding rules. Put a checkpoint after each, so a rules edit doesn’t invalidate the tools cache, and keep the conversation after them.python
Things to get right:
import json
import os
import boto3
brt = boto3.client("bedrock-runtime")
MODEL_ID = os.environ["AGENT_MODEL_ID"] # a model or inference profile that supports caching
TOOLS = [
{
"toolSpec": {
"name": "read_file",
"description": "Read a file from the checked-out repository.",
"inputSchema": {"json": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
}},
}
},
# ... more tool specs ...
{"cachePoint": {"type": "default"}}, # checkpoint 1: after the tools
]
def build_system(rules_text: str) -> list[dict]:
return [
{"text": rules_text}, # long, stable coding rules
{"cachePoint": {"type": "default"}}, # checkpoint 2: after the rules
]
def agent_turn(rules_text: str, history: list[dict], user_text: str) -> dict:
resp = brt.converse(
modelId=MODEL_ID,
system=build_system(rules_text),
toolConfig={"tools": TOOLS},
messages=history + [{"role": "user", "content": [{"text": user_text}]}],
inferenceConfig={"maxTokens": 2048},
)
u = resp["usage"]
read = u.get("cacheReadInputTokens", 0)
write = u.get("cacheWriteInputTokens", 0)
total_input = u["inputTokens"] + read + write # inputTokens excludes cached tokens
print(json.dumps({
"metric": "bedrock_usage",
"input_total": total_input,
"cache_read": read,
"cache_write": write,
"cache_hit_ratio": round(read / total_input, 3) if total_input else 0,
"output": u["outputTokens"],
}))
return resp- Nothing that changes per request (timestamps, user names, request IDs, retrieved chunks) may appear before a checkpoint. One changing character in the rules section turns every call into a cache write.
- If your total prefix before a checkpoint is below the model’s minimum, it isn’t cached. Small system prompts may not benefit at all.
- For long sessions with pauses longer than 5 minutes, a model that supports
"ttl": "1h"in thecachePointkeeps the cache alive. Don’t send thettlfield to models that only support 5 minutes; that can cause aValidationException. - Cross-Region inference works with caching, but at times of high demand it may lead to more cache writes.
Step 2: Measure whether caching pays off
Whether caching saves money depends on how often the same prefix is reused within the TTL. The arithmetic per call is simple:- Without caching, a prefix of P tokens is billed P times the input rate on every call.
- With caching, the first call in a TTL window pays P times the cache-write rate, and each later hit pays P times the cache-read rate.
cache_hit_ratio metric from Step 1 and the current rates on the Amazon Bedrock pricing page for your model and Region instead of assuming a discount. Latency also improves on hits because the prefix isn’t reprocessed; measure time to first token in your own traces.
Step 3: Avoid stale coding rules
A common worry: after a big repository change (a renamed package, a banned library, a new lint rule), will agents keep following the old rules because of the cache? Not because of Bedrock’s cache. Prefix caching requires an exact match, so new rules text is simply a cache miss followed by a new write. When agents do follow old rules, the cause is almost always in your own system:- The agent runtime loads rules from an old checkout or an old prompt version pin.
- An application-level cache (for example, a cache of answers or of assembled prompts) is keyed only by the question, not by the rules version.
- Old few-shot examples that use the removed API are still in the prompt.
python
If you manage the system prompt with Bedrock Prompt Management, remember it versions prompts but has no aliases; your application pins a version number, so a rules change means publishing a new version and updating that pin. Untrusted text such as issue bodies and PR descriptions changes per request anyway, so it belongs after the checkpoints, clearly delimited as data.
import hashlib
import pathlib
import re
def rules_version(rules_dir: str) -> str:
"""Content hash of every rules file, so any edit produces a new version."""
h = hashlib.sha256()
for path in sorted(pathlib.Path(rules_dir).rglob("*.md")):
h.update(str(path).encode())
h.update(path.read_bytes())
return h.hexdigest()[:16]
def app_cache_key(tenant: str, question: str, rules_ver: str, tools_ver: str) -> str:
# Anything that changes behavior belongs in the key, and so does the tenant.
return hashlib.sha256(f"{tenant}|{rules_ver}|{tools_ver}|{question}".encode()).hexdigest()
CANARIES = [
{"prompt": "Add a function that loads a user record.",
"must_not_match": r"LegacyUserRepo", "reason": "LegacyUserRepo removed in ADR-214"},
]
def run_canaries(ask) -> None:
"""Run after every rules change; fail the deploy if a removed pattern comes back."""
for c in CANARIES:
answer = ask(c["prompt"])
if re.search(c["must_not_match"], answer):
raise AssertionError(f"stale rule detected: {c['reason']}")Step 4: Move offline work to batch inference
Evals, backfills and bulk classification don’t need answers in seconds. With batch inference you upload JSONL files to S3, submit one job, and collect results from S3 later. Each line has arecordId and a modelInput. With the Converse invocation type, modelInput uses the Converse request format, so the same request shape works across models.
python
Batch input must meet the service quotas for minimum records per job, records per file and file size; check the current values in Service Quotas before you build the pipeline. The job’s service role needs read access to the input, write access to the output, and the caller needs import json
import os
import boto3
s3 = boto3.client("s3")
bedrock = boto3.client("bedrock")
BUCKET = "agent-batch-jobs"
RULES = open("rules/system.md").read()
def submit_eval_batch(run_id: str, cases: list[dict]) -> str:
lines = []
for case in cases:
lines.append(json.dumps({
"recordId": case["id"],
"modelInput": {
"system": [{"text": RULES}],
"messages": [{"role": "user", "content": [{"text": case["prompt"]}]}],
"inferenceConfig": {"maxTokens": 1024},
},
}))
key = f"evals/{run_id}/input.jsonl"
s3.put_object(Bucket=BUCKET, Key=key, Body=("\n".join(lines) + "\n").encode())
job = bedrock.create_model_invocation_job(
jobName=f"eval-{run_id}",
roleArn=os.environ["BATCH_ROLE_ARN"],
modelId=os.environ["EVAL_MODEL_ID"],
modelInvocationType="Converse",
inputDataConfig={"s3InputDataConfig": {"s3Uri": f"s3://{BUCKET}/{key}"}},
outputDataConfig={"s3OutputDataConfig": {"s3Uri": f"s3://{BUCKET}/evals/{run_id}/out/"}},
timeoutDurationInHours=24,
)
return job["jobArn"]iam:PassRole for that role.
A job moves through Submitted, Validating, Scheduled, InProgress and ends as Completed, PartiallyCompleted, Failed, Stopped or Expired (scheduled but not started before the timeout). Use EventBridge job state change notifications rather than polling from a request thread. When it finishes, read the results:
python
Output order isn’t guaranteed to match input order, so always join on import json
import boto3
s3 = boto3.client("s3")
def read_batch_results(bucket: str, prefix: str):
"""Yield (record_id, output, error) from every .out file except the manifest."""
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
for obj in page.get("Contents", []):
key = obj["Key"]
if key.endswith("manifest.json.out"):
manifest = json.loads(s3.get_object(Bucket=bucket, Key=key)["Body"].read())
print("records ok/err:", manifest["successRecordCount"], manifest["errorRecordCount"])
continue
if not key.endswith(".out"):
continue
body = s3.get_object(Bucket=bucket, Key=key)["Body"].read().decode()
for line in body.splitlines():
rec = json.loads(line)
yield rec.get("recordId"), rec.get("modelOutput"), rec.get("error")recordId. Records that failed carry an error object instead of modelOutput, and the manifest.json.out file summarizes record and token counts.
Caching or batch: choose per workload
| Workload | User waiting? | Repeated long prefix? | Use |
|---|---|---|---|
| Agent turns in an IDE session | Yes | Yes (tools, rules) | Prompt caching |
| One-off questions with unique large context | Yes | No | On-demand; shrink the prompt |
| Nightly eval suite | No | Maybe | Batch inference (no caching there) |
| Bulk labelling or summarizing history | No | No | Batch inference |
| Anything needing tool calls | Either | Either | On-demand (batch doesn’t support tool calling) |
Worked example: one agent, two paths
- Your review agent sends about 9,000 tokens of tool specs and rules on every turn (illustrative). You add checkpoints after the tools and after the rules.
- The usage metric shows the first turn of each session writing to the cache and later turns reading from it. Sessions with long pauses show repeated writes, so you test the 1-hour TTL on a model that supports it.
- A platform team renames a core package. The rules hash changes, the next calls show cache writes instead of reads (as expected), and the canary confirms the agent no longer suggests the old package.
- The nightly eval of 5,000 cases moves from an on-demand loop to one batch job in the Converse format. The morning report joins outputs by
recordIdand lists the records that came back with errors.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
cacheReadInputTokens always 0 | Prefix below the model minimum, model doesn’t support explicit caching, or something dynamic sits before the checkpoint | Check the model table; move dynamic content after the checkpoint |
| Every call writes to the cache | Prefix changes each time (timestamps, reordered tools) or gaps longer than the TTL | Make the prefix byte-stable; consider the 1-hour TTL |
ValidationException after adding ttl | Model supports only the 5-minute TTL | Remove the ttl field for that model |
| Input token counts look too low | inputTokens excludes cached tokens | Total = inputTokens + cacheRead + cacheWrite |
Batch job Failed during validation | Role can’t read S3, file isn’t .jsonl, or quotas not met | Fix permissions and file format; check record quotas |
Batch job Expired | It was scheduled but didn’t start before the timeout | Resubmit; use a longer timeoutDurationInHours |
| Agent follows removed rules | Old rules source or an app cache without the rules version | Version rules by content hash; run canaries after changes |
Production checklist
- ☐ Checkpoints after tools and after system rules; nothing dynamic before them
- ☐ Prefix meets the model’s minimum tokens; TTL chosen per session pattern
- ☐ Cache read/write tokens and hit ratio logged per route
- ☐ Rules version (content hash) logged with each call and included in your own cache keys
- ☐ Canary prompts run after every rules or prompt-version change
- ☐ Untrusted, per-request text placed after the checkpoints
- ☐ Offline workloads on batch inference, joined by
recordId, with EventBridge notifications - ☐ Savings forecasts based on measured hit ratios and current pricing
FAQ
Can I use prompt caching inside a batch job?
No. Prompt caching is supported only for on-demand inference, not the batch inference API.Can Prompt Management prompts use caching?
Yes. When you create or edit a prompt in Bedrock Prompt Management you can enable prompt caching, and depending on the model cache the system prompt, system instructions and messages.Does caching change the model’s answers?
It’s meant to skip reprocessing an identical prefix, not to change behavior. If outputs differ after you enable it, check whether your prompt content or order changed.Why are my batch outputs out of order?
Bedrock doesn’t guarantee output order. Join onrecordId.
Official documentation
- Prompt caching for faster model inference (checkpoints, TTL, supported models)
- TokenUsage (cache usage fields)
- Process multiple prompts with batch inference
- Format and upload batch inference data
- Create a batch inference job
- Batch job statuses
- View the results of a batch inference job
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.

Pingback: Bedrock Prompt Management: Versioned System Prompts for Production - CheatCoders