Jamf Enforces Per-User Bedrock Spend Limits in Under 15 Minutes
In this article
Generative AI spend breaks every assumption traditional FinOps tooling was built on. Provisioned compute scales predictably; token consumption scales with developer behaviour. A single engineer running an agentic coding loop against a premium model can exhaust more budget in a few hours than an entire team consumes in a week. Jamf — trusted by more than 76,000 organizations to manage and secure Apple devices — hit this wall after giving its engineering organization broad access to Amazon Bedrock. Productivity climbed, but so did the need for per-user cost accountability. Their response was a production system that enforces tiered spend limits in near-real-time, without interrupting active sessions or requiring re-authentication.
The architecture demonstrates that governance is a prerequisite for scaling agentic workflows, not an obstacle to them — a pattern that transfers directly to any organization running foundation models through managed APIs.
Three-Layer Architecture: Measure, Decide, Enforce
Jamf decomposed the problem into three discrete concerns, each handled by a purpose-built serverless component.
Measure. When an engineer calls bedrock:InvokeModel through an AWS IAM Identity Center SSO session, Amazon Bedrock logs the invocation — model ID, input token count, output token count, and user identity — to a configured Amazon S3 bucket. No custom instrumentation is required at the SDK layer.
Decide and notify. An Amazon Athena view named bedrock_cost_today reads those S3 logs and computes per-user daily spend by multiplying token counts against published per-model rates, grouped by user identity and the current date. An AWS Lambda enforcement handler, triggered every 15 minutes by an Amazon EventBridge schedule, queries that view and cross-references a DynamoDB exceptions table holding any custom, time-boxed limits. When a user crosses a new spending threshold, the handler emits a one-time Slack direct message for that tier, reading the user's previous state from a DynamoDB state table to avoid repeated notifications.
Enforce. For each engineer over a threshold, Lambda publishes a new version of the appropriate Customer Managed Policy (CMP) via iam:CreatePolicyVersion. The policy targets specific users through a saml:sub condition key. Because the CMPs are attached directly to the IAM permission set, the updated policy takes effect on the engineer's next Bedrock call — no re-provisioning, no session teardown.
Tiered Thresholds and the Always-Available Model
The threshold logic encodes a deliberate product decision. At 80% of the daily budget, access to Anthropic Claude Opus is denied. At 100%, Anthropic Claude Sonnet is also denied. Anthropic Claude Haiku is never restricted — an engineer who has exhausted their budget retains a low-cost model and can continue working. Enforcement is not a hard stop; it is a cost-weighted capability tier.
Engineers who need more capacity for a large migration, a customer escalation, or model evaluation can request an elevated limit via a Slack slash command (/bedrock-limit). Admins grant time-boxed higher limits that write to the DynamoDB exceptions table with an expiry timestamp and a full audit trail. A DynamoDB Time to Live (TTL) attribute on the expiry timestamp means exceptions clean themselves up without a separate code path.
| Spend Threshold | Denied Model | Still Available | Enforcement Mechanism |
|---|---|---|---|
| 80% of daily budget | Anthropic Claude Opus | Claude Sonnet, Claude Haiku | CMP version published via iam:CreatePolicyVersion |
| 100% of daily budget | Anthropic Claude Sonnet | Claude Haiku | CMP version published via iam:CreatePolicyVersion |
| Exception granted | None (elevated limit) | All models | DynamoDB exceptions table with TTL expiry |
Operational Specifics and Control Plane Cost
The enforcement Lambda is idempotent by construction. Each run recomputes the full restricted-user list from cumulative spend for the current day rather than applying incremental changes. A missed run or duplicate execution produces the same result once it catches up. The daily reset is implicit: the Athena view scopes spend to a rolling window anchored at 00:00 in the configured reference time zone. When the window rolls over, the next run drops users who are no longer over threshold, and the CMP restriction lifts automatically on the following iam:CreatePolicyVersion call.
Running this system across hundreds of engineers, the combined cost of Lambda, DynamoDB, and S3 stayed well under $10 per month. Athena is the one line item requiring active management. JSON logs force Athena to deserialize every row before applying any filter — column pruning and predicate pushdown provide no relief. Jamf observed that four differently filtered queries against the same view each scanned approximately 11 GB. The fix is to consolidate derived queries into a single SELECT ... GROUP BY and split results in application code, or convert logs to a columnar format such as Parquet.
Two hard constraints require attention. First, a managed policy retains a maximum of five versions; the enforcement Lambda must delete the oldest non-default version before calling iam:CreatePolicyVersion, or the call fails. Second, Athena's asynchronous query model means Lambda must submit a query, poll for completion, and then read results — the function timeout must be set accordingly. An unrecognized model in the pricing map is deliberately priced at the highest tier as a fail-safe, not at zero, so a newly enabled model cannot silently bypass enforcement.
The code is available at https://github.com/aws-samples/sample-bedrock-spend-enforcement.
The counterintuitive outcome Jamf documented: installing hard per-user caps made leadership comfortable expanding AI access rather than contracting it. Spend became observable, which made the ROI case tractable. Organizations that can answer the tokenomics question at the individual contributor level are the ones that will turn broad model access into durable productivity gains.
Related Reading
Bedrock AgentCore Queries Cross-Account Knowledge Bases via STS Role
AWS shows how AgentCore agents can call RetrieveAndGenerate across account boundaries using a narrowly scoped IAM role assumed via STS.
Three Async Patterns Cut Lambda Idle Cost in Bedrock AgentCore Pipelines
AWS authors show how task-token callback, direct integration, and durable functions cut Lambda idle cost when calling Bedrock AgentCore agents.
AWS AgentCore Runtime Hosts MCP Servers for Amazon Quick Agents
AWS shows how to deploy MCP servers on AgentCore Runtime and wire them into Amazon Quick chat agents via AgentCore Gateway with dual OAuth 2.0 auth flows.