GenAI Cost Governance Dashboard

Tag Bedrock costs by team and alert on spend spikes with CloudWatch.

Introduction

30 Second Summary

When multiple teams start calling AI models, the monthly cloud bill climbs fast. The harder question is figuring out which team is responsible when the invoice shows one flat number with no breakdown.

In this project, you will build a cost governance system on AWS that attributes every Amazon Bedrock API call to a specific team. You'll publish token usage as real-time CloudWatch metrics and configure anomaly detection to catch spend spikes before they hit the invoice.

What You'll Build

Picture opening your CloudWatch dashboard and seeing each team's AI token usage graphed in real time, with an alarm already firing because one team just spiked.

By the end of this project, you'll have:

  • Tagged Bedrock calls that attribute every token to the team that used it, turning an opaque bill into a per-team cost breakdown.
  • A live CloudWatch dashboard showing per-team input and output token trends in real time.
  • An anomaly detection alarm that learns normal usage patterns and alerts you when a team's token consumption spikes beyond its baseline.
  • Secret Mission: Add an AWS Budget guard rail that automatically restricts Bedrock access when costs cross a threshold.

Are there any prerequisites?

This project assumes you have an AWS account with Amazon Bedrock model access enabled for Claude Haiku 4.5. Familiarity with the AWS Console, Lambda, and Bedrock basics (from Projects 1-3 in this series or equivalent) is recommended.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. Your company's Amazon Bedrock costs tripled last quarter, and nobody can explain which team drove the spike.

Set Up Your Environment

Your cost governance system will use the AWS CLI to create infrastructure and Python to power a Lambda function that tracks token usage. Before any of that, you need to confirm that these tools are installed and that your Amazon Bedrock model access is ready.

In this step, you'll verify the AWS CLI, Python, and Bedrock model access so that every later step can run without surprises.

In this step, get ready to:
  • Confirm the AWS CLI is installed and configured with valid credentials.
  • Verify Python 3 is installed.
  • Confirm Claude Haiku 4.5 model access is granted in the Bedrock console.
Verify your AWS CLI installation

The AWS CLI lets you create and manage AWS resources directly from your terminal. You need it configured with credentials that can access your AWS account.

  • Check your AWS CLI installation by running this command:
aws sts get-caller-identity

What do you see?

✔️ I see my account details

You should see a JSON response containing your Account, UserId, and Arn. Your AWS CLI is configured and ready to go.

ⓧ I see a credentials error

Your AWS CLI is installed but not configured with credentials.

  • Run the interactive configuration command:
aws configure
  • Enter your AWS Access Key ID when prompted.
  • Enter your Secret Access Key when prompted.
  • Enter your default region (e.g. us-east-1).
  • Press Enter to accept the default output format.
  • Run aws sts get-caller-identity again to confirm you see your account details.

Where do I find my access keys?

Access keys are created in the IAM console under your user's Security credentials tab. If your organization uses IAM Identity Center, ask your admin for the sign-in URL and temporary credentials.

ⓧ Command not found

The AWS CLI is not installed on your machine.

  • Install the AWS CLI by following the official installation guide for your operating system.
  • After installation, close and reopen your terminal.
  • Run aws sts get-caller-identity again to confirm the CLI is installed and configured.

Configure after installing

If the command runs but shows a credentials error after installation, follow the steps in the I see a credentials error tab above to set up your credentials.

Verify Python 3 is installed

The Lambda function you'll build later runs on a Python runtime. Confirm that Python 3 is available on your machine.

  • Check your Python version by running this command:
python3 --version

What do you see?

✔️ I see a version number

You should see something like Python 3.x.x. Python is ready.

ⓧ Command not found

Python 3 is not installed on your machine.

  • Download and install the latest Python 3 release from python.org/downloads.
  • During installation on Windows, check the Add python.exe to PATH checkbox.
  • Close and reopen your terminal after installation.
  • Run python3 --version again to confirm you see a version number.

Still not working on Windows?

On some Windows setups, the command is python --version (without the 3). Try that instead. If neither works, make sure Python was added to your system PATH during installation.

Help me get Python 3 working on my machine.

Confirm Claude Haiku 4.5 model access in Bedrock

Bedrock requires you to explicitly enable access for each model you want to use. Your project uses Claude Haiku 4.5, so you need to confirm it is granted before making any API calls.

  • Sign into the AWS Management Console at console.aws.amazon.com.
  • Search for Bedrock in the top search bar and open the Amazon Bedrock service.
  • In the left sidebar, click Model access.
  • Look for the model anthropic.claude-haiku-4-5-20251001-v1:0 in the list.
  • Confirm its status shows as Access granted.

What do you see?

✔️ I see Access granted

Claude Haiku 4.5 is ready for your project. No further action needed.

ⓧ Access is not granted

You need to request access before you can use this model.

  • Click Modify model access at the top of the page.
  • Select the checkbox for Claude Haiku 4.5 (anthropic.claude-haiku-4-5-20251001-v1:0).
  • Click Next, then review the terms and click Submit.
  • Wait for the status to change to Access granted. This usually takes a few seconds.

Access still not granted?

Some accounts need additional approval before model access is granted. If the status stays pending, check with your AWS account administrator.

Help me get Bedrock model access enabled.

Why Claude Haiku 4.5?

Claude Haiku 4.5 is the current generally available Haiku model from Anthropic. The older Claude 3 Haiku (anthropic.claude-3-haiku-20240307-v1:0) reaches end of life on September 10, 2026, so this project uses the supported model.

Confirm your environment is ready

All three prerequisites are in place. One final check confirms your AWS CLI can talk to the Bedrock service directly.

Before you run the next command, take a guess: what do you think you'll see if you list inference profiles when none have been created yet?

  • Run this command to list any existing Bedrock inference profiles:
aws bedrock list-inference-profiles --output table

You should see an empty table or a table listing any existing profiles. Either result means the CLI is communicating with Bedrock successfully.

Seeing an access denied or region error?

Make sure you are using a region where Bedrock and inference profiles are supported (for example, us-east-1). You can set your default region by running aws configure and updating the region value.

Also confirm your IAM user or role has permissions for bedrock:ListInferenceProfiles.

Help me fix my Bedrock CLI access.

Your environment is locked and loaded. Next up, you'll make some untagged Bedrock calls and see the billing blind spot for yourself.

See the Billing Blind Spot

In Step 1, you confirmed your AWS CLI, Python, and Bedrock model access are ready. Your tools are in place.

Imagine three teams at your company all calling Amazon Bedrock to power different AI features. At the end of the month, the bill shows a single charge for Bedrock with no way to tell who spent what. In this step, you will experience that blind spot firsthand.

In this step, get ready to:
  • Make untagged Bedrock calls to simulate different teams using the same model.
  • Open Cost Explorer and see the undifferentiated billing line item that makes team-level attribution impossible.
Make untagged Bedrock calls

To feel the billing blind spot, you need to generate some real Bedrock usage. You will send a few different prompts to Claude Haiku 4.5 to simulate what multiple teams would look like in practice.

  • In the AWS Management Console, type Bedrock in the top search bar and open Amazon Bedrock.
  • In the left navigation pane, choose Chat / Text under Playgrounds.
  • Click Select model.
  • Choose Anthropic as the provider.
  • Select Claude Haiku 4.5 as the model.
  • Click Apply.

The model name appears in the playground header and the chat interface is ready for input.

  • Enter this prompt to simulate a data science team request:
Summarize the key trends in machine learning research for 2026 in three bullet points.
  • Click Run.

You should see Claude Haiku 4.5 respond with a summary. This is your first Bedrock invocation.

  • Clear the conversation, then enter a second prompt to simulate an engineering team request:
Explain three best practices for optimizing database query performance.
  • Click Run.

Another response appears. Two teams, two different questions, same model.

  • Clear the conversation one more time, then enter a third prompt to simulate a product team request:
Draft a short product description for an AI-powered customer support chatbot.
  • Click Run.

You now have three Bedrock invocations from three different "teams." Each one used the same Claude Haiku 4.5 model with no tags or metadata to distinguish them.

Why three separate prompts?

Each prompt simulates a different team asking a different kind of question. In a real company, the Data Science team might run summarization tasks, Engineering might ask about performance tuning, and Product might generate marketing copy.

The key point is that all three requests hit the same model. The bill treats them identically.

Not seeing a response?

Confirm that Claude Haiku 4.5 shows as Access granted on the Model access page in the Bedrock console. If access is not granted, request it and wait for approval before retrying.

Help me troubleshoot why my Bedrock playground is not returning a response from Claude Haiku 4.5.

See the blind spot in Cost Explorer

You just made three Bedrock calls on behalf of three different teams. Now check what the billing system actually recorded about those calls.

  • Open the Billing and Cost Management console by searching for Billing in the top search bar of the AWS Management Console.
  • In the navigation pane, choose Cost Explorer.
  • Click Explore costs in the upper-right corner to open the Cost Explorer report page.

First time opening Cost Explorer?

If this is your first time using Cost Explorer, AWS may prompt you to enable it. Click the enable button and wait a few moments for the data to load.

Cost Explorer can take up to 24 hours to populate historical data, but recent charges typically appear sooner.

  • In the Filters section, choose Service.
  • Start typing Bedrock and select Amazon Bedrock from the list.
  • Click Apply filters.

Before you look at the chart, think about what you expect to see. Do you think Cost Explorer will show you which team made which call?

The chart shows a single line item for Amazon Bedrock. All three calls are lumped together with no breakdown by team, prompt type, or use case.

Why is there no team breakdown?

When you call Bedrock directly through the playground or API, the request carries no team metadata. AWS knows which account made the call, but it has no way to distinguish which internal team or project triggered it.

This is the exact problem you are solving in the rest of this project. In the next step, you will create Application Inference Profiles that attach cost allocation tags to each team's calls, making every token attributable.

Cost Explorer data delay

If your Bedrock charges have not appeared yet, that is normal. Cost Explorer data can take several hours to reflect new usage. The important observation is that even when the data does appear, it shows a single undifferentiated Amazon Bedrock line item with no team dimension.

You have experienced the billing blind spot firsthand. Three teams, three use cases, one undifferentiated line item. Next up, you will fix this by creating Application Inference Profiles that tag every Bedrock call by team.

Tag Bedrock Calls by Team

You have seen the problem firsthand. Every Bedrock call you made shows up as a single, undifferentiated line item in Cost Explorer. No team names. No way to figure out who is driving costs.

The root cause is straightforward. Bedrock calls carry no team metadata by default. Application Inference Profiles solve this by wrapping a foundation model with cost allocation tags. Every call routed through a profile gets tagged automatically, and those tags flow directly into Cost Explorer.

In this step, get ready to:
  • Create two Application Inference Profiles, one per team, each tagged with a Team value.
  • Activate the Team cost allocation tag in the Billing console.
  • Verify both profiles are active and ready to use.
Create the DataScience inference profile

An Application Inference Profile acts as a tagged wrapper around a foundation model. You create one per team, and every Bedrock call routed through that profile inherits the team's tag for billing purposes.

What is an Application Inference Profile?

Bedrock offers two types of inference profiles. System-defined profiles handle cross-region routing and are created by AWS. Application inference profiles are ones you create yourself to track costs and usage.

By pointing an application inference profile at a foundation model and tagging it with a team name, every API call made through that profile gets attributed to that team in your cost reports.

  • Create the DataScience profile by running this command in your terminal:
aws bedrock create-inference-profile \
  --inference-profile-name DataScienceProfile \
  --model-source copyFrom=arn:aws:bedrock:[[AWS_REGION="your AWS region e.g. us-east-1"]]::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0 \
  --tags Key=Team,Value=DataScience

What does this command do?

  • --inference-profile-name DataScienceProfile gives the profile a human-readable name that identifies the Data Science team.
  • --model-source copyFrom=arn:aws:bedrock:... points the profile at the Claude Haiku 4.5 foundation model ARN in your region.
  • --tags Key=Team,Value=DataScience attaches a cost allocation tag so every call through this profile is attributed to the DataScience team.

You should see a JSON response containing an inferenceProfileArn and "status": "ACTIVE".

  • Copy the inferenceProfileArn value from the response and save it somewhere safe. You will need it in a later step.

Seeing an access denied error?

  • Make sure your AWS CLI credentials have the bedrock:CreateInferenceProfile permission.
  • Confirm that Claude Haiku 4.5 model access is enabled in the Bedrock console under Model access.
  • Double-check that the region in the foundation model ARN matches your CLI's configured region. Run aws configure get region to check.
  • Help me troubleshoot this Bedrock inference profile creation error
Create the Engineering inference profile

The second profile follows the same pattern, with a different name and team tag.

  • Create the Engineering profile by running this command:
aws bedrock create-inference-profile \
  --inference-profile-name EngineeringProfile \
  --model-source copyFrom=arn:aws:bedrock:[[AWS_REGION="your AWS region e.g. us-east-1"]]::foundation-model/anthropic.claude-haiku-4-5-20251001-v1:0 \
  --tags Key=Team,Value=Engineering

You should again see a JSON response with inferenceProfileArn and "status": "ACTIVE".

  • Copy this second inferenceProfileArn and save it alongside the first one. You now have two profile ARNs ready.

Second profile also failing?

  • If the first profile succeeded but the second fails, check that you didn't accidentally reuse the same --inference-profile-name. Profile names must be unique.
  • Help me debug why my second inference profile creation is failing
Activate the Team cost allocation tag

The profiles are created, but the Team tag won't appear in Cost Explorer until you explicitly activate it as a cost allocation tag in the Billing console.

  • In the AWS Management Console, search for Billing in the top search bar and open the AWS Billing and Cost Management console.
  • In the left navigation pane, click Cost allocation tags.
  • Click the User-Defined Cost Allocation Tags tab.
  • Find and select the checkbox next to the Team tag key.
  • Click Activate.

Why do I have to activate the tag separately?

AWS requires you to explicitly activate cost allocation tags before they appear in Cost Explorer and the Cost and Usage Report. This prevents accidental clutter from every tag ever applied to a resource showing up in your billing data.

Don't see the Team tag yet?

After you apply tags to resources, it can take up to 24 hours for the tag keys to appear on the cost allocation tags page. If the Team tag doesn't show up yet, wait a few hours and check again. The profiles are still active and usable in the meantime.

Cost allocation tags take up to 24 hours

Even after activation, the Team tag takes up to 24 hours to appear in Cost Explorer. That is far too slow for a hands-on project. In the next step, you will build real-time CloudWatch monitoring that shows token usage within minutes.

Both profiles are created and the tag is activated. Before you move on, confirm everything is in place.

Before you run the next command, think about this: will the output show only your two profiles, or will you see others as well?

  • Verify both profiles by running this command:
aws bedrock list-inference-profiles --output table

The table includes both system-defined profiles (created by AWS for cross-region routing) and your application profiles. Look for DataScienceProfile and EngineeringProfile in the output. Both should show a status of ACTIVE and a type of APPLICATION.

Don't see your profiles in the list?

  • The table may be long. Scroll through or pipe the output to grep to filter: aws bedrock list-inference-profiles --output text | grep DataScienceProfile.
  • If the profiles are missing entirely, re-run the create commands from the earlier substeps and check for errors in the response.
  • Help me find my Application Inference Profiles in the Bedrock list output

Your Bedrock calls now carry team metadata. Every request routed through these profiles will be tagged for cost attribution. Next up, you will build a Lambda function that routes calls through these profiles and publishes token usage as real-time CloudWatch metrics.

Build the Token Tracking Lambda

You activated cost allocation tags in the last step, but those tags take up to 24 hours to appear in Cost Explorer. That is too slow when you need to know right now which team is burning through tokens.

In this step, you will build a Lambda function that calls Bedrock through each team's inference profile and publishes token usage as custom CloudWatch metrics. These metrics show up within minutes, giving you real-time per-team visibility.

In this step, get ready to:
  • Create a Lambda function with the right permissions to call Bedrock and publish CloudWatch metrics.
  • Write the Lambda code that loops through each team's inference profile, calls Bedrock, and publishes token counts.
  • Test the Lambda and verify custom metrics appear in CloudWatch.
Create the Lambda function and configure its permissions

Your Lambda function needs three things: the Python 3.13 runtime (which includes boto3 out of the box), an execution role that can call Bedrock and publish CloudWatch metrics, and the inference profile ARNs from Step 3.

  • Open the Functions page of the Lambda console at https://console.aws.amazon.com/lambda/home#/functions.
  • Click Create function.
  • Select Author from scratch.
  • For Function name, enter BedrockCostGovernance.
  • For Runtime, choose Python 3.13.
  • Leave Architecture set to x86_64.
  • Under Permissions, expand Change default execution role and leave Create a new role with basic Lambda permissions selected.
  • Click Create function.

Lambda creates the function and a default execution role with AWSLambdaBasicExecutionRole attached. You should see the function's Code tab with a built-in code editor.

Why a default role first?

Lambda auto-creates a role with basic CloudWatch Logs permissions. You will add the Bedrock and CloudWatch metrics permissions next as an inline policy on this same role.

Now add the two permissions your function needs beyond basic logging.

  • At the top of the function page, find the Configuration tab and click it.
  • In the left sidebar, click Permissions.
  • Under Execution role, click the role name link. This opens the role in the IAM console in a new tab.
  • On the role's Permissions tab, click Add permissions, then choose Create inline policy.
  • Select the JSON tab and replace the contents with this policy:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "bedrock:InvokeModel"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudwatch:PutMetricData"
            ],
            "Resource": "*"
        }
    ]
}

What does this policy do?

This inline policy grants two permissions. bedrock:InvokeModel lets the function call the Bedrock Converse API through your inference profiles. cloudwatch:PutMetricData lets it publish custom metrics to CloudWatch.

  • Click Next.
  • For Policy name, enter BedrockAndCloudWatchMetrics.
  • Click Create policy.

You should see the new inline policy listed alongside AWSLambdaBasicExecutionRole on the role's Permissions tab.

Switch back to the Lambda console tab to set the timeout and environment variables.

  • In the Lambda console, click the Configuration tab, then click General configuration in the left sidebar.
  • Click Edit.
  • Change the Timeout to 0 min 30 sec.
  • Click Save.

Why 30 seconds?

The default Lambda timeout is 3 seconds. Your function makes two Bedrock API calls in sequence, and each one can take several seconds to return. A 30-second timeout gives enough room for both calls to complete.

  • In the left sidebar, click Environment variables.
  • Click Edit.
  • Click Add environment variable.
  • For Key, enter DATASCI_PROFILE_ARN. For Value, paste the ARN of your DataScience inference profile from Step 3.
  • Click Add environment variable again.
  • For Key, enter ENGINEERING_PROFILE_ARN. For Value, paste the ARN of your Engineering inference profile from Step 3.
  • Click Save.

You should see both environment variables listed with their ARN values.

Don't see both environment variables?

Make sure you clicked Add environment variable for each one separately. If you only see one, click Edit again and add the missing entry.

Double-check that the keys are exactly DATASCI_PROFILE_ARN and ENGINEERING_PROFILE_ARN with no extra spaces.

Still stuck? Help me configure my Lambda environment variables for Bedrock inference profiles.

Write the Lambda code

With the function configured, you can write the code that calls Bedrock and publishes token metrics to CloudWatch.

  • Click the Code tab in the Lambda console.
  • In the built-in code editor, select lambda_function.py and delete all of the existing code.
  • Paste the following imports and configuration code into the empty editor:
import boto3
import json
import os

# Create clients for Bedrock and CloudWatch
bedrock_runtime = boto3.client("bedrock-runtime")
cloudwatch = boto3.client("cloudwatch")

# Map each team to its inference profile ARN from environment variables
TEAM_PROFILES = {
    "DataScience": os.environ["DATASCI_PROFILE_ARN"],
    "Engineering": os.environ["ENGINEERING_PROFILE_ARN"],
}

# Sample prompts that simulate each team's typical workload
PROMPTS = {
    "DataScience": "Summarize the key trends in machine learning research for 2026 in three bullet points.",
    "Engineering": "Explain three best practices for optimizing database query performance.",
}

What does this code do?

The TEAM_PROFILES dictionary reads the inference profile ARNs you just saved as environment variables. Each key is a team name that becomes a CloudWatch dimension later.

The PROMPTS dictionary gives each team a distinct sample prompt. Different prompts generate different token counts, which makes the dashboard more interesting to look at.

  • Below the PROMPTS dictionary, add the handler function that calls Bedrock and extracts token usage:


def lambda_handler(event, context):
    results = {}

    # Loop through each team and call Bedrock via their inference profile
    for team, profile_arn in TEAM_PROFILES.items():
        response = bedrock_runtime.converse(
            modelId=profile_arn,
            messages=[
                {
                    "role": "user",
                    "content": [{"text": PROMPTS[team]}],
                }
            ],
            inferenceConfig={"maxTokens": 256},
        )

        # Extract token usage from the response
        usage = response["usage"]
        input_tokens = usage["inputTokens"]
        output_tokens = usage["outputTokens"]
        total_tokens = usage["totalTokens"]

What does this code do?

The lambda_handler loops through each team. For each team, it calls bedrock_runtime.converse() using that team's inference profile ARN as the modelId. This is what makes the cost allocation tags work. Bedrock sees the call as coming through the tagged profile.

The response includes a usage object with inputTokens, outputTokens, and totalTokens counts. These are the numbers you will publish to CloudWatch next.

  • Still inside the for loop (indented at the same level as the usage lines), add the code that publishes metrics and returns results:
        # Publish per-team token metrics to CloudWatch
        cloudwatch.put_metric_data(
            Namespace="BedrockCostGovernance",
            MetricData=[
                {
                    "MetricName": "InputTokens",
                    "Dimensions": [{"Name": "Team", "Value": team}],
                    "Value": input_tokens,
                    "Unit": "Count",
                },
                {
                    "MetricName": "OutputTokens",
                    "Dimensions": [{"Name": "Team", "Value": team}],
                    "Value": output_tokens,
                    "Unit": "Count",
                },
                {
                    "MetricName": "TotalTokens",
                    "Dimensions": [{"Name": "Team", "Value": team}],
                    "Value": total_tokens,
                    "Unit": "Count",
                },
            ],
        )
        results[team] = {
            "inputTokens": input_tokens,
            "outputTokens": output_tokens,
            "totalTokens": total_tokens,
        }
    return {"statusCode": 200, "body": json.dumps(results)}

What does this code do?

The put_metric_data call publishes three metrics to a custom namespace called BedrockCostGovernance. Each metric includes a Team dimension so CloudWatch can separate DataScience usage from Engineering usage.

The function finishes by returning a JSON summary of each team's token counts. You will see this output when you test the function.

  • In the DEPLOY section above the code editor, click Deploy.

You should see a green banner confirming that the function was updated successfully.

Seeing a deployment error?

Check that all the code blocks are pasted in order. The lambda_handler function should sit below the PROMPTS dictionary with a blank line separating them. The put_metric_data call and results assignment should be indented inside the for loop (8 spaces of indentation). The return statement should be at the function level (4 spaces).

Need help? Help me fix a deployment error in my Lambda function code.

✔️ Awesome, I've got everything!

Double check you have saved and deployed your code with the green success banner showing.

ⓧ I'd like to double check the full code

Scroll up and confirm each of the three code chunks is present in this order:

  • Imports, bedrock_runtime and cloudwatch clients, TEAM_PROFILES dictionary, PROMPTS dictionary.
  • The lambda_handler function definition with the for loop, bedrock_runtime.converse() call, and usage extraction.
  • The cloudwatch.put_metric_data() call, results dictionary, and the return statement.

Make sure there are no duplicate imports or function definitions. The return statement should be the last line of lambda_handler, indented at 4 spaces.

Test the Lambda and verify custom metrics

Time to run the function and confirm it publishes metrics to CloudWatch.

  • Click the Test tab in the Lambda console.
  • For Event name, enter CostTest.
  • Leave the default event JSON as-is. The function does not read the event payload.
  • Click Test.

Before you look at the result, what do you expect the response to contain?

You should see a green Execution result: succeeded banner. The response body contains a JSON object with DataScience and Engineering keys, each showing inputTokens, outputTokens, and totalTokens values.

Seeing an AccessDeniedException?

This means the Lambda execution role is missing the bedrock:InvokeModel permission. Go back to the IAM console, find the role, and check that the inline policy you created includes bedrock:InvokeModel with Resource set to "*".

If you see a timeout error instead, confirm the timeout is set to 30 seconds under Configuration > General configuration.

Still stuck? Help me debug my Lambda function invocation error.

You need more data points for the dashboard you will build in the next step.

  • Click Test three more times, waiting for each invocation to complete before clicking again.

Each successful invocation publishes six metric data points (three metrics for two teams). After four total invocations, you have 24 data points in CloudWatch.

Now verify the metrics are visible in CloudWatch. Do you expect to see them immediately, or will there be a delay?

  • Open the CloudWatch console at https://console.aws.amazon.com/cloudwatch/.
  • In the left sidebar, click All metrics under Metrics.
  • Under Custom namespaces, click BedrockCostGovernance.
  • Click Team to see the dimension.

You should see InputTokens, OutputTokens, and TotalTokens listed for both DataScience and Engineering team values. Custom metrics typically appear within one to two minutes of the first data point being sent.

Why is this faster than Cost Explorer?

Cost allocation tags take up to 24 hours to appear in Cost Explorer. Custom CloudWatch metrics appear within minutes. By publishing token counts directly from your Lambda, you get near-real-time visibility into per-team usage without waiting for the billing pipeline.

Don't see BedrockCostGovernance in Custom namespaces?

Wait two minutes and refresh the page. Custom namespaces only appear after CloudWatch receives the first data point.

If it still does not appear, go back to the Lambda console and check that the last invocation succeeded. Look at the function's Monitor tab for error logs that mention cloudwatch:PutMetricData permissions.

Need help? Help me find my custom CloudWatch metrics for BedrockCostGovernance.

Your Lambda is publishing per-team token metrics to CloudWatch in near real time. Next up, you will turn these raw numbers into a visual dashboard and add an anomaly detection alarm that catches usage spikes.

Visualize and Detect Anomalies

Your Lambda function is publishing per-team token metrics to CloudWatch every time it runs. That data pipeline is sorted.

Raw metrics sitting in CloudWatch are data, not insight. In this step, you will build a dashboard that turns those numbers into at-a-glance visibility across teams, then wire up an anomaly detection alarm that uses machine learning to catch spend spikes before they hit your invoice.

In this step, get ready to:
  • Create a CloudWatch dashboard showing per-team token usage trends.
  • Set up an SNS topic and an anomaly detection alarm on token usage.
  • Simulate a usage spike and verify the alarm fires.
Create the CloudWatch dashboard

A dashboard collects your custom metrics into graphs you can glance at instead of hunting through raw namespace data. You will create one with three widgets covering input tokens, output tokens, and total tokens by team.

  • In the AWS Console search bar, type CloudWatch and open the CloudWatch service.
  • In the left sidebar, click Dashboards.
  • Click Create dashboard.
  • Enter BedrockCostDashboard as the dashboard name.
  • Click Create dashboard to confirm.

CloudWatch immediately opens the widget editor. You will add your first widget for input token tracking.

  • Select Line as the widget type.
  • Select Metrics as the data source.
  • In the metric browser, click Custom Namespaces, then select BedrockCostGovernance.
  • Click the Team dimension.
  • Check the boxes for InputTokens for both DataScience and Engineering.
  • Click Create widget.

You should see a line graph appear on your dashboard showing input token data points for both teams.

What do these metrics represent?

Each data point on the graph corresponds to one Lambda invocation. InputTokens tracks how many tokens went into each Bedrock call. OutputTokens tracks what came back. TotalTokens is the sum of both, giving you a single cost-proxy number.

Now add two more widgets for output tokens and total tokens.

  • Click the + button or Add widget at the top of the dashboard.
  • Repeat the same steps: select Line widget, Metrics data source, navigate to Custom Namespaces > BedrockCostGovernance > Team, but this time select OutputTokens for both teams.
  • Click Create widget.
  • Add one more Line widget. This time select TotalTokens for both DataScience and Engineering to show total token usage across both teams in a single graph.
  • Click Save in the top right corner of the dashboard.

Your dashboard should now display three line graph widgets. Each one shows data points from the Lambda invocations you ran in the previous step, broken down by the Team dimension.

Widgets showing no data?

  • Check the time range in the top right of the dashboard. Switch it to 1h or 3h to make sure it covers the period when you invoked your Lambda function.
  • Make sure you selected metrics from the BedrockCostGovernance custom namespace, not a default AWS namespace.
  • Verify the Team dimension is selected, not Metrics with no dimensions.

Need more help? Help me troubleshoot why my CloudWatch dashboard widgets show no data.

Set up anomaly detection with SNS alerts

A dashboard shows you what is happening right now. An anomaly detection alarm watches the data for you and sounds the alarm when something deviates from the learned pattern. Before creating the alarm, you need a notification channel to deliver the alert.

  • In the AWS Console search bar, type SNS and open the Simple Notification Service.
  • In the left sidebar, click Topics.
  • Click Create topic.
  • Select Standard as the type.
  • Enter BedrockCostAlerts as the topic name.
  • Scroll down and click Create topic.

The topic details page appears. Now subscribe your email address so you receive alerts.

  • Click Create subscription.
  • Under Protocol, select Email.
  • In the Endpoint field, enter your email address.
  • Click Create subscription.
  • Open your email inbox and find the confirmation email from AWS Notifications.
  • Click the Confirm subscription link in the email.

Back in the SNS console, your subscription status should show Confirmed. Your notification channel is ready.

What is SNS?

SNS (Simple Notification Service) is a managed messaging service. You create a topic, subscribe endpoints to it (email, SMS, Lambda, and more), and any AWS service can publish messages to that topic. Every subscriber receives the message.

Don't see the confirmation email?

  • Check your spam or junk folder. The email comes from no-reply@sns.amazonaws.com.
  • Make sure you typed your email address correctly in the Endpoint field. If it was wrong, delete the subscription and create a new one.

Still stuck? Help me troubleshoot my SNS email subscription confirmation.

With the notification channel in place, you can now create the anomaly detection alarm. This alarm uses machine learning to establish a baseline of normal token usage and alerts you when usage deviates.

  • Return to the CloudWatch console.
  • In the left sidebar, click Alarms, then All alarms.
  • Click Create alarm.
  • Click Select metric.
  • Navigate to Custom Namespaces > BedrockCostGovernance > Team.
  • Select TotalTokens for the DataScience team.
  • Click Select metric to confirm your choice.

You are now on the Conditions page. This is where you tell CloudWatch to use anomaly detection instead of a static threshold.

  • Under Threshold type, select Anomaly detection.
  • Set the condition to alarm when the metric is Greater than the band.
  • Set the Anomaly detection threshold to 2 (this means 2 standard deviations from the learned baseline).
  • Click Next.
  • Under Notification, select In alarm as the trigger.
  • Choose Select an existing SNS topic and pick BedrockCostAlerts from the dropdown.
  • Click Next.
  • Enter BedrockTotalTokensAnomaly as the alarm name.
  • Click Next to review, then click Create alarm.

Your alarm should now appear in the All alarms list with a state of Insufficient data. This is normal. The ML model is still building its baseline from the few data points it has.

How does anomaly detection work?

A static threshold alarm fires when a metric crosses a fixed number (for example, more than 500 tokens). An anomaly detection alarm is smarter. It uses machine learning to study your metric over time, learns the expected range, and builds a gray band around it.

When data points break out of that band by more than 2 standard deviations, the alarm fires. This means it adapts to your real usage patterns instead of relying on a guess.

What does this alarm cost?

Each anomaly detection alarm counts as 3 alarm metrics (the actual metric plus the upper and lower bands). At $0.10 per standard resolution alarm metric per month, that is $0.30/month. The CloudWatch free tier includes 10 alarm metrics, so this alarm fits within free tier if you have no other alarms in the account.

Simulate a spike and verify the alarm

The anomaly detection band is narrow right now because the model only has a few data points. A burst of rapid invocations should push the TotalTokens metric outside the band and trigger your alarm.

  • Switch back to the Lambda console and open your BedrockCostGovernance function.
  • Click the Test tab.
  • Invoke the function five or six times in quick succession by clicking Test repeatedly. Wait for each invocation to complete before clicking again.

Why multiple rapid invocations?

Your earlier invocations established a slow, steady pattern. Firing five or six calls back to back creates a sudden cluster of data points that the ML model has never seen. That cluster pushes TotalTokens above the anomaly band and trips the alarm.

Before you check the alarm, what do you think its state will be after that burst of invocations? Will it still show Insufficient data, stay in OK, or transition to In alarm?

  • Wait two to three minutes for CloudWatch to evaluate the new data points.
  • Return to the CloudWatch console.
  • In the left sidebar, click Alarms, then All alarms.
  • Look for your BedrockTotalTokensAnomaly alarm. Its state should show In alarm (highlighted in red).
  • Check your email inbox. You should have received an alarm notification from AWS with the subject line containing your alarm name.

The alarm transitioned to In alarm because the rapid burst of token usage pushed the TotalTokens metric beyond the anomaly detection band. The email you received is the proof that the full pipeline works: Lambda invocation, metric publishing, anomaly detection, and SNS notification.

Alarm still showing Insufficient data or OK?

  • Give it a few more minutes. CloudWatch evaluates metrics on a schedule, so the alarm may not update instantly.
  • Try invoking the Lambda function a few more times to add more data points above the band.
  • Click on the alarm name to see its graph. The gray anomaly band should be visible. Confirm that the red data points from your burst sit above the upper band line.
  • Make sure you selected the TotalTokens metric for the DataScience team when creating the alarm, not a different metric or team.

Need more help? Help me troubleshoot why my CloudWatch anomaly detection alarm is not transitioning to ALARM state.

Now open your BedrockCostDashboard one more time. You should see the spike from your burst of invocations reflected in the graphs. This is the complete picture: per-team visibility plus automated alerting.

Your cost governance system is live. You have per-team attribution through inference profiles, real-time visibility on a dashboard, and an ML-powered alarm that catches spend anomalies. Next up, take it further by adding budget guard rails that automatically restrict Bedrock access when costs cross a threshold.

Secret mission

Add Budget Guard Rails

Your anomaly detection alarm tells you when Bedrock usage spikes. But what if nobody is around to respond? In this secret mission, you'll create an AWS Budget that automatically restricts Bedrock access when costs cross a threshold. This is the difference between monitoring and governance.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Most resources in this project fall within the AWS free tier, but the CloudWatch Anomaly Detection alarm costs approximately $0.30 per month if left running.

Cost warning

The anomaly detection alarm counts as 3 alarm metrics at $0.10 each, totaling $0.30 per month.

If you do not plan to return to this project, choose the Pause or Delete option below to stop the recurring alarm charge.

Resources you used:

  • 2 Application Inference Profiles (DataScienceProfile and EngineeringProfile).
  • 1 Lambda function (BedrockCostGovernance) with IAM execution role (LambdaBedrockCostGovernanceRole).
  • 1 CloudWatch dashboard (BedrockCostDashboard).
  • 1 CloudWatch Anomaly Detection alarm (BedrockTotalTokensAnomaly).
  • 1 SNS topic (BedrockCostAlerts) with email subscription.
  • 1 cost allocation tag (Team) activated in the Billing console.
  • (Secret Mission) 1 AWS Budget (BedrockMonthlyCostBudget) with budget action, IAM role (BudgetsActionRole), and IAM policy (DenyBedrockInvokeModel).

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your cost governance dashboard.

  • The dashboard, inference profiles, and Lambda function are within free tier and cost nothing to keep.
  • The anomaly detection alarm costs approximately $0.30 per month.
  • The SNS topic and email subscription are within free tier.
  • The AWS Budget (Secret Mission) uses 1 of your 2 free action-enabled budgets at no charge.

Pause - I'll come back to this later

Delete the resources that cost money while keeping the rest for reference.

  • Delete the anomaly detection alarm to stop the $0.30/month charge by running:
aws cloudwatch delete-alarms --alarm-names BedrockTotalTokensAnomaly
  • Delete the SNS topic to stop receiving alert emails by running:
aws sns delete-topic --topic-arn [[SNS_TOPIC_ARN="your BedrockCostAlerts topic ARN"]]

Keep the dashboard, inference profiles, and Lambda function for reference. They remain within free tier.

Delete - I don't want to use this again

Remove all project resources and start fresh. Run each command below in your terminal.

  • Delete the CloudWatch alarm by running:
aws cloudwatch delete-alarms --alarm-names BedrockTotalTokensAnomaly
  • Delete the CloudWatch dashboard by running:
aws cloudwatch delete-dashboards --dashboard-names BedrockCostDashboard
  • Delete the SNS topic by running:
aws sns delete-topic --topic-arn [[SNS_TOPIC_ARN="your BedrockCostAlerts topic ARN"]]
  • Delete the Lambda function by running:
aws lambda delete-function --function-name BedrockCostGovernance
  • Delete the Lambda execution role from the IAM console.
  • In the IAM console, select Roles in the navigation pane.
  • Search for LambdaBedrockCostGovernanceRole and select it.
  • Choose Delete, type the role name to confirm, and choose Delete again.
  • Delete both Application Inference Profiles by running:
aws bedrock delete-inference-profile --inference-profile-identifier [[DATASCI_PROFILE_ARN="your DataScienceProfile ARN"]]
aws bedrock delete-inference-profile --inference-profile-identifier [[ENGINEERING_PROFILE_ARN="your EngineeringProfile ARN"]]
  • Deactivate the Team cost allocation tag.
  • In the Billing and Cost Management console, select Cost allocation tags in the navigation pane.
  • Search for the Team tag, select it, and choose Deactivate.
  • (Secret Mission) Delete the AWS Budget.
  • In the Billing and Cost Management console, select Budgets in the navigation pane.
  • Select BedrockMonthlyCostBudget, choose Actions, then choose Delete.
  • Choose Confirm to delete the budget.
  • (Secret Mission) Delete the BudgetsActionRole IAM role.
  • In the IAM console, select Roles, search for BudgetsActionRole, select it, and choose Delete.
  • Type the role name to confirm, then choose Delete again.
  • (Secret Mission) Delete the DenyBedrockInvokeModel IAM policy.
  • In the IAM console, select Policies, search for DenyBedrockInvokeModel, select it, and choose Delete.
  • Type the policy name to confirm, then choose Delete again.

Nice Work!

Nice Work!

You just built a GenAI cost governance system that turns an opaque Bedrock bill into per-team visibility with automated anomaly alerts. That undifferentiated line item from Step 2 is now a fully attributed, monitored, and protected cost surface.

Here's what you learned:

  • Created Application Inference Profiles with cost allocation tags that attribute every Bedrock token to a specific team, turning a single billing line item into per-team cost breakdowns.
  • Built a Lambda function that calls Bedrock through tagged inference profiles and publishes token usage as custom CloudWatch metrics for real-time monitoring.
  • Configured a CloudWatch dashboard showing per-team input and output token trends, and wired up an anomaly detection alarm that uses machine learning to learn normal usage patterns and alert when something spikes.
  • (Secret Mission) Added AWS Budget guard rails that automatically restrict Bedrock access when costs cross a threshold.

Ready to quiz yourself?