AWS Builder Center

How to limit usage and cost on Amazon Bedrock

This post compares three approaches to controlling Bedrock costs — critical for agentic development — including CloudWatch Alarms with Lambda for near-real-time IAM-based blocking, AWS Budgets with cost allocation tags for lightweight alerting and forecasting, and an app-layer gateway for synchronous per-user enforcement before a single token is consumed.

The problem

When multiple teams or developers share an AWS account and call Amazon Bedrock, the bill arrives as one lump sum. There is no built-in way to cap how many tokens a team can consume, or to automatically stop a runaway agent before it burns through your budget.
The three options below each solve this differently. They trade off latency, complexity, and how much control you actually get.



Option 1: CloudWatch Alarms + Lambda

Near real-time enforcement. Medium complexity.
This approach uses Bedrock's built-in invocation logging to feed CloudWatch metrics, then reacts to threshold breaches with an alarm that triggers a Lambda.

How it works

Bedrock can write a log entry for every model invocation to a CloudWatch log group. You create a metric filter on that log group to count tokens per team or per Application Inference Profile (AIP). When the metric crosses a threshold — say, 1 million tokens in a day — a CloudWatch Alarm fires, notifies SNS, and SNS triggers a Lambda that takes action.
The action the Lambda takes is up to you. The most effective option is updating an IAM deny policy attached to the team's role. The policy uses a condition on the session tag that was passed at assume-role time:
Python
# The team assumed their role with: Tags=[{"Key": "team", "Value": "data-science"}]
# This deny policy blocks them once they're over budget

doc = {
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Action": "bedrock:*",
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:PrincipalTag/team": ["data-science"] # add teams as they exceed budget
}
}
}]
}
iam.create_policy_version(
PolicyArn=DENY_POLICY_ARN,
PolicyDocument=json.dumps(doc),
SetAsDefault=True,
)
Explicit denies always win in IAM. Once the policy is updated, any new session from that team is blocked — regardless of what allow policies are attached to the role.
The session tag is passed when the developer assumes their team role:
Python
resp = sts.assume_role(
RoleArn="arn:aws:iam::123456789012:role/team-ds-role",
RoleSessionName=f"bob-{timestamp}",
Tags=[{"Key": "team", "Value": "data-science"}],
TransitiveTagKeys=["team"],
)
 TransitiveTagKeys  ensures the tag persists if the session later assumes another role. It flows through to Bedrock invocation logs as  aws:PrincipalTag/team .

Architecture

Plain text
CW Alarm → SNS → Lambda → IAM Policy Update

Enforcement latency

5–15 minutes. CloudWatch metrics aggregate over time, and alarms have a minimum evaluation period. This is not instant, but it is fast enough for most team-level budget controls.

Best for

  • Automated responses to usage spikes
  • Teams needing near-real-time controls
  • Moderate enforcement requirements



Option 2: AWS Budgets + Tags

Daily updates. Low complexity.
This is the lightest-weight option. It uses AWS Budgets to set cost thresholds per AIP using cost allocation tags, and sends alerts when those thresholds are crossed.

How it works

You tag your IAM roles with keys like  team  or  cost-center . AWS Budgets can filter by those tags and create a budget scoped to a specific team's Bedrock spend. When the spend hits 80%, 90%, or 100% of the budget, Budgets sends a notification via SNS or email.
You can also attach a Lambda action to the budget alert for automated responses — for example, posting to Slack, opening a ticket, or triggering the same IAM policy update described in Option 1.
To make IAM principal tags visible in Budgets and Cost Explorer, you need to activate them:
  1. Go to Billing and Cost Management > Cost Allocation Tags
  1. Filter for IAM principal type tags
  1. Select your tag keys and activate them
After activation, tags appear in CUR 2.0 under the prefix  iamPrincipal/team  and can be used as budget filters. Note that it can take up to 24 hours for tags to appear and another 24 hours to activate.
AWS Budgets also supports cost forecasting — it will project whether you are on track to exceed your monthly budget based on current spend rate, and can alert you before you actually hit the limit.

Architecture

Plain text
Budget → Alert → SNS → Email / Lambda

Enforcement latency

Daily. Budget data updates once per day, so this is not suitable for stopping a runaway process in real time. It is a monitoring and alerting tool, not a hard gate.

Best for

  • Cost-based monitoring and forecasting
  • Simple alerting without custom code
  • Budget-conscious organizations that want visibility before enforcement

 Deep dive with implementation instruction 




Option 3: App-Layer Gateway

Real-time enforcement. High complexity.
This approach puts a Lambda or API Gateway in front of every Bedrock call. The application never calls Bedrock directly — it calls your gateway, which checks the budget in DynamoDB and either forwards the request or returns an error.

How it works

Every client routes its Bedrock requests through an API Gateway endpoint backed by a Lambda. Before forwarding to Bedrock, the Lambda looks up the caller's usage in DynamoDB and compares it against their limit:
Python
import boto3, os, json
from decimal import Decimal

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["USAGE_TABLE"])
bedrock = boto3.client("bedrock-runtime")

def handler(event, context):
user = event["requestContext"]["authorizer"]["principalId"]
body = json.loads(event["body"])

# Check current usage against limit
item = table.get_item(Key={"user": user}).get("Item", {})
tokens_used = int(item.get("tokens_used", 0))
token_limit = int(item.get("token_limit", 0))

if token_limit > 0 and tokens_used >= token_limit:
return {
"statusCode": 429,
"body": json.dumps({"error": "Token limit exceeded for this period"})
}

# Forward to Bedrock
response = bedrock.invoke_model(
modelId=body["modelId"],
body=json.dumps(body["payload"]),
)
result = json.loads(response["body"].read())

# Update usage counter
input_tokens = result.get("usage", {}).get("input_tokens", 0)
output_tokens = result.get("usage", {}).get("output_tokens", 0)
table.update_item(
Key={"user": user},
UpdateExpression="ADD tokens_used :total",
ExpressionAttributeValues={":total": Decimal(input_tokens + output_tokens)},
)

return {"statusCode": 200, "body": json.dumps(result)}
Because the check happens before the Bedrock call, enforcement is immediate. There is no window where an over-budget user can sneak through.
The tradeoff is that every request now goes through an extra hop. This adds roughly 50–200ms of latency depending on DynamoDB response time and Lambda cold starts. For interactive applications this is usually acceptable; for high-throughput batch workloads it may not be.

Architecture

Plain text
Client → API GW → Lambda → DDB check → Bedrock

Enforcement latency

Real-time (milliseconds). The limit is checked synchronously before every call.

Best for

  • Multi-tenant SaaS applications
  • Strict real-time usage quotas
  • Complex quota logic (daily, weekly, monthly windows)

3rd Party Solution

  • Few fully-built solutions are included and can be purchased using AWS Marketplace (e.g., LiteLLM LLM Gateway)



Choosing the right option


CloudWatch + Lambda
AWS Budgets + Tags
App-Layer Gateway
Enforcement latency
5–15 minutes
Daily
Real-time (ms)
Complexity
Medium
Low
High
Hard blocking
Yes (IAM deny)
Optional (via Lambda)
Yes (HTTP 429)
Cost forecasting
No
Yes
No
Code required
Yes
No
Yes
Best for
Team-level controls
Visibility and alerting
Per-user SaaS quotas
Start with AWS Budgets + Tags if you need visibility without writing code. Move to CloudWatch + Lambda if you need automated blocking at the team level. Use the App-Layer Gateway only if you need per-user real-time enforcement or are building a multi-tenant product.



Open Source Solutions:




Key takeaways

  • Session tags passed at  sts:AssumeRole  time flow through to invocation logs and IAM conditions, making them the connective tissue for identity-based controls
  • A single shared IAM deny policy with a  StringEquals  condition on  aws:PrincipalTag  can block any number of teams without per-user policy changes
  • AWS Budgets is the lowest-friction starting point — no code, integrates with Cost Explorer, supports forecasting
  • The app-layer gateway enforces limits synchronously before a token is consumed



References

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