Until recently, "which team spent this on Bedrock?" had no answer in the bill. Every invocation from every application in an account collapsed into one line per model, and the usual workaround was to stitch CloudTrail events to CloudWatch metrics to billing data by hand. AWS has since shipped two features that answer it directly, and they answer different questions.
Why doesn't Bedrock cost break down by team by default?
Because there is nothing to break it down by. Bedrock inference is not a
resource you tag. There is no instance, no cluster, no bucket carrying a
team tag that the billing pipeline can attach to the charge. The
charge is attached to a model invocation, and an invocation historically
carried no identity beyond the account it was billed to.
That is the same structural problem that EKS split cost allocation data solves for pods: a shared resource generating cost on behalf of many owners, with no native column naming the owner. AWS solved it for Bedrock in two different ways.
What is IAM principal cost allocation?
AWS
supports cost allocation for Amazon Bedrock based on IAM principal identity and tags.
Per that documentation, when you enable IAM principal data in CUR 2.0, "AWS
automatically records the caller identity (IAM principal ARN) for each
Bedrock API call in the line_item_iam_principal column."
The CUR 2.0 line item column reference defines it as "the IAM ARN of the principal that performed the Amazon Bedrock model inference. This column is populated when you enable IAM principal data in your CUR 2.0 data export. Currently supported for Amazon Bedrock only."
The ARN takes one of three documented shapes:
arn:aws:iam::123456789012:user/userID_A
arn:aws:iam::123456789012:role/application-role
arn:aws:sts::123456789012:assumed-role/application-role/session-name
On its own that is a caller, not a team. The second half is tagging the IAM users and roles themselves. AWS states that when you "apply tags to IAM principals (users or roles), those tags are automatically captured with associated costs and token usage," and explicitly that this "eliminates manual reconciliation processes that required combining CloudWatch and CloudTrail logs with billing data."
The feature is new enough that it is worth checking your export version. The
CUR 2.0 data dictionary
notes that the INCLUDE_IAM_PRINCIPAL_DATA table configuration
"only adds data in the new columns starting April 8, 2026." Enabling it does
not backfill a period before that.
How do you turn it on?
Three steps, in this order, per the AWS setup instructions:
-
In the IAM console, apply tags to the users and roles that call Bedrock.
AWS suggests keys that map to your org chart:
department,cost-center,team,project,environment. - In Billing and Cost Management, under Cost Organization, open Cost Allocation Tags, filter for IAM principal type tags, select yours and choose Activate.
- In Data Exports, create a Standard data export (CUR 2.0) and under Additional export content select Include caller identity (IAM principal) allocation data.
There is a chicken-and-egg step people trip over. AWS documents that "for Amazon Bedrock, tags only appear for activation after the IAM principal with the tags has made at least one API call. This applies whether the principal is an IAM user, role, or assumed role session." So a freshly tagged role that has not yet invoked a model will not be in the activation list at all, and nothing is wrong.
Then the usual billing latency applies twice over: "after you apply tags to your IAM principals, it can take up to 24 hours for the tag keys to appear on your cost allocation tags page for activation. It can then take up to 24 hours for tag keys to activate."
What does it cost you in report size?
This is the part that does not appear in any vendor's summary of the feature. AWS states plainly: "Enabling IAM principal data will increase the number of CUR rows by a factor of the number of calling identities accessing each model, resulting in larger file sizes compared to typical CUR exports."
Read that multiplicatively. Ten roles calling four models is up to forty rows
where you had four. If you already run
INCLUDE_RESOURCES and INCLUDE_SPLIT_COST_ALLOCATION_DATA
on the same export, those multipliers compose. AWS's own best-practice list
for this feature ends with "plan for CUR file size growth: account for
increased Amazon S3 storage costs when enabling IAM principal data," and
warns against high-cardinality tag values: "do not use unique session IDs,
timestamps, or random GUIDs as tag values."
Where do the tags actually land in CUR 2.0?
In the tags column, which the
CUR 2.0 tags column reference
defines as "a map column containing key-value pairs of all tags and their
values for a given line item," of type map<string, string>.
Two conditions gate what appears in it: "tag keys only appear in this column
if they've been enabled as cost allocation tags in the Billing console. After
being enabled, a particular key only appears in the map column if it has a
value that applies to the specific line item."
Keys are prefixed by where the tag came from, so the same word from different sources does not collide:
| Prefix | Tag source |
|---|---|
resourceTags/ | Tags applied directly to AWS resources |
userAttribute/ | User attributes imported from IAM Identity Center |
accountTag/ | Tags applied at the AWS account level |
costCategory/ | Tags derived from AWS Cost Categories |
iamPrincipal/ | Tags applied to IAM principals |
AWS's own worked example has department arriving from four
sources at once with four different values, which is worth internalising
before you write a query that assumes there is one:
{
"resourceTags/department": "teamA",
"userAttribute/Department": "teamB",
"accountTag/department": "teamC",
"costCategory/department": "teamD"
}
So the Bedrock spend for a department is iamPrincipal/department,
and it is a genuinely different number from
resourceTags/department. Naming that distinction in your queries
is not pedantry: it is the difference between "cost this team's code caused"
and "cost sitting on resources this team owns."
The map keys can be queried as columns with the dot operator, so in Athena a per-department Bedrock breakdown is roughly:
SELECT tags['iamPrincipal/department'] AS department,
sum(line_item_unblended_cost) AS cost
FROM cur2
WHERE line_item_iam_principal IS NOT NULL
AND bill_billing_period_start_date = TIMESTAMP '2026-08-01 00:00:00'
GROUP BY 1
ORDER BY 2 DESC
Filtering on line_item_iam_principal IS NOT NULL rather than on a
service name is deliberate: AWS defines that column as "currently supported
for Amazon Bedrock only", so its presence already restricts the rows to
Bedrock inference without depending on the exact spelling of a product code.
What do application inference profiles do instead?
They attach the identity to the call path rather than the caller.
Per the
Create an application inference profile
documentation, "you can create an application inference profile with one or
more Regions to track usage and costs when invoking a model." You give it a
modelSource (a foundation model, or a cross-Region system-defined
profile), optionally attach tags, and get back an
inferenceProfileArn that your application invokes in place of the
model ID. The
CreateInferenceProfile API reference
gives the request shape: modelSource is a union whose member is
copyFrom, and tags is an array of key/value objects.
aws bedrock create-inference-profile \
--inference-profile-name checkout-assistant \
--model-source copyFrom=arn:aws:bedrock:us-east-1::foundation-model/MODEL_ID \
--tags key=team,value=payments key=cost-center,value=CC-4471
The two mechanisms answer different questions, and they compose:
| IAM principal allocation | Application inference profile | |
|---|---|---|
| Identity is | Who called | Which application called |
| Attached to | An IAM user or role | A profile ARN your code invokes |
| Code change | None | Yes: invoke the profile ARN, not the model ID |
| Appears as | line_item_iam_principal and iamPrincipal/ tags | Tags on an AWS resource, which is what the resourceTags/ prefix covers |
| Fails when | Many teams share one role | Teams call the model ID directly and bypass it |
If several teams share a single application role, IAM principal tags cannot separate them and profiles can. If teams have their own roles but you cannot change application code, the reverse. Note also that application inference profiles are not available everywhere: the supported Regions list names fifteen Regions, and AWS notes "some models, such as embedding models, do not support inference profiles."
Can CloudWatch metrics tell you cost per team?
No, and it is worth being exact about why, because this is where most
home-built Bedrock dashboards stop. The
bedrock-runtime CloudWatch metrics reference
lists the metrics published under the AWS/Bedrock namespace:
| Metric | AWS description |
|---|---|
Invocations | Number of successful requests to Converse, ConverseStream, InvokeModel and InvokeModelWithResponseStream |
InputTokenCount | Number of tokens in the input |
OutputTokenCount | Number of tokens in the output |
CacheReadInputTokens | Input tokens read from the prompt cache. "These tokens are charged at a reduced rate and don't count toward your TPM quota." |
CacheWriteInputTokens | Input tokens written to the prompt cache. "These tokens count toward your TPM quota." |
InvocationLatency | Time from request sent to last token received |
TimeToFirstToken | Time to first token, streaming operations only |
InvocationClientErrors | Invocations that result in client-side errors |
InvocationServerErrors | Invocations that result in AWS server-side errors |
InvocationThrottles | Invocations that the system throttled |
And then the constraint. AWS documents exactly two dimension shapes for these
metrics: "ModelId – all metrics" and
"ModelId + ImageSize + BucketedStepSize –
OutputImageCount." There is no principal dimension, no profile
dimension, no tag dimension. CloudWatch can tell you what a model did; it
cannot tell you who asked. Token counts per team have to come from the
billing path, not the metrics path.
Two more things worth knowing before you build alerting on these. Throttles
are not errors and not invocations: "throttled requests and other invocation
errors don't count as either Invocations or Errors," and the count you see
depends on your SDK retry settings. And
EstimatedTPMQuotaUsage, tempting as it looks, carries an explicit
AWS warning that it "is an approximation and does not reflect the
reservation-based token consumption that drives throttling decisions…
do not use this metric as the sole indicator for quota use or capacity
planning."
What does none of this tell you?
Four things, and pretending otherwise is how a chargeback model loses its audience:
- Cost per feature or per customer. Both mechanisms resolve to an identity you configured in advance. If one role serves ten customers, the bill sees one role.
- Anything before you switched it on. The IAM principal column has data only from 8 April 2026 onward, and only after you enable the table configuration. Cost allocation tag backfill, covered in why cost allocation tags show up empty, works on tag activation status, not on a column that was never exported.
- Whether the spend was worthwhile. A department total is an allocation, not a unit economic. The denominator, requests served or customers supported, is yours and is not in the CUR.
- Rates. Deliberately, no per-token prices appear above. Allocation tells you whose tokens they were, not what a token costs, and the two are metered separately: AWS notes only that prompt-cached reads "are charged at a reduced rate". Per-model rates change, so read them on the Amazon Bedrock pricing page and validate against your own CUR rather than against a number in a blog post.
One last note on export format. Both of these are CUR 2.0 features. If you
standardised on FOCUS for cross-cloud reporting, neither the
line_item_iam_principal column nor the CUR 2.0 table
configurations exist there, for the same reason
FOCUS cannot produce per-pod cost. Export
both.