Home / AI & GenAI

AI & GenAIHow-toAdvanced

Amazon Bedrock application inference profiles: per-team cost tracking, tagging and access control

Give each team or app its own Bedrock application inference profile, tag it for Cost Explorer, lock IAM to it and alert on its usage, so the Bedrock bill can be split by owner.

Deepak Bhardwaj7 min read

Applies toAmazon Bedrock (on-demand models and cross-Region inference profiles), AWS CLI v2, AWS Cost Explorer

The goal

Every team is calling the same Bedrock model and the bill shows one big Amazon Bedrock line. Finance wants a split by team, and you want to know which app caused last Tuesday's spike without searching invocation logs.

On-demand foundation models can't be tagged. Application inference profiles can. You create a profile per team or app that points at a model (or at a system-defined cross-Region profile), tag it, and have the app pass the profile ARN as its modelId. Usage through that ARN carries the tags into billing.

This post covers creating the profiles, switching apps over, enforcing their use with IAM, and reporting on cost.

Check what you have today

List the system-defined profiles (the us., eu., apac. and global. prefixed ones) and any application profiles that already exist:

aws bedrock list-inference-profiles --type-equals SYSTEM_DEFINED \
  --query 'inferenceProfileSummaries[].[inferenceProfileId,status]' --output table

aws bedrock list-inference-profiles --type-equals APPLICATION \
  --query 'inferenceProfileSummaries[].[inferenceProfileName,inferenceProfileArn,status]' --output table

Find the model ARN or the system-defined profile ARN you will copy from:

aws bedrock get-inference-profile \
  --inference-profile-identifier us.anthropic.claude-sonnet-4-20250514-v1:0 \
  --query '{arn:inferenceProfileArn,models:models[].modelArn}'

Model and profile IDs change with every model release; check your account's output rather than copying IDs from this page.

Model is invoked through Copy from
Plain model ID (single Region, on-demand) arn:aws:bedrock:<region>::foundation-model/<model-id>
Cross-Region profile (us., eu. and so on) The system-defined inference profile ARN

If a model only supports inference profiles, copy from the system-defined profile. Otherwise the application profile will fail with the same "on-demand throughput isn't supported" error as the plain model ID.

Create one profile per team or app

Agree on tag keys before you start. Cost allocation tags are case-sensitive, and Team and team show up as two columns in Cost Explorer.

REGION=us-east-1
SOURCE_ARN=$(aws bedrock get-inference-profile --region $REGION \
  --inference-profile-identifier us.anthropic.claude-sonnet-4-20250514-v1:0 \
  --query inferenceProfileArn --output text)

aws bedrock create-inference-profile --region $REGION \
  --inference-profile-name claims-assistant-prod \
  --description "Claims assistant, production" \
  --model-source copyFrom=$SOURCE_ARN \
  --tags '[{"key":"team","value":"claims"},{"key":"app","value":"claims-assistant"},{"key":"env","value":"prod"}]'

The response returns the new inferenceProfileArn, which looks like arn:aws:bedrock:us-east-1:111122223333:application-inference-profile/<id>. Store it in the app's configuration (Parameter Store, App Config or a Kubernetes ConfigMap), not in code.

As far as we know, application inference profiles are managed through the API and CLI rather than the console. Check the current Bedrock documentation if you prefer to manage them in the console.

Naming and granularity

  • One profile per app and environment is usually right. One per team is too coarse to explain a spike. One per user is too many to manage.
  • Keep dev and prod on separate profiles so load tests don't land on the production cost line.
  • Manage profiles with infrastructure as code (CloudFormation AWS::Bedrock::ApplicationInferenceProfile or the Terraform equivalent in your provider version) so tags stay consistent.

Switch the app to the profile ARN

The profile ARN goes where the model ID used to go. Nothing else changes.

aws bedrock-runtime converse --region us-east-1 \
  --model-id arn:aws:bedrock:us-east-1:111122223333:application-inference-profile/abc123example \
  --messages '[{"role":"user","content":[{"text":"Reply with OK"}]}]' \
  --query 'output.message.content[0].text'
import os, boto3

brt = boto3.client("bedrock-runtime", region_name="us-east-1")
resp = brt.converse(
    modelId=os.environ["BEDROCK_PROFILE_ARN"],
    messages=[{"role": "user", "content": [{"text": "Summarise claim 4411"}]}],
    inferenceConfig={"maxTokens": 400},
)
print(resp["usage"])

Tip

Roll out by switching one environment at a time and comparing the usage block and latency before and after. The model behind the profile is the same, so answers shouldn't change.

Force apps to use their profile with IAM

Tags don't help if an app can still call the raw model ID. Give each app role permission only on its own profile, plus the underlying models the profile routes to.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "InvokeOwnProfile",
      "Effect": "Allow",
      "Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
      "Resource": "arn:aws:bedrock:us-east-1:111122223333:application-inference-profile/abc123example"
    },
    {
      "Sid": "UnderlyingModelsViaProfileOnly",
      "Effect": "Allow",
      "Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
      "Resource": [
        "arn:aws:bedrock:*::foundation-model/anthropic.claude-sonnet-4-20250514-v1:0",
        "arn:aws:bedrock:us-east-1:111122223333:inference-profile/us.anthropic.claude-sonnet-4-20250514-v1:0"
      ],
      "Condition": {
        "StringEquals": {
          "bedrock:InferenceProfileArn": "arn:aws:bedrock:us-east-1:111122223333:application-inference-profile/abc123example"
        }
      }
    }
  ]
}

The Converse and ConverseStream APIs are authorised by the InvokeModel actions. Check the condition key name against the Bedrock service authorization reference for your account. If the policy simulator doesn't recognise it, drop the condition and rely on removing direct model access from the roles instead.

Warning

Cross-Region profiles route requests to several Regions. If the foundation-model resource is pinned to one Region, calls will fail intermittently with AccessDeniedException whenever a request is routed elsewhere. Use * for the Region or list every Region in the profile.

Activate the tags for billing

Tags only reach Cost Explorer after you activate them as cost allocation tags. Run this from the management (payer) account:

aws ce list-cost-allocation-tags --status Inactive \
  --query 'CostAllocationTags[?TagKey==`team` || TagKey==`app`]'

aws ce update-cost-allocation-tags-status \
  --cost-allocation-tags-status TagKey=team,Status=Active TagKey=app,Status=Active TagKey=env,Status=Active

New tag keys can take up to 24 hours to appear in the list, and cost data is only tagged from activation onwards. It is not backfilled.

Report cost by team

aws ce get-cost-and-usage \
  --time-period Start=2026-10-01,End=2026-11-01 \
  --granularity MONTHLY \
  --metrics UnblendedCost \
  --group-by Type=TAG,Key=team \
  --query 'ResultsByTime[0].Groups[].[Keys[0],Metrics.UnblendedCost.Amount]' --output table

Rows keyed team$ (an empty value) are untagged spend: apps still calling raw model IDs, or usage from before activation. Third-party models may be billed under a Marketplace service name rather than "Amazon Bedrock", so group by tag rather than filtering by service until you have checked the service names on your bill.

Alert on a team's usage

Set a budget per team using the cost allocation tag:

aws budgets create-budget --account-id 111122223333 \
  --budget '{"BudgetName":"bedrock-claims","BudgetLimit":{"Amount":"2000","Unit":"USD"},
             "TimeUnit":"MONTHLY","BudgetType":"COST",
             "CostFilters":{"TagKeyValue":["user:team$claims"]}}' \
  --notifications-with-subscribers '[{"Notification":{"NotificationType":"FORECASTED",
      "ComparisonOperator":"GREATER_THAN","Threshold":80,"ThresholdType":"PERCENTAGE"},
      "Subscribers":[{"SubscriptionType":"EMAIL","Address":"claims-oncall@example.com"}]}]'

Budgets run on billing data, which lags by hours. For faster signals, use CloudWatch AWS/Bedrock metrics such as InputTokenCount, OutputTokenCount and Invocations. Check which ModelId dimension values appear for your profile with:

aws cloudwatch list-metrics --namespace AWS/Bedrock --metric-name InputTokenCount \
  --query 'Metrics[].Dimensions[?Name==`ModelId`].Value' --output text

Whether the profile ARN or the underlying model ID shows up there can vary. Build the alarm on whatever value appears for your profile.

Common problems

Symptom Cause Fix
ValidationException on the profile ARN Profile copied from a model ID that needs a cross-Region profile Recreate from the system-defined profile ARN
Intermittent AccessDeniedException IAM allows the model in one Region only Allow every Region the profile routes to
Tags missing in Cost Explorer Not activated, or activated less than 24 h ago update-cost-allocation-tags-status in the payer account
Large team$ (untagged) row Apps still on raw model IDs Remove direct model permissions from app roles
ResourceNotFoundException Profile created in another Region Call the profile from the Region it was created in

Prevent it next time

  • Make "new Bedrock app" mean "new application inference profile with the standard tags", created by a pipeline rather than by hand.
  • Audit monthly: list-inference-profiles --type-equals APPLICATION against your app inventory, and check that the untagged Cost Explorer row is shrinking.
  • When a model retires, create new profiles from the replacement model and switch apps over by changing configuration only. Tags and budgets move with the new profile if you reuse the names and tags.
  • Delete profiles for retired apps with aws bedrock delete-inference-profile --inference-profile-identifier <arn> after their roles are removed.