,

Cut Bedrock Token Costs: Prompt Caching and Right-Sizing

Scissors trimming a ribbon of token coins above an AI model server, illustrating ways to cut Bedrock token costs

Most Bedrock token costs aren’t caused by the model being expensive. They’re caused by you sending it the same 6,000-token system prompt four hundred times an hour, asking a frontier model to classify an email, and letting it ramble without a maxTokens cap. Once you can see where the money goes, these are the knobs that move the number, roughly in order of payoff.

Where Bedrock Token Costs Actually Come From

Three facts from AWS’s tokenomics guidance explain almost every runaway bill:

  • Output tokens cost roughly three times as much as input tokens.
  • Multi-turn context compounds. A session that starts around 500 tokens can hit 5,000 by turn ten, because every turn resends the history.
  • Repeated prefixes (system prompts, tool definitions, reference docs) are billed every single call unless you cache them.

So the playbook is: cache the static stuff, shrink what you send, cap what comes back, and route work to the cheapest model that does the job.

Prompt Caching: Layout Is Everything

Bedrock prompt caching can cut costs by up to 90% and latency by up to 85% on cached content, but only for exact prefix matches. Bedrock evaluates checkpoints in order tools → system → messages, so the rule is brutal and simple: static content first, dynamic content last. Put a timestamp or user name at the top of your system prompt and you’ve just invalidated the cache on every call. Nice going.

import boto3
brt = boto3.client("bedrock-runtime")

resp = brt.converse(
    modelId=MODEL_OR_PROFILE_ARN,
    toolConfig={"tools": TOOL_SPECS + [{"cachePoint": {"type": "default"}}]},
    system=[
        {"text": STATIC_INSTRUCTIONS + REFERENCE_DOCS},   # never changes
        {"cachePoint": {"type": "default", "ttl": "1h"}},
    ],
    messages=[{"role": "user", "content": [{"text": user_question}]}],  # changes
    inferenceConfig={"maxTokens": 400},
)
u = resp["usage"]
print(u.get("cacheReadInputTokens"), u.get("cacheWriteInputTokens"), u["inputTokens"])

Rules worth taping to your monitor:

  • Minimums are cumulative and per model. Newer Claude models cache from 512 or 1,024 tokens; Claude Haiku 4.5 and older Opus versions need 4,096 tokens of prefix before a checkpoint counts. Below that, nothing caches and nothing errors.
  • Four checkpoints max per request on Claude models.
  • TTL is 5 minutes by default, with a 1-hour option on supported models. Use 1h for prefixes shared across bursty traffic; longer-TTL checkpoints must come before shorter ones.
  • Writes aren’t free. Cache writes bill at or above the normal input rate, so caching a prefix used once is a loss. Watch the cacheReadInputTokens to cacheWriteInputTokens ratio; if reads don’t dwarf writes, your layout is wrong.
  • Caching doesn’t apply to batch inference, which already has its own discount.

Right-Size the Model, and the Harness

Classification, extraction, routing, and short summaries don’t need a frontier model. Build a tiered router: a small, cheap model by default, a mid-tier model for reasoning and code, and the top tier only when a task fails validation or is flagged as high-stakes. The tiers are getting explicit across vendors; OpenAI’s GPT-6 Luna and Sol on Bedrock are positioned exactly that way, with Luna for high-volume extraction and classification, Sol for recurring complex work, and Astra above them.

Don’t stop at the model ID. The agent harness around it matters just as much. AWS cites a benchmark where the same Haiku 4.5 coding task cost about 1 cent in Kiro and 7 cents in Claude Code. Same model, 7x the bill, purely from how much context and how many turns the tool used. Benchmark your harnesses before you standardize on one.

Conveyor routing small tasks to small AI models and heavy tasks to a large model, with a locked sandbox gate

Cap the Output, Structure the Input

Since output is the pricey side, cap it. Set maxTokens to what the task needs, not the model maximum, and ask for structured output (a JSON schema or a fixed field list) instead of prose. A model told to return {"category": ..., "confidence": ...} can’t pad its answer with three paragraphs of pleasantries.

Tighten the input too. AWS’s example rewrote a polite conversational prompt into a terse structured directive and dropped it from 85 tokens to 38, a 55% cut, with the same result. Version prompts in Bedrock Prompt Management so a token regression shows up in review, not on the invoice.

Stop Resending the Whole Conversation

Unbounded chat history is the silent multiplier. Keep a sliding window of recent turns and fold older turns into a running summary produced by a cheap model:

WINDOW = 6  # recent messages kept verbatim

def build_context(history, summary):
    recent = history[-WINDOW:]
    older = history[:-WINDOW]
    if older:
        summary = summarize_with_small_model(summary, older)  # e.g. a Haiku-class call
    prefix = [{"role": "user", "content": [{"text": f"Conversation so far: {summary}"}]},
              {"role": "assistant", "content": [{"text": "Understood."}]}]
    return prefix + recent, summary

The same logic applies to agents and MCP. Every tool schema rides along on every turn. An agent wired to a server exposing forty tools with verbose descriptions can spend thousands of tokens per call before anyone says hello. Load only the tools a given agent needs, trim descriptions to one line, drop unused parameters, and cache what remains with a tools checkpoint.

Guardrails: SCPs and Gateway Budgets

Optimization relies on good behavior; guardrails don’t. Use a service control policy on your sandbox and dev OUs to keep frontier models out of experiments that don’t need them:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyFrontierModelsInSandbox",
    "Effect": "Deny",
    "Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
    "Resource": ["arn:aws:bedrock:*::foundation-model/anthropic.claude-opus-*"]
  }]
}

Converse calls are authorized through the InvokeModel actions, and inference profile calls are checked against the underlying foundation model too, so this blocks both paths. Adjust the resource patterns to whatever you consider premium.

Then put a gateway in front of shared access and give every team a hard token or dollar budget. LiteLLM’s proxy supports per-team budgets with reset windows:

curl -X POST "$LITELLM_URL/team/new" \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"team_alias": "search", "max_budget": 300, "budget_duration": "30d"}'

AWS-native options are arriving too: the Claude Apps Gateway for Claude on Bedrock, and the open-source Dogwood policy layer for AgentCore Gateway, which caps token budgets and blocks runaway agent loops. Whatever you choose, the cap belongs in the request path, not in a dashboard someone checks on Fridays.

Work this list top to bottom on your three most expensive workloads. Caching and output caps usually pay off within a day; model routing and context trimming take a sprint. Either way, you’ll be cutting real money, not shaving pennies.

At Enkompass (enkompass.net), this is our home turf. Our FinOps work has clawed back six figures of annual AWS spend for clients, and Bedrock token bills are now part of that hit list, from cache-friendly prompt layouts to model routing and SCP guardrails. If you’d rather have seasoned cloud people handle it instead of learning the hard way at 2 AM, let’s talk. Reach out through our contact page or call us directly at +1-828-436-5667 — we’ll help you turn this from a someday-project into a done-deal.

Ready when you are

Let’s map your route to AWS

A free 60-minute architecture review with a senior engineer. No slides, no SDR, just a plan.

Discover more from Enkompass

Subscribe now to keep reading and get access to the full archive.

Continue reading