Generative AI creates a cost-management problem that differs from conventional provisioned infrastructure. Spending can rise with individual behavior: one engineer repeatedly invoking a premium model in an agentic coding loop may consume more tokens in several hours than a team uses over a week. Because that activity may not become visible until billing data arrives, organizations can struggle to attribute spending, impose practical limits, and evaluate whether productivity gains justify the expense.
Jamf encountered that problem after giving its engineering organization broad access to Amazon Bedrock to support AI-assisted development. The company, which is trusted by more than 76,000 organizations to manage and secure Apple devices at scale, needed individual cost visibility and accountability as adoption grew. Its answer was a production system combining Amazon Bedrock invocation logging, Amazon S3, an Amazon Athena cost view, AWS Lambda, Amazon DynamoDB, Amazon EventBridge, Slack, and AWS Identity and Access Management Customer Managed Policies.
The design measures daily spending per engineer and applies model-specific restrictions as users approach their budgets. Crucially, it does not treat enforcement as an all-or-nothing shutdown. A lower-cost model remains available, restrictions are evaluated on subsequent calls without requiring users to authenticate again, and legitimate exceptions can receive higher limits for a defined period.
Solution overview
Jamf uses tiered controls rather than one universal deny point. In the example described by AWS, Anthropic Claude Opus access is denied when an engineer reaches 80% of the daily budget. Claude Sonnet is denied at 100%, while Claude Haiku remains available. This means an engineer who has exhausted the normal budget can continue working with the lower-cost option instead of losing Bedrock access entirely.
The scheduled enforcement cycle means changes take effect within minutes rather than instantaneously. The controls require no re-authentication and are automatically removed after the daily spending window resets and a later enforcement run republishes the relevant policies. A documented, time-boxed exception route accommodates work such as major migrations, customer escalations, and model evaluations.
Architecture diagram and operating flow
The source architecture divides the system into three responsibilities: measuring usage and cost, deciding and notifying, and enforcing restrictions. Its architecture diagram shows Bedrock logs flowing to S3, Athena calculating daily spending, and scheduled Lambda processing updating IAM policies.
Measure
An engineer invokes a Bedrock model through an AWS IAM Identity Center single sign-on session using bedrock:InvokeModel. With model invocation logging configured, Bedrock sends records to an S3 bucket. Those records include the model ID, input and output token counts, and the user identity. These fields supply the minimum information needed to associate consumption with an engineer and calculate model-dependent cost.
Decide and notify
An Athena view named bedrock_cost_today reads the logs in place. It multiplies input and output token counts by the applicable published model rates, then groups the resulting daily cost by user identity. This avoids introducing a separate data pipeline, although the format and scan pattern have important cost implications.
An EventBridge schedule invokes the enforcement Lambda every 15 minutes. Each run reads the current day’s calculated spending and checks a DynamoDB exceptions table for active custom limits. The same handler consults a DynamoDB state table to determine each user’s preceding tier. When someone enters a new threshold, it sends that engineer a one-time Slack direct message for the tier, giving the user notice of the restriction rather than allowing it to arrive unexplained.
Enforce
For every threshold, the Lambda calculates the complete list of engineers who should be restricted. It publishes a new version of the corresponding Customer Managed Policy through iam:CreatePolicyVersion, identifying affected users with the saml:sub condition key. The policies are attached to the IAM permission set.
IAM evaluates the new policy when the engineer next calls Bedrock. As a result, the updated allow-or-deny decision does not require re-provisioning the permission set or forcing a new login. “Near-real-time” in this design should therefore be understood in the context of the 15-minute enforcement schedule, query completion, policy publication, and evaluation on the next model request.
Prerequisites
Implementing the pattern requires several configured services and suitable administrative permissions:
- An AWS account authorized to create IAM roles and Customer Managed Policies, Lambda functions, Athena workgroups, S3 buckets, and DynamoDB tables.
- AWS IAM Identity Center with a permission set assigned to the relevant users.
- Amazon Bedrock model invocation logging configured to send records to S3.
- The AWS Command Line Interface configured with appropriate credentials.
- A Slack workspace and app supporting slash commands, interactivity, and bot messages.
AWS also provides the associated sample implementation at sample-bedrock-spend-enforcement.
Deployment steps
Step 1: Create the Amazon Athena cost view
Bedrock invocation records arrive in S3 as JSON. Deployment begins by defining an Athena table over that location and creating a view that converts token usage into dollar estimates. The calculation must distinguish input and output tokens and apply the published per-token rate for each model before grouping results by identity and current date.
The rate constants must match current Bedrock pricing for the deployment Region. Every enabled model family needs an explicit branch in the pricing logic. Jamf’s pattern assigns an unknown model the highest pricing tier rather than $0. This fail-safe prevents an unmapped model from escaping enforcement, but it can temporarily overestimate that model’s actual cost. Operators should monitor the corresponding alert and add the correct rate promptly.
Step 2: Create the Customer Managed Policies
Each enforcement policy denies a particular model family for the users listed by saml:sub. The policies begin with empty user lists and are attached to the IAM permission set. At runtime, Lambda populates the lists by publishing new policy versions. Changes made with iam:CreatePolicyVersion take effect without permission-set re-provisioning.
Step 3: Deploy the enforcement Lambda and schedule
The enforcement handler runs every 15 minutes. It queries Athena, loads active exceptions from DynamoDB, computes the complete restricted-user list for each tier, and publishes the required policy versions.
Recomputing complete lists makes the loop idempotent. A duplicate execution does not apply a restriction twice, while a missed execution is corrected when the next successful run catches up. The same approach simplifies the daily reset. Athena scopes spending to a daily window beginning at 00:00 in the chosen reference time zone. After that window rolls over, users below the new day’s thresholds disappear from the recomputed lists, and their restrictions lift on the following successful policy-version update. No separate unblock workflow is required.
Step 4: Add the exception workflow
Administrators can use a Slack command, /bedrock-limit, to grant an engineer a temporary higher limit. The command writes the user identity, elevated limit, and expiry timestamp to the DynamoDB exceptions table. It also records who granted the exception, when it was granted, and optionally the authorizing ticket. A DynamoDB Time to Live attribute on the expiry timestamp allows expired entries to clean themselves up. The Lambda incorporates active exceptions during its next scheduled calculation.
Learnings and operational caveats
The supporting serverless services were inexpensive in Jamf’s reported use case: Lambda, DynamoDB, and S3 together generated costs well under $10 per month for hundreds of engineers. That result is specific to the described deployment rather than a universal cost guarantee. Athena requires closer sizing because its cost varies with the data scanned and the query frequency.
Raw JSON is a notable limitation. Athena must deserialize every row before filtering, so merely selecting fewer columns does not provide the column pruning available with a columnar format. In the production experience described, four differently filtered queries over the same view each scanned approximately 11 GB. The recommended remedies are to produce all needed aggregates with one SELECT ... GROUP BY and separate the results in application code, or convert the logs to a columnar format such as Parquet.
- Keep the invocation-log schema lean, retaining token counts plus necessary identity and model metadata.
- Query the aggregated cost view once per enforcement run instead of repeatedly scanning raw logs.
- Treat the pricing map as an operational artifact and add an explicit branch whenever a model is enabled.
- Preserve a lower-cost model so reaching 100% of the budget limits expensive choices without stopping all AI-assisted work.
- Account for the IAM managed-policy limit of five retained versions. Before creating another version, Lambda must delete the oldest non-default version or
iam:CreatePolicyVersionwill fail. - Handle Athena’s asynchronous execution model. Lambda must submit the query, poll until it completes, and then retrieve the results, with its timeout configured accordingly.
The source reports that visible, enforceable per-user limits made leadership more comfortable expanding AI access. That is an account of Jamf’s production experience, not evidence that the same organizational result is guaranteed elsewhere. Still, the design illustrates how governance can support adoption by making exposure measurable and bounded.
Cleanup
Organizations evaluating or retiring the pattern should delete the resources they created to avoid continuing charges and remove the enforcement controls. Because deleting the policies also removes the guardrails, cleanup should be treated as both a cost-management and access-governance action.
Conclusion
Jamf’s system connects attribution, calculation, communication, and enforcement in a repeatable loop. Bedrock logs supply token and identity data; Athena estimates the current day’s cost; Lambda and DynamoDB determine thresholds and exceptions; Slack communicates tier changes; and IAM policies restrict selected model families on subsequent requests.
The most important design choice is graduated enforcement. Opus can be withdrawn at 80% and Sonnet at 100%, while Haiku remains available. Combined with temporary audited exceptions and an automatic daily reset, that structure aims to contain premium-model spending without halting engineering work. Its effectiveness nevertheless depends on correct regional pricing, complete model mappings, timely log delivery and query completion, successful policy-version maintenance, and careful Athena scan management.
About the authors
The AWS source credits Arun Chandapillai, Cami Persson, Aditya Mettu, Andre Bernardo, Andrew Dunham, and Levi McCormick. Their stated backgrounds span cloud architecture, account management, technical account management, cloud infrastructure and AI design, FinOps engineering, and engineering leadership. Levi McCormick is described as having more than 25 years in technology.
Source attribution: This article is a synthesis of the AWS Machine Learning Blog article, “Tokenomics at scale: How Jamf built real-time spend enforcement for Amazon Bedrock.”
Definition. Jamf’s Bedrock spending-control system is a scheduled enforcement loop that attributes token costs to individual users and applies model-specific IAM restrictions based on daily budget thresholds.
| Budget threshold | Model access outcome |
|---|---|
| Below 80% | Claude Opus, Claude Sonnet, and Claude Haiku remain available. |
| 80% to below 100% | Claude Opus is denied; Claude Sonnet and Claude Haiku remain available. |
| 100% or more | Claude Opus and Claude Sonnet are denied; Claude Haiku remains available. |
| Active temporary exception | A higher time-boxed limit is applied during the next scheduled calculation. |
Key takeaways
- Bedrock invocation logs provide the model ID, token counts, and user identity needed for per-user cost estimates.
- An Athena view calculates daily spending, and EventBridge invokes the enforcement Lambda every 15 minutes.
- The example denies Claude Opus at 80% of budget and Claude Sonnet at 100% while leaving Claude Haiku available.
- Lambda republishes complete restricted-user lists in IAM Customer Managed Policies, making the enforcement loop idempotent.
- Temporary higher limits are stored in DynamoDB through an auditable, time-boxed Slack exception workflow.
- Operators must maintain regional pricing mappings, limit Athena scans, handle asynchronous queries, and manage IAM’s five-version policy limit.
FAQ
How does Jamf calculate per-user Bedrock spending?
Bedrock sends invocation records containing model IDs, token counts, and user identities to S3. An Athena view applies model-specific input and output token rates and groups the estimated daily cost by user.
How quickly do spending restrictions take effect?
EventBridge runs the enforcement Lambda every 15 minutes. Effective timing also depends on Athena query completion, IAM policy publication, and evaluation on the user’s next model request.
What happens when an engineer reaches the daily budget?
In the described tiers, Claude Opus is denied at 80% and Claude Sonnet at 100%, while Claude Haiku remains available so work can continue with a lower-cost model.
Do users need to sign in again after a policy change?
No. IAM evaluates the updated Customer Managed Policy on subsequent Bedrock calls without permission-set re-provisioning or a new login.
How are temporary budget exceptions handled?
Administrators use the /bedrock-limit Slack command to store an elevated limit and expiry in DynamoDB. The Lambda includes active exceptions in its next scheduled calculation.
How are restrictions removed each day?
After the daily spending window resets, the next successful enforcement run recomputes the policy lists. Users below the new day’s thresholds disappear from those lists, so no separate unblock workflow is required.
