Skip to content

Setting Per-Environment Budgets for Dev, Staging and Prod Model Calls

10 min read · updated August 11, 2026

A shared budget alert fires at 80% and tells you nothing about which environment got you there. The fix is a tag, but the obvious place to put it does not work: tagging your ECS service or your Lambda function tags the compute, not the model invocation, and the model invocation is where the money is.

Why a tag on your service does not reach the model call

AWS cost allocation tags attach to resources. An on-demand InvokeModel or Converse call against a foundation model is not a resource — it is a metered API request against a model id that is identical in every environment. The tags on the caller do not propagate to it. So a staging job that loops on a retry bug produces Bedrock usage that is byte-for-byte indistinguishable in the bill from production usage, and a single account-wide budget is the only thing watching it.

There are three real ways out of that, and it is worth naming all three before picking one:

  • Separate accounts per environment. The cleanest boundary and the one that also solves quota isolation and blast radius. Filter budgets on Linked account. If you already have this, stop reading — you are done.
  • An application inference profile per environment. A taggable Bedrock resource that you invoke instead of the raw model id. This is the mechanism this page uses.
  • Attribution outside AWS billing. Your own per-request accounting, which is the only option that works when the provider is not Bedrock at all. Covered in cost attribution.

Application inference profiles carry the tag

Amazon Bedrock documents application inference profiles as a resource you create specifically to track usage and cost, and states that you can “attach tags to an application inference profile to track costs when you submit on-demand model invocation requests” — see the AWS inference profiles documentation. You create one per environment, tag it, and invoke it instead of the model.

The API action is CreateInferenceProfile. It requires inferenceProfileName and modelSource, and takes tags and description optionally. The modelSource names either a foundation model in one Region, or a cross-Region (system-defined) profile if you want the requests spread across Regions. It returns an inferenceProfileArn.

aws bedrock create-inference-profile \
  --inference-profile-name "chat-staging" \
  --description "Staging traffic for the assistant" \
  --model-source '{"copyFrom":"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0"}' \
  --tags Key=Environment,Value=staging Key=Service,Value=assistant

Then the only application change is the model identifier. The profile ARN goes exactly where the model id went, for InvokeModel, InvokeModelWithResponseStream, Converse and ConverseStream alike:

import boto3, os

bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")

response = bedrock.converse(
    modelId=os.environ["INFERENCE_PROFILE_ARN"],   # not the bare model id
    messages=[{"role": "user", "content": [{"text": "Summarise this ticket."}]}],
)

Reading the profile ARN from the environment is what makes this work operationally: the same image runs in dev, staging and production, and the environment variable decides which tagged profile the spend lands against. Hard-coding the ARN reintroduces the problem in a new place.

Activating the cost allocation tag

Tagging a resource does nothing to your bill until the tag key is activated as a cost allocation tag, and this step has a delay that catches people out. AWS documents it plainly: after you apply user-defined tags, “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” — from the AWS guide to activating user-defined cost allocation tags. So plan for up to two days before a tag-filtered budget has anything to show.

Activation is also not retroactive in any useful sense — the tag starts partitioning usage from the point the resources carry it, so spend from before you created the profiles stays unattributed forever. Do this at the start of a cost investigation, not in the middle of one.

aws ce update-cost-allocation-tags-status \
  --cost-allocation-tags-status TagKey=Environment,Status=Active

The budget, filtered on the tag

AWS Budgets supports a Tag filter dimension, and its documentation notes that user-defined tag keys must carry the user: prefix. In the API the filter goes in CostFilters under the TagKeyValue key, with values in the form user:Key$value.

{
  "BudgetName": "bedrock-staging",
  "BudgetType": "COST",
  "TimeUnit": "MONTHLY",
  "BudgetLimit": { "Amount": "200", "Unit": "USD" },
  "CostFilters": {
    "TagKeyValue": ["user:Environment$staging"],
    "Service": ["Amazon Bedrock"]
  }
}
aws budgets create-budget \
  --account-id 111122223333 \
  --budget file://bedrock-staging.json \
  --notifications-with-subscribers file://notify.json

Two figures worth having before you create twenty of these. Amazon states on the AWS Budgets pricing page that you can “monitor and receive notifications on your budgets free of charge”, and that the first two action-enabled budgets are free with each subsequent one costing $0.10 per day. So alert-only budgets are free at any count; the ones that actually do something are not.

Those AWS Budgets prices are what AWS publishes at the time of writing, and they have been revised before. Check the pricing page before assuming a per-environment fleet of action-enabled budgets is free.

The important limitation: a budget is a notification, not a control. It evaluates against billing data that lags real usage by hours, so a runaway loop in staging can spend a great deal before an 80% threshold is crossed. For an actual ceiling you need something that refuses the call — see a hard spend cap on Bedrock and LLM budget controls.

End to end

  1. Create one application inference profile per environment with bedrock create-inference-profile, tagged Environment=dev|staging|prod.
  2. Publish each profile ARN as an environment variable and change the application to pass it as modelId.
  3. Activate the Environment tag key as a cost allocation tag, then wait — up to 24 hours for it to appear and up to 24 more for it to activate.
  4. Create one budget per environment with a CostFilters.TagKeyValue of user:Environment$<value>, scoped to the Amazon Bedrock service so unrelated spend does not muddy it.
  5. Set the non-production thresholds far lower than the production one. A staging budget that alerts at $50 is doing its job; one set to the same value as production is decoration.