Skip to content

Cloud

Attributing Bedrock Costs per User and per Project: 4 Mechanisms That Actually Work

The claim that you can't tag Bedrock calls is partly true and mostly misleading. Here are the four cost attribution mechanisms, when to use each, and the gateway pattern that unlocks per-user dollars in your bill.

 ·  10 MIN READ


Alexandre Agius

Alexandre Agius

AWS SOLUTIONS ARCHITECT

SHARE

A platform team asked me a question last week that I now get from almost every team scaling on Bedrock:

“We need an accurate way to measure token cost per project and per user. Since you can’t tag Bedrock calls, we found a workaround with Application Inference Profiles. What’s the best way to do this?”

The premise in that question is half right, and the half that’s wrong sends people down the wrong path. So let me take it apart.

In an earlier post I covered tracking and capping spend per team using inference profiles. That post answered “what did the marketing team spend?” This one answers the two harder questions that come next once the platform grows: “what did this user spend?” and “how do I get that number onto the actual bill, not just an estimate?”

The Claim, Taken Apart

“You can’t tag Bedrock calls.”

Strictly, this is true: you cannot attach an AWS cost allocation tag as a parameter on an individual InvokeModel or Converse call. Cost allocation tags are a resource-level construct. A single inference call is not a resource you tag.

But the conclusion people draw from that (“so per-call attribution is impossible”) is wrong. Bedrock has four different attribution mechanisms, and the tag limitation only affects one dimension of one of them. The reason teams get confused is that these four mechanisms answer different questions, live in different data planes, and some are recent enough that older blog posts do not mention them.

Here is the whole picture on one screen.

MechanismPer-request?Flows to CUR / Cost Explorer?
Application Inference Profiles (AIPs)No (aggregated per day)Yes
requestMetadata (Converse / InvokeModel)Yes (per prompt)No (logs only)
IAM principal attribution (caller ARN)No (aggregated)Yes (CUR 2.0 only)
Projects (bedrock-mantle)No (aggregated)Yes

Notice the tension in that table. The mechanism that gives you per-request detail does not flow to the bill. The mechanisms that flow to the bill do not give you per-request detail. No single row does both. That is the entire reason this problem feels harder than it should.

Start Here: IAM Principal Attribution

If you are starting fresh in 2026, do not start with AIPs for per-user tracking. Start with IAM principal attribution, which went GA in April 2026 and quietly reframes the whole question.

Bedrock now automatically attributes every inference call to the IAM principal that made it: an IAM user, an assumed role, or a federated identity from Okta, Entra, or Auth0. No profiles to create, no resources to manage, no code change. The caller identity shows up in the line_item_iam_principal column of CUR 2.0 once you enable “Include caller identity (IAM principal) allocation data” in the Data Exports configuration.

If you attach principal tags or session tags, those roll costs up by team, project, or cost center in Cost Explorer and CUR 2.0 under the iamPrincipal/ prefix. API keys map to IAM principals too, so the same attribution applies to them.

For a lot of teams, that is the answer, and they can stop reading. The catch is what happens when you put a gateway in front of Bedrock.

The Gateway Gotcha

Most teams at any scale do not let application code call Bedrock directly. They route through an LLM gateway (LiteLLM, an internal proxy, or a commercial gateway product) for rate limiting, key management, and routing. That gateway is where per-user attribution quietly dies.

By default, the gateway calls Bedrock with the gateway’s own role. Every user in the company collapses into one identity on the bill. You enabled caller-identity attribution, you look at CUR, and you see one principal responsible for 100% of the spend. Technically accurate. Completely useless.

The fix is to make the gateway embed the user identity in the STS session name when it assumes its role:

sts.assume_role(
    RoleArn="arn:aws:iam::123456789012:role/bedrock-gateway-role",
    RoleSessionName="alice",          # surfaces in logs AND CUR 2.0
    Tags=[{"Key": "project", "Value": "forecasting-app"}],
)

That RoleSessionName surfaces as assumed-role/bedrock-gateway-role/alice in the caller identity, and it flows automatically into CUR 2.0. This one line is the difference between “the gateway spent everything” and “alice spent $42 on the forecasting-app project this month.”

Two practical warnings. AssumeRole adds roughly 100ms of latency and STS has rate limits, so cache the session credentials for the lifetime of the user session rather than calling STS on every request. And do not put an email address or anything else sensitive in the session name if you can avoid it; a stable hashed user ID is safer, because that value lands in logs and in CUR.

requestMetadata: The Only Per-Prompt Detail

The second mechanism worth knowing well is requestMetadata, and it is the one the original “you can’t tag calls” claim completely misses.

requestMetadata is supported on both Converse and InvokeModel. You can attach up to 16 key-value pairs per call, with no resource to pre-create. It does not flow to the bill; it lands in your model invocation logs. That makes it the only mechanism that gives you genuine per-prompt token and cost detail.

response = client.converse(
    modelId=AIP_ARN,          # AIP ARN handles project-level billing
    messages=messages,
    requestMetadata={
        "user": "u-8f3a2c",   # stable hashed ID, not an email
        "project": "forecasting-app",
        "session_id": "sess-a1b2c3",
        "feature": "rfq-analysis",
        "agent_step": "planning",
    },
)

That agent_step key is the sleeper feature. In an agentic workflow where a single user request fans out into planning, tool calls, and synthesis, requestMetadata is the only way to see which step of the loop is burning your tokens. If you run agents, this is where you find out that your planning step is quietly three times more expensive than the actual work.

To use it: enable Model Invocation Logging in the Bedrock console settings, then query the logs. CloudWatch Logs Insights gives you fast token totals by user or model; Athena over the logs in S3 lets you compute estimated cost at per-prompt grain.

The word “estimated” matters. Log-derived cost is token count times the public rate card. It does not know about your EDP discounts or Savings Plans. Treat it as a high-resolution relative signal, not an invoice, and reconcile it against CUR periodically at the model and usage-type grain.

Where AIPs and Projects Still Fit

AIPs have not stopped being useful. They remain the clean answer to “what did project X cost this month?” as invoice-accurate dollars in Cost Explorer and CUR. Create one profile per project (or per project times model), tag it with project, cost-center, and env, and use the profile ARN as the modelId in your calls.

A few things to keep in mind with AIPs: activate the tags as cost allocation tags in the Billing console, expect up to 24 hours before they appear, and know that activation is not retroactive. The finest grain you get is per-usage-type per-day, not per request, and the profile count grows with projects times models. And AIPs give you no per-user dimension at all on their own.

Projects (the newer bedrock-mantle construct) fill a similar per-workload role and also flow to CUR. If you are already invested in AIPs, as this team was, there is no reason to rip them out. They are doing the project-level job correctly. The gap they cannot fill is per-user.

The Layered Answer

Here is the honest structural truth: no single mechanism gives you both per-project dollars on the bill and per-user per-prompt detail. If you need both, you layer.

Question you’re answeringMechanismGranularity
”What did project X cost this month?”AIP tags to Cost Explorer / CURPer usage type, per day
”What did user alice spend on prompt Y?“requestMetadata to logs to AthenaPer request (estimated $)
“Per-user invoice-accurate dollars”IAM principal attribution to CUR 2.0Per usage type/day, by identity
”Per-step cost in an agentic workflow”requestMetadata agent_step key to logsPer agent step

A mature setup runs two data planes and reconciles them. Plane one is a CUR 2.0 dashboard (Cloud Intelligence Dashboards, or a QuickSight build on CUR 2.0) for invoice-accurate project and per-user dollars. Plane two is Athena plus QuickSight over the invocation logs for per-prompt and per-step token detail. You reconcile the log-derived numbers against CUR at the model and usage-type grain so your rate-card math stays honest. One caveat that trips people up: if you use prompt caching heavily, you have to account for all four token types (input, output, cache-read, cache-write) or your totals will never reconcile.

There is one console-level nuance worth stating plainly, because it costs people an afternoon. The per-user caller identity (line_item_iam_principal) is queryable in CUR 2.0 through Athena or QuickSight only. The Cost Explorer console does not expose it as a filter or grouping dimension. If you want per-user cost visible in the Cost Explorer console specifically, you have to go through session tags activated as cost allocation tags, not the IAM principal column.

Four Rules Before You Build

A few design rules that will save you a rebuild later.

Design tags for analytics, not noise. Use stable, low-cardinality keys (team, environment, feature, cost center) as cost allocation tags. Keep high-cardinality values (session ID, trace ID, user ID) in requestMetadata only. If you push high-cardinality values into the cost-allocation-tag space, you bloat it badly. The practical implication: attribute per-user through IAM principal identity, not through a cost allocation tag.

Enforce tagging in a shared layer. Bedrock does not reject a call that omits metadata or a session name. Opt-in tagging means partial data, which means wrong dashboards. Stamp everything in the gateway or a shared client wrapper, and use SCPs or IAM policies to deny Bedrock access outside the gateway role. The gateway becoming the only call path is what guarantees 100% coverage.

Treat tag values as log content. Metadata and session tags land in invocation logs and in CUR 2.0. No PII, no credentials, no secrets. Hash the user ID.

Respect the limits. requestMetadata allows 16 entries per request, 256 characters per key and value. IAM principals allow up to 50 tags. Resource tags follow standard AWS limits.

An Honest Recommendation

Start with IAM principal attribution. It is automatic, it went GA in April 2026, and for most teams the only work between them and per-user cost is the gateway session-name pattern. Do that one thing first, because it is cheap and it puts real dollars against real identities on the actual bill.

Add requestMetadata at the gateway next, but only if you actually need per-prompt or per-agent-step detail. Plenty of teams think they need per-prompt attribution and really just need per-user-per-month, which IAM principal attribution already gives them. If you run agents, though, the agent_step breakdown earns its keep quickly.

Keep your AIPs if you already have them. They do the project-level billing job correctly, and there is no prize for tearing out something that works.

Where this tips into overkill: running the full two-plane, reconciled setup for a platform with three projects and twenty users. If your monthly Bedrock bill is a rounding error, one AIP per project and a Cost Explorer view is plenty. Build the per-user, per-prompt machinery when the number of tenants (or the size of the bill) makes the question “who spent this?” something you have to answer for chargeback, not curiosity. The mechanisms will still be there when you need them.

ABOUT THE AUTHOR

Alexandre Agius

Alexandre Agius

AWS Solutions Architect

Passionate about AI & Security. Building scalable cloud solutions and helping organizations leverage AWS services to innovate faster. Specialized in Generative AI, serverless architectures, and security best practices.

ONE LETTER A MONTH · NO TRACKER · UNSUBSCRIBE ANYTIME

CONTINUE READING

Related dispatches

Comments

Sign in to leave a comment