TL;DR
- Amazon Bedrock has five distinct mechanisms for attributing inference cost, and I tested all five live in an account: application inference profiles, Projects, Workspaces, IAM identity tags, and request metadata joined to invocation logs.
- Four of the five reach real AWS billing data (Cost Explorer / Cost and Usage Report). All four are bound to a resource or an identity you provision ahead of time — none vary per individual API call.
- The fifth, request metadata, is the only one that's per-call, but it lands in logs, not the bill. You compute the dollar figure yourself, from token counts — a real number, but not necessarily the invoiced one, since it can't see discounts or commitments applied at the account level.
- For a multi-tenant SaaS product, per-customer billing isn't a cloud-tagging problem at all — it's an application-layer metering problem, and AWS's own Well-Architected guidance says so directly.
The five mechanisms
Every one of these was tested with real API calls against a live Bedrock account, not read off a docs page. Each does something genuinely different, and picking the wrong one for your use case is where most cost-attribution confusion starts.
1. Application inference profiles — tag a model, not a call
An inference profile is a named resource that wraps a specific model. Tag the profile, route calls through it, and the tag shows up on the resulting billing line items.
aws bedrock create-inference-profile \
--inference-profile-name "team-search-claude" \
--model-source copyFrom="arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-haiku-4-5" \
--tags Key=team,Value=search Key=cost-center,Value=eng-42
Every call that specifies this profile's ARN as the model ID gets billed under it, and the team / cost-center tags become groupable dimensions in Cost Explorer once activated. The catch: one profile per model per cost dimension. Ten teams sharing five models means fifty profiles to create and keep in sync.
2. Projects and Workspaces — one resource, two entry points
A Project is a billing-scoped container that can span multiple models, which fixes the per-model constraint of inference profiles. Workspaces are the same underlying resource, referenced from a different API surface.
curl -X POST https://bedrock-mantle.us-east-1.api.aws/v1/organization/projects \
-H "Authorization: Bearer $TOKEN" \
-d '{"name": "search-team-project", "tags": {"team": "search", "cost-center": "eng-42"}}'
Scope a call to it via a header — OpenAI-Project: proj_xxxx on the Chat Completions surface, or anthropic-workspace: proj_xxxx on the Anthropic Messages surface. I created one Project, then called both surfaces against the same project ID and confirmed both routed to the identical billing bucket. One resource, callable from either API shape.
3. IAM identity tags — tag the caller, not the call
Tag the IAM role or user making the calls, and the tag rides along on every request that role makes.
aws iam tag-role --role-name bedrock-search-service \
--tags Key=team,Value=search Key=cost-center,Value=eng-42
Once the tag key is activated as a cost allocation tag, it shows up prefixed iamPrincipal/team in the billing export's tag column, alongside the caller's ARN in a dedicated line_item_iam_principal field. This is the mechanism that scales best across many models without creating a resource per dimension — one tagged role covers every model that role touches.
The limit is architectural, not a missing feature: the tag lives on the identity, not the request. If one shared service role serves ten thousand different end-customers, the bill sees one tagged identity, not ten thousand.
4. Session tags — a different tag per login, not per role
STS lets you attach tags at the moment a role is assumed, distinct from the role's own static tags. This is the mechanism that gets you closer to per-tenant, because a federated identity provider can mint a differently-tagged session for every login.
sequenceDiagram
participant User as Tenant User
participant IdP as Identity Provider
participant STS as AWS STS
participant Bedrock
User->>IdP: Login
IdP->>IdP: Read tenant_id attribute
IdP->>STS: AssumeRoleWithWebIdentity (principalTags: tenant_id)
STS->>STS: Attach session tag to temporary credentials
STS-->>User: Temporary credentials (1hr TTL)
User->>Bedrock: Converse(...) using tagged session
Bedrock-->>User: Response
Note over STS,Bedrock: Session tag rides on every call<br/>until credentials expire
Wiring this up: a Cognito Identity Pool (or any OIDC/SAML identity provider — Okta, Auth0, Entra ID all support the identical pattern) maps a custom user attribute to a principal tag.
aws cognito-identity set-principal-tag-attribute-map \
--identity-pool-id "us-east-1:xxxx" \
--identity-provider-name "cognito-idp.us-east-1.amazonaws.com/us-east-1_xxxx" \
--principal-tags tenant_id=custom:tenant_id
The IAM role being assumed has to explicitly trust sts:TagSession, not only sts:AssumeRoleWithWebIdentity — leaving it off doesn't produce an obvious tagging error, it produces an unrelated-looking InvalidIdentityPoolConfigurationException. I hit this omission once and it cost me twenty minutes tracing it back to the trust policy.
Getting the tag to actually show up in the billing export requires two separate switches, both easy to miss: INCLUDE_IAM_PRINCIPAL_DATA has to be turned on in the export's table configuration, and the export's own query has to explicitly select the tags column — a separate column from resource_tags, which is where I looked first and found nothing. Missing either switch produces a report with no error and no tenant breakdown, which reads exactly like the tag isn't working when it actually is.
This mechanism scales to per-tenant, but the cost is real: minting a session per tenant means caching credentials properly. A naive implementation that re-authenticates on every inbound request will hit identity-provider rate limits well before it hits any meaningful production traffic — Cognito's GetCredentialsForIdentity, for instance, defaults to 200 requests per second, account-wide. The fix is the same one every AWS SDK's credential provider already implements: cache the session for its lifetime, refresh a few minutes before expiry, never mint one per request.
5. Request metadata — the only per-call mechanism, and it skips the bill
Every other mechanism attributes cost by who's calling. This one attributes by what's in the call, and it's the only one that's genuinely per-request.
aws bedrock-runtime converse \
--model-id us.anthropic.claude-haiku-4-5-20251001-v1:0 \
--messages '[{"role":"user","content":[{"text":"hi"}]}]' \
--request-metadata '{"customer_id":"cust-042","feature":"chat-widget"}'
That metadata lands in CloudWatch invocation logs — automatically, alongside input and output token counts — the moment you turn on model invocation logging. It does not appear anywhere in Cost Explorer or the Cost and Usage Report. To get a dollar figure, you query the logs and multiply.
filter ispresent(requestMetadata.customer_id)
| fields requestMetadata.customer_id as customer_id,
input.inputTokenCount as inputTokens,
output.outputTokenCount as outputTokens,
(input.inputTokenCount * 0.000001) + (output.outputTokenCount * 0.000005) as estCostUSD
| stats sum(inputTokens), sum(outputTokens), sum(estCostUSD) by customer_id
I ran this against two simulated customers and got a clean per-customer split — real token counts, real cost math, computed in one CloudWatch Logs Insights query. I haven't compared this number against an actual invoice line item, but AWS's own cost-management guidance is explicit that a token-times-published-rate estimate doesn't account for volume discounts, committed spend, or whatever pricing tier the account is on — it's a real, defensible number for a dashboard, and it's built from a different formula than whatever your invoice actually applies.
The map, end to end
flowchart TD
A[API call to Bedrock] --> B{What are you tagging?}
B -->|A model resource| C[Inference Profile]
B -->|A billing container| D[Project / Workspace]
B -->|The calling role| E[IAM identity tag]
B -->|The calling session| F[STS session tag]
B -->|The request itself| G[Request metadata]
C --> H[Cost Explorer / CUR]
D --> H
E --> H
F --> H
G --> I[CloudWatch Logs]
I --> J[Self-computed estimate]
H --> K[Real invoice dollars,<br/>bound to a resource or identity]
J --> L[Real per-request granularity,<br/>estimated dollars]
Four paths converge on the same billing platform and inherit the same shape: attribution follows a resource or an identity, aggregated by day and usage type, never by individual request. One path breaks out to the request layer and trades the aggregation ceiling for an estimate. There's no version of this that gives you both a per-request breakdown and an invoice-accurate number, on Bedrock, today.
Where this stops being an AWS problem
If you're running a multi-tenant SaaS product on Bedrock, the natural next question is whether any of the five mechanisms above solve per-customer cost — team A pays for tenant X's usage, and you need to know the exact number.
They don't, and the reason isn't a gap in Bedrock specifically. AWS's own Well-Architected SaaS Lens states the design point directly: measuring per-tenant consumption in a shared-resource architecture requires the application itself to instrument tenant activity and correlate it with billing data afterward — the billing report alone was never going to enumerate an unbounded, growing customer base. The reference pattern in the same guidance is: capture tenant activity at the request layer (request counts, token counts, whatever correlates with cost in your architecture), store it, then apply that consumption ratio against the aggregate AWS bill for the period. That's mechanism five above, generalized — the SaaS billing layer sits next to CUR, reading from it, not inside it.
I went and checked whether another provider had actually solved this differently rather than only packaged it better. Google's Vertex AI lets a single service account attach a label to every individual request and have that label reach the actual Cloud Billing export — no per-tenant credential required, which is a real architectural difference from minting a tagged STS session per tenant. But the mechanism has a limit stated plainly in Google's own documentation: each label key holds at most 1,000 unique values, for the lifetime of the billing account, silently dropping anything past that with no error surfaced anywhere. A product with a few hundred tenants gets real per-tenant billing visibility with none of the identity plumbing Bedrock's session-tag pattern needs. A product that expects to grow past a thousand tenants hits the same wall — later, and more quietly than a missing feature would announce itself.
So what
Pick the mechanism by what you're actually trying to attribute, not by which one sounds most granular. Team, department, cost center — tag the role or the resource, and it's done; that's what all four billing-linked mechanisms are actually built for. Per-customer, in a product with a growing and unbounded tenant base — nothing in Bedrock's billing layer, or in Vertex's once you check the fine print, gives you that natively past a bounded scale. Build it where AWS's own architecture guidance says to build it: instrument the request layer, correlate against the bill yourself, and treat the result as a well-reasoned estimate rather than a substitute for the invoice.
I haven't found a platform that closes this gap natively — a request-level tag that lands in real, invoice-accurate billing data with no cardinality ceiling. I don't know if that's a hard problem or an unbuilt one.
Top comments (0)