Skip to content

Know who is spending on Amazon Bedrock — attribute costs to every team, tenant, and user

12 minute read
Content level: Advanced
0

Amazon Bedrock now natively attributes every inference cost to the IAM principal that made the call. This post shows engineers and FinOps teams how to set it up and query it.

When your Amazon Bedrock bill doubles, Finance asks a direct question: Which team is driving this? Without an answer, you cannot do chargebacks, cannot spot waste, and cannot tell whether the spend is creating value.

The challenge is structural. Unlike Amazon EC2 or Amazon RDS, Bedrock workloads produce no taggable resources. Every InvokeModel and Converse call is a cost event with no natural owner. The billing unit is tokens — invisible, ephemeral, and consumed at the speed of a function call.

In this post, we show how to use IAM principal-based cost allocation to attribute every Amazon Bedrock token to the team, application, or tenant that consumed it — with no changes to your Bedrock API calls and no additional cost.

How it works

When you enable IAM principal data in your AWS Cost and Usage Report (CUR 2.0), AWS automatically records the identity of the caller for each Bedrock inference request. This adds two things to your billing data:

  • line_item_iam_principal — the full IAM ARN of the caller (user, role, or assumed-role session)
  • IAM principal tags — tags you attach to IAM users or roles, which appear in CUR 2.0 with the iamPrincipal/ prefix (for example, iamPrincipal/department, iamPrincipal/application)

The same attribution works across all Amazon Bedrock APIs: InvokeModel, Converse, batch inference, Bedrock API keys, and the OpenAI-compatible Bedrock Mantle endpoint (Chat Completions, Responses API).

Prerequisites

Before you begin:

  • IAM users or roles are making Amazon Bedrock API calls
  • You have access to the IAM console and the AWS Billing and Cost Management console
  • You have a CUR 2.0 data export configured, or permissions to create one

Important: IAM principal cost allocation must be activated at the management account level in the Billing and Cost Management console. Member accounts in an AWS Organization cannot activate this independently.

Choose your attribution pattern

Most organizations use a combination of these patterns depending on how their applications call Bedrock.

PatternBest forComplexityWhat you change
1. Tag the roleDedicated role per app or teamLowTag the IAM role once — zero code changes
2. Session tagsShared role, LLM gateway, multi-tenantMediumAdd Tags to your AssumeRole call
3. Federated SSOOkta, Entra ID, AWS IAM Identity CenterMediumConfigure IdP to pass attributes as session tags
4. Application inference profilesPer-model Amazon CloudWatch metricsMediumSwap modelId for a profile ARN
5. Bedrock ProjectsOpenAI-compatible Mantle APIsLowAssociate calls with a project

Guidance: Start with the pattern that matches your current architecture. If you have a dedicated role per application, tag it — zero application changes required. If you have a shared gateway role, use session tags. These patterns work together in the same CUR 2.0 table.

Define your tag taxonomy

Establish consistent tag keys before you tag anything. Inconsistent naming (dept vs Department vs department) splits one dimension into three and fragments your cost views.

Tag keyPurposeExample values
applicationWorkload identifiercustomer-chatbot, ticket-summarizer
departmentOrganizational ownerproduct, data-science, platform
environmentDeployment stageproduction, staging, development
cost_centerFinancial reporting codeCC-4401, CC-4502
tenantExternal customer (ISV/multi-tenant)customer-a, customer-b (session tag)

Use AWS Tag Policies in audit mode to enforce your convention across the organization.

Activate the data pipeline

Complete these three steps before generating traffic. Usage that occurs before activation appears as untagged in CUR and cannot be retroactively attributed.

Step 1: Tag your IAM principals

Tag the IAM users or roles that call Amazon Bedrock. Using the AWS CLI:

aws iam tag-role \
  --role-name my-bedrock-app-role \
  --tags Key=application,Value=customer-chatbot \
         Key=department,Value=product \
         Key=environment,Value=production \
         Key=cost_center,Value=CC-4401

Or using the AWS SDK for Python:

import boto3
iam = boto3.client('iam')

iam.tag_role(
    RoleName='my-bedrock-app-role',
    Tags=[
        {'Key': 'application', 'Value': 'customer-chatbot'},
        {'Key': 'department',  'Value': 'product'},
        {'Key': 'environment', 'Value': 'production'},
        {'Key': 'cost_center', 'Value': 'CC-4401'},
    ]
)

Your Bedrock application code requires no changes. Tags flow from the IAM principal to billing automatically.

Step 2: Enable IAM principal data in CUR 2.0

CUR 2.0 is required — legacy CUR does not have the line_item_iam_principal column.

In the Billing and Cost Management console, go to Data Exports, open your CUR 2.0 export, and select "Include caller identity (IAM principal) allocation data".

Note: Enabling IAM principal data increases CUR file size because cost line items expand into multiple rows — one per calling identity. For high-volume accounts with many distinct callers, plan for increased Amazon S3 storage costs and consider lifecycle policies for older CUR exports.

Step 3: Activate cost allocation tags (IAM Principal type)

Even if you have application activated as a resource tag, you must separately activate it as an IAM Principal tag.

In Billing Console → Cost Allocation Tags, change the Tag Type filter to "IAM Principal", select your tag keys, and choose Activate.

Common mistake: Filtering by "AWS-generated" or "User-defined" shows resource tags, not IAM Principal tags. You must select "IAM Principal" from the Tag Type dropdown.

After activation, allow up to 24 hours for tags to appear in AWS Cost Explorer and CUR 2.0.

Pattern walkthroughs

Pattern 1: Tag the role (dedicated role per application)

If each application already has its own IAM role, this is the simplest path — tag the role once, make no code changes.

# Application code — no tagging parameters needed
import boto3
bedrock = boto3.client('bedrock-runtime')

response = bedrock.converse(
    modelId="amazon.nova-lite-v1:0",
    messages=[{"role": "user", "content": [{"text": "Summarize this ticket..."}]}],
    inferenceConfig={"maxTokens": 200},
)

Tags flow from the IAM role to CUR automatically. What appears in CUR 2.0:

line_item_iam_principaltags['iamPrincipal/application']line_item_unblended_cost
...assumed-role/my-bedrock-app-role/session-1customer-chatbot$0.0015

Action: Tag your dedicated roles today. Cost data appears within 24 hours.

Pattern 2: Session tags via AssumeRole (shared role, LLM gateway, multi-tenant)

For LLM gateways and multi-tenant platforms using a single shared role, inject per-tenant tags dynamically at the gateway layer. Each tenant gets its own cost line in CUR without requiring separate IAM roles.

import boto3

sts = boto3.client('sts')
GATEWAY_ROLE_ARN = "arn:aws:iam::123456789012:role/my-llm-gateway-role"

def invoke_for_tenant(tenant_id: str, prompt: str) -> str:
    # Assume the shared role with per-tenant session tags
    assumed = sts.assume_role(
        RoleArn=GATEWAY_ROLE_ARN,
        RoleSessionName=f"tenant-{tenant_id}",  # Visible in line_item_iam_principal
        Tags=[
            {'Key': 'tenant',      'Value': tenant_id},
            {'Key': 'application', 'Value': 'llm-gateway'},
        ]
    )
    credentials = assumed['Credentials']
    bedrock = boto3.client(
        'bedrock-runtime',
        aws_access_key_id=credentials['AccessKeyId'],
        aws_secret_access_key=credentials['SecretAccessKey'],
        aws_session_token=credentials['SessionToken'],
    )
    # Bedrock call is unchanged — tags are on the session
    response = bedrock.converse(
        modelId="amazon.nova-lite-v1:0",
        messages=[{"role": "user", "content": [{"text": prompt}]}],
    )
    return response['output']['message']['content'][0]['text']

What appears in CUR 2.0:

line_item_iam_principaltags['iamPrincipal/tenant']line_item_unblended_cost
...assumed-role/my-llm-gateway-role/tenant-customer-acustomer-a$0.0312
...assumed-role/my-llm-gateway-role/tenant-customer-bcustomer-b$0.0087

Session tags and role tags both appear in CUR — you get infrastructure-level attribution (application=llm-gateway) and tenant-level attribution (tenant=customer-a) in the same row.

Note on tag merging: Session tags override role tags with matching keys, and new session tags are added alongside existing role tags. Session tags cannot remove existing role tags — the CUR row carries the union of both.

Action: Add Tags to your existing AssumeRole call at the gateway layer. Each tenant gets its own cost line — no additional IAM roles needed.

Pattern 3: Federated users (Okta, Entra ID, AWS IAM Identity Center)

For teams accessing Bedrock through corporate SSO, user identity flows automatically to line_item_iam_principal via the session name. Add session tags in your IdP configuration for structured project dimensions.

Update the IAM role's trust policy to allow sts:TagSession:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Federated": "arn:aws:iam::123456789012:saml-provider/MyIdP"},
    "Action": ["sts:AssumeRoleWithSAML", "sts:TagSession"],
    "Condition": {
      "StringEquals": {"SAML:aud": "https://signin.aws.amazon.com/saml"}
    }
  }]
}

Configure your IdP to pass PrincipalTag:project and PrincipalTag:team as SAML attributes. Even without tags, the user's SSO identity — for example, alice@example.com — is parseable from line_item_iam_principal.

Patterns 4 and 5: Inference profiles and Bedrock Projects

Application inference profiles let you associate a model configuration with a named resource and get native Amazon CloudWatch metrics per workload. They complement IAM principal tags — use both for multi-dimensional visibility.

Bedrock Projects provide workload-level attribution for Mantle (OpenAI-compatible) API calls. Combine with IAM principal tags for both which workload and who called it.

IAM principal attribution works identically across all Amazon Bedrock APIs — bedrock-runtime (Converse, InvokeModel) and bedrock-mantle (Chat Completions, Responses API). DeepSeek, Qwen, Mistral, and GPT OSS models appear in the same CUR table as Nova and Claude. No separate tracking is needed for OpenAI-compatible endpoints.

Query your cost data in Athena

Once CUR 2.0 is flowing with IAM principal tags, you can answer the questions that matter. Run these queries in the Amazon Athena console.

Before running: Replace your_cur_database.your_cur_table in each query with your actual Athena database and table name. Find it in the Athena console — your database name matches your CUR 2.0 Data Export name. The queries below use your_cur_database.your_cur_table as a placeholder.

Which team is driving Bedrock spend?

SELECT
    tags['iamPrincipal/application']        AS application,
    tags['iamPrincipal/department']         AS department,
    ROUND(SUM(line_item_unblended_cost), 6) AS total_cost,
    ROUND(SUM(line_item_usage_amount), 1)   AS tokens_k
FROM your_cur_database.your_cur_table
WHERE line_item_product_code = 'AmazonBedrock'
    AND tags['iamPrincipal/application'] IS NOT NULL
    AND tags['iamPrincipal/application'] != ''
GROUP BY 1, 2
ORDER BY total_cost DESC

How much does each tenant cost? (chargeback)

SELECT
    tags['iamPrincipal/tenant']             AS tenant,
    ROUND(SUM(line_item_unblended_cost), 6) AS total_cost,
    ROUND(SUM(line_item_usage_amount), 1)   AS tokens_k,
    COUNT(*)                                AS api_calls
FROM your_cur_database.your_cur_table
WHERE line_item_product_code = 'AmazonBedrock'
    AND tags['iamPrincipal/tenant'] IS NOT NULL
    AND tags['iamPrincipal/tenant'] != ''
GROUP BY 1
ORDER BY total_cost DESC

Who on my team is spending, and on which project?

SELECT
    line_item_iam_principal,
    tags['iamPrincipal/project']            AS project,
    tags['iamPrincipal/team']               AS team,
    ROUND(SUM(line_item_unblended_cost), 6) AS cost
FROM your_cur_database.your_cur_table
WHERE line_item_product_code = 'AmazonBedrock'
    AND line_item_iam_principal LIKE '%my-data-science-role%'
GROUP BY 1, 2, 3
ORDER BY cost DESC

Rows where project and team are null represent usage that occurred before tags were activated. This is a useful reminder to activate cost allocation tags before generating traffic.

What is our input vs. output token ratio?

A high output-to-input ratio signals over-generation which is a common source of avoidable cost. If output_tokens_k is more than 5 times input_tokens_k for an application, consider adding max_tokens constraints or using structured output.

SELECT
    tags['iamPrincipal/application']        AS application,
    ROUND(SUM(CASE WHEN LOWER(line_item_usage_type) LIKE '%input%'
                   THEN line_item_usage_amount ELSE 0 END), 2) AS input_tokens_k,
    ROUND(SUM(CASE WHEN LOWER(line_item_usage_type) LIKE '%output%'
                   THEN line_item_usage_amount ELSE 0 END), 2) AS output_tokens_k,
    ROUND(SUM(line_item_unblended_cost), 6)                    AS total_cost
FROM your_cur_database.your_cur_table
WHERE line_item_product_code = 'AmazonBedrock'
    AND tags['iamPrincipal/application'] IS NOT NULL
GROUP BY 1
ORDER BY total_cost DESC

Which models are teams using, and what do they cost?

SELECT
    line_item_usage_type                    AS model_and_token_type,
    ROUND(SUM(line_item_unblended_cost), 6) AS total_cost,
    ROUND(SUM(line_item_usage_amount), 1)   AS tokens_k
FROM your_cur_database.your_cur_table
WHERE line_item_product_code = 'AmazonBedrock'
GROUP BY 1
ORDER BY total_cost DESC

The line_item_usage_type column encodes region, model, and token direction in one string — for example, USE1-NovaLite-output-tokens or USE1-deepseek.v3.2-mantle-output-tokens-standard. Bedrock Mantle models (DeepSeek, Qwen, Mistral, GPT OSS) appear in the same CUR table as bedrock-runtime models.

From visibility to action

Data without action is a report nobody reads. Use your CUR data to drive these conversations:

SignalAction
Output tokens more than 5x input tokensAdd max_tokens limits; use structured output
Zero CacheRead usageImplement prompt caching — up to 90% savings on repeated prefixes
One tenant consuming more than 60% of spendBegin chargeback conversation
Legacy or end-of-life model detectedNotify team; provide upgrade path
Spend growing faster than usageReview model selection; evaluate batch or Flex inference

Set up AWS Budgets scoped to Service = Amazon Bedrock. Budget Actions can automatically apply a deny policy at threshold — not just send a notification. For per-team limits, add rate controls at the LLM gateway layer.

Clean up resources

If you created IAM roles or policies while following this post, remove them from the IAM console when you no longer need them.

The CUR 2.0 export and its Amazon S3 bucket continue to incur storage costs. If you created a new export for testing, delete it from Billing Console → Data Exports and remove the associated S3 bucket from the Amazon S3 console.

Conclusion

We showed how to attribute every Amazon Bedrock inference cost to the IAM principal — team, application, or tenant — that consumed it, using IAM role tags, session tags, and CUR 2.0 with IAM principal data enabled.

The setup takes an afternoon. Within 24 hours you can group AWS Cost Explorer by iamPrincipal/application or run the Athena queries in this post and see exactly who is spending, on which model, and whether that spend is creating value.

To learn more: