
Amazon Bedrock Cost per Customer: Per-Tenant AI Cost Tracking and Real-Time Caps on AWS
Your AWS bill shows Bedrock spend per model, not per customer, so you can't tell which accounts lose you money. This guide covers three levels with code: tagged application inference profiles, requestMetadata with invocation logs, and a DynamoDB meter that enforces a real-time AI budget per tenant, even under concurrent requests. It ends with a Per-Customer AI Margin Sheet you can copy.
A SaaS company adds an AI assistant to its product. It's included in every plan. Customers love it.
Three months later, the Bedrock bill is $9,600 a month, and nobody can say which customers it comes from. The AWS bill shows one line per model. It doesn't show customer names.
So an engineer spends a day tagging every model call with the customer's ID. The first report shows:
- 3 customers paying 9% of revenue drive 52% of the AI cost
- One of them, on the $299/month plan, costs $1,840/month in AI alone
- That account's margin: −$1,541 a month
Nobody did anything wrong. The customer connected their helpdesk through the API, and the assistant re-summarized the whole ticket thread on every single update: 12,000 calls a month, each carrying about 40,000 tokens.
The pricing page said "AI included." Nobody had asked, "included up to what?"
This post shows how to answer that question on AWS: cost per customer, and a cap you can enforce before the bill arrives.
(Illustrative numbers. Swap in yours; the pattern is common.)
TL;DR
- Your AWS bill shows Bedrock spend by model and region, not by customer, user or feature. You need to add the "who" yourself.
- There are three levels of attribution, and most teams need all three:
- Application inference profiles + cost allocation tags → spend per product, team or plan tier, right in Cost Explorer
requestMetadata+ model invocation logs → spend per customer, user and feature, at any scale- A real-time meter in DynamoDB → stop one customer at their limit without affecting anyone else
- AWS Budgets can't do level 3. Billing data lags by hours, and budget actions work on IAM roles, not on one customer inside a shared app.
- Then fix your pricing, not just your prompts: every plan needs an AI allowance, a soft warning and a defined "what happens at the limit."
Why the bill can't tell you
When your app calls Bedrock, AWS knows which IAM role made the call and which model ran. It doesn't know that the call was for customer #17's ticket summary.
In a multi-tenant SaaS, every customer's request goes through the same role, the same model and the same line on the invoice. So the questions every founder eventually asks have no answer:
- Which customers are profitable once AI costs are included?
- Which feature is the expensive one?
- If our biggest customer doubles their usage tomorrow, what happens to our margin?
You fix this by adding the "who" at call time. AWS gives you two hooks for that, and you build the third piece yourself.
| Question | AWS feature | How many groups | How fresh |
|---|---|---|---|
| Which product, team or plan tier? | Application inference profiles + cost allocation tags | Tens to a few hundred | About a day (billing data) |
| Which customer, user or feature? | requestMetadata + model invocation logs | Unlimited | Minutes |
| Can I stop one customer right now? | Your own meter (DynamoDB) | Unlimited | Real time |
Level 1: Spend per product or plan tier in Cost Explorer
An application inference profile is a named, taggable wrapper around a model. You call the profile instead of the model, and its cost allocation tags show up in Cost Explorer and the Cost and Usage Report.
Create one per product or plan tier, not per customer. There's a per-account quota, and Cost Explorer is the wrong tool for 5,000 tenants anyway.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import boto3
bedrock = boto3.client("bedrock", region_name="us-east-1")
resp = bedrock.create_inference_profile(
inferenceProfileName="support-assistant-growth-plan",
modelSource={
# A foundation model ARN, or a cross-Region (system) inference profile ARN
"copyFrom": "<MODEL_OR_SYSTEM_INFERENCE_PROFILE_ARN>"
},
tags=[
{"key": "product", "value": "support-assistant"},
{"key": "plan", "value": "growth"},
],
)
GROWTH_PROFILE_ARN = resp["inferenceProfileArn"]
Then go to Billing and Cost Management → Cost allocation tags and activate
product and plan. Tags only appear in Cost Explorer after activation, and only for usage from then on.Now finance can see "the Growth plan's AI costs $3,100 a month" without asking engineering.
Level 2: Spend per customer with requestMetadata
For customer-level detail, add
requestMetadata to every Converse call. These key-value pairs don't change the response; they're written to your model invocation logs, where you can group by them.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
runtime = boto3.client("bedrock-runtime", region_name="us-east-1")
resp = runtime.converse(
modelId=GROWTH_PROFILE_ARN, # level 1: the tagged profile
messages=messages,
inferenceConfig={"maxTokens": 1500},
requestMetadata={ # level 2: who and what
"tenant_id": tenant.id,
"user_id": user.id,
"feature": "ticket-summary",
},
)
usage = resp["usage"]
# inputTokens, outputTokens, and cacheReadInputTokens / cacheWriteInputTokens if you use prompt caching
Turn on model invocation logging (Bedrock console → Settings) with CloudWatch Logs as a destination. Then this CloudWatch Logs Insights query gives you tokens per customer:
1
2
3
4
5
6
7
8
9
fields requestMetadata.tenant_id as tenant,
input.inputTokenCount as in_tok,
output.outputTokenCount as out_tok
| stats sum(in_tok) as input_tokens,
sum(out_tok) as output_tokens,
count(*) as calls
by tenant
| sort input_tokens desc
| limit 20
Multiply by your model's prices and you have your top 20 customers by AI cost. Swap
tenant for feature to find the expensive feature. In the story above, that's the query that surfaced customer #17 and the "summarize on every update" pattern.Two rules for
requestMetadata:- Use IDs, never emails or names. Invocation logs are data too, and you don't want PII spread across log groups.
- Add it in one place. Wrap your Bedrock client so no code path, including background jobs, retries and evals, can call a model without a tenant ID. Anything without one goes into an
unattributedbucket, and you should alert when that bucket grows above about 5%.
Level 3: A real-time cap per customer
Reports tell you what happened last month. To stop a runaway customer this afternoon, you need a check in the request path.
You might think of AWS Budgets first. It's great for alerts, but it can't do this job:
- Billing data refreshes a few times a day, so a runaway loop can burn hours of spend before the alert fires.
- Budget actions apply IAM or SCP policies. In a shared app, that would cut off every customer, not just the one over their limit.
The pattern that works is a small meter of your own: reserve before the call, settle after it.
One DynamoDB item per customer per month holds their remaining AI allowance in micro-dollars (integers, so there's no floating-point drift):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from botocore.exceptions import ClientError
ddb = boto3.client("dynamodb")
TABLE = "ai_allowance"
# $ per 1M tokens (illustrative; use your model's real prices and version this map)
PRICE = {"input": 3.00, "output": 15.00, "cache_read": 0.30, "cache_write": 3.75}
def cost_micro(usage):
# tokens × ($ per 1M tokens) = micro-dollars
return round(
usage["inputTokens"] * PRICE["input"]
+ usage["outputTokens"] * PRICE["output"]
+ usage.get("cacheReadInputTokens", 0) * PRICE["cache_read"]
+ usage.get("cacheWriteInputTokens", 0) * PRICE["cache_write"]
)
def reserve(tenant_id, month, estimate):
"""Take the worst-case cost up front. Fails if the customer can't afford it."""
try:
ddb.update_item(
TableName=TABLE,
Key={"pk": {"S": f"{tenant_id}#{month}"}},
UpdateExpression="SET remaining = remaining - :est",
ConditionExpression="remaining >= :est",
ExpressionAttributeValues={":est": {"N": str(estimate)}},
)
return True
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return False
raise
def settle(tenant_id, month, estimate, actual):
"""Give back what the call didn't use."""
ddb.update_item(
TableName=TABLE,
Key={"pk": {"S": f"{tenant_id}#{month}"}},
UpdateExpression="ADD remaining :refund",
ExpressionAttributeValues={":refund": {"N": str(estimate - actual)}},
)
And around every model call:
1
2
3
4
5
6
7
8
9
10
11
12
estimate = round((len(prompt_text) / 4) * PRICE["input"] + MAX_TOKENS * PRICE["output"])
if not reserve(tenant.id, month, estimate):
return over_limit_response(tenant) # see "what happens at the limit" below
try:
resp = runtime.converse(...) # as in level 2
except Exception:
settle(tenant.id, month, estimate, 0) # failed call: give it all back
raise
settle(tenant.id, month, estimate, cost_micro(resp["usage"]))
Why reserve first instead of checking the balance? Because of concurrency. If a customer fires 50 requests at once and each one only checks the balance, all 50 pass and the customer blows straight through the cap. The conditional update is atomic: each request takes its worst-case cost, and request 51 is refused. Since
maxTokens caps the output, the worst case is known before the call.For internal teams (developers using coding assistants on Bedrock, say), the same idea works as a gateway with per-user keys. Hari Shanker Tripathi's open-source Bedrock Meter , covered on AWS Builder Center, is a good reference implementation.
What to do with the numbers
Once you can see cost per customer, the fixes are usually less about the model and more about pricing and product design.
For customer #17, the team made three changes:
- Summarize when a ticket changes status, not on every update. Calls dropped from 12,000 to 5,000 a month. Same value to the customer.
- Prompt caching for the long ticket thread that repeats between calls. Cache reads are billed at a fraction of the normal input price.
- Bulk backfills go through Bedrock batch inference, which costs about half the on-demand price for supported models, instead of thousands of live calls.
AI cost for that account: $1,840 → $610. And because the usage report made the conversation easy, the customer moved to the $999 plan with a bigger AI allowance. Account margin: −$1,541 → +$389 a month.
Then they changed the pricing page:
- Every plan includes an AI allowance (in credits or actions, not tokens; customers don't think in tokens).
- At 80%, the customer and their account manager get an email.
- At 100%, the product doesn't just break. It degrades: a smaller model, a queue instead of instant answers, or "upgrade to continue." Decide this up front. A hard cutoff mid-conversation is the fastest way to turn a power user into a churned user.
"AI included" is not a pricing model. "AI included up to X" is.
Failure points
- Untagged calls. One background job without
requestMetadataand your report is quietly wrong. Enforce it in a shared client and alert on theunattributedshare. - Only counting the model. Retrieval (Knowledge Bases, vector store), embeddings, Lambda tools and logs cost money too. Allocate them by the same tenant ID, or at least by each tenant's share of calls.
- Ignoring cache tokens. If you use prompt caching,
usagereports cache reads and writes separately, and they're priced differently. A price map with only "input" and "output" gets per-customer cost wrong. - One inference profile per end user. Profiles have quotas and Cost Explorer isn't built for thousands of tags. Profiles per tier or product;
requestMetadataper customer. - No allowance row at month start. With the conditional update above, a missing row means the call is refused. Create next month's rows ahead of time with a scheduled job, and decide on purpose whether your meter fails open (serves, logs, alerts) or fails closed (refuses).
- A stale price map. Model prices change and you'll switch models. Keep prices in config with a version and an effective date, not hard-coded.
- PII in metadata. IDs only. Your logs are kept longer and read by more people than your database.
The Per-Customer AI Margin Sheet
Fill this in once a month. It takes one Logs Insights query and your billing export.
| Customer | Plan price / month | AI cost / month | AI cost as % of plan | Top feature by cost | Action |
|---|---|---|---|---|---|
| #17 | $299 | $1,840 | 615% | ticket-summary | Fix trigger + upgrade talk |
| #04 | $999 | $420 | 42% | chat | Watch |
| #31 | $299 | $35 | 12% | chat | OK |
| Unattributed | n/a | $___ | n/a | n/a | Fix tagging if over 5% |
Rules of thumb (adjust to your target margin):
- Under 20% of plan price: healthy
- 20–50%: watch it, and check for wasteful patterns
- Over 50%: talk to the customer or change the feature
- Over 100%: you're paying them to use your product
5 questions a founder should ask the AI team
- Which 5 customers cost us the most in AI, and what do they pay us?
- What share of our Bedrock spend is unattributed?
- If one customer sends 10× their usage tomorrow, what stops them?
- Can we cut off one customer without affecting the others?
- Does our pricing page say what "AI included" includes?
If the answer to #3 is "the monthly AWS bill," you have a margin risk and a reliability risk at the same time.
Wrapping up
The AWS bill tells you what you spent on AI. Only your own data tells you who you spent it on. To get there:
- Level 1: application inference profiles + cost allocation tags → cost per product and plan in Cost Explorer
- Level 2:
requestMetadataon every call + invocation logs → cost per customer, user and feature - Level 3: reserve-then-settle in DynamoDB → a real-time cap per customer that holds under concurrency
- Then: put an AI allowance in every plan and decide what happens at the limit
Do it before your biggest customer finds the API.
I'm an AWS Community Builder writing practical posts on building AI on AWS, for the people who build it and the people who pay for it. My earlier Builder Center posts cover AWS Lambda as AI agent tools, and why cost per resolved conversation beats token price for AI agent ROI.
Originally published on Medium.
Question for the comments: how do you charge for AI in your product today?
- A · Included, no limit
- B · Included, with a fair-use cap
- C · Usage-based or credits
- D · We don't know our AI cost per customer yet
Reply with a letter. I'll answer every reply.
# agents# amazon-bedrock-agentcore# cost-optimization# aws-ai-league# advertising-marketing-industry
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article