AI agents are fundamentally different from ordinary chatbot requests.
A normal API request might look like:
User
↓
One model call
↓
Response
An agent can look more like:
User
↓
Model
↓
Web search
↓
Model
↓
Database tool
↓
Model
↓
Specialist agent
↓
Model
↓
Retry
↓
Tool
↓
Model
↓
Final answer
One user action can therefore trigger:
multiple model requests
multiple tool calls
sub-agent calls
retries
long contexts
reasoning tokens
paid search requests
sandbox/container usage
That creates a new engineering problem:
How do we guarantee that one agent run cannot spend more than we are willing to pay?
Simply checking the bill after the agent finishes is not enough.
Neither is:
if cost > 1.00:
stop()
because by the time that code runs, the expensive API call may already have happened.
A stronger architecture looks like this:
User Request
│
▼
┌───────────────┐
│ Budget Policy │
└───────┬───────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
Dollar Budget Model Calls Tool Calls
│ │ │
└────────────┼────────────┘
▼
Budget Ledger
│
▼
Reserve Before Work
│
┌───────┴───────┐
│ Enough budget?│
└───────┬───────┘
Yes │ No
│
┌───────────┴──────────┐
▼ ▼
Execute Reject
│
▼
Settle Actual Cost
│
▼
Continue Agent
In this tutorial, we will build that pattern using:
Python
Redis
OpenAI Responses API example
integer micro-dollar accounting
model-call limits
tool-call limits
wall-clock deadlines
per-tool budgets
budget reservations
emergency kill switches
The architecture itself is provider-independent.
The same principles apply to:
OpenAI
Anthropic
Google
AWS Bedrock
Azure AI
self-hosted model gateways
paid search APIs
browser services
vector databases
code execution platforms
The most important rule is:
Never start expensive work unless the maximum possible cost of that work fits inside the remaining budget.
Table of Contents
Why Post-Call Cost Checking Is Not a Hard Limit
Consider this implementation:
response = call_model()
spent += response_cost(response)
if spent > budget:
stop_agent()
Suppose:
Budget:
$0.10
Spent so far:
$0.09
Next model call:
$0.18
The sequence becomes:
$0.09 spent
↓
model call executes
↓
another $0.18 consumed
↓
total = $0.27
↓
application notices budget was $0.10
↓
agent stops
The agent stopped.
But the budget did not work.
You spent:
$0.27
against a supposed:
$0.10
limit.
That is monitoring, not enforcement.
The Correct Pattern: Reserve Before Execute
Before starting work, calculate a conservative maximum cost.
Example:
Current spending:
$0.06
Already reserved:
$0.01
Maximum budget:
$0.10
New model call could cost:
$0.025
Check:
spent
+
reserved
+
new maximum cost
<=
budget
So:
0.06
+
0.01
+
0.025
=
0.095
The call can proceed.
If instead the maximum possible call cost were:
$0.04
then:
0.06 + 0.01 + 0.04
=
0.11
The call is rejected before it starts.
That is much closer to a true hard budget.
What Does “Hard Limit” Really Mean?
There are several different limits people often mix together.
| Limit | What It Controls |
|---|---|
| Token budget | Maximum model tokens |
| Dollar budget | Maximum estimated monetary cost |
| Model-call budget | Number of LLM requests |
| Tool-call budget | Total external actions |
| Per-tool budget | Calls to one particular tool |
| Runtime budget | How long the agent may run |
| Delegation budget | Number of sub-agent calls |
| Retry budget | Number of repeated attempts |
| Provider account budget | Aggregate billing across a project/account |
A good production agent normally uses several at once.
For example:
Maximum cost:
$0.25
Maximum model calls:
8
Maximum tool calls:
15
Maximum web searches:
3
Maximum emails:
1
Maximum sub-agent delegations:
4
Maximum runtime:
90 seconds
Any one limit can terminate the run.
Why a Dollar Limit Alone Is Not Enough
Suppose your agent uses a very inexpensive model.
A broken loop could make:
2,000 model calls
while still staying under a relatively generous dollar budget.
That could cause:
rate-limit problems
latency
resource exhaustion
unexpected downstream traffic
large traces
poor user experience
You therefore want both:
money limits
and
behavior limits
Why a Call Limit Alone Is Not Enough
The reverse is also true.
Suppose:
max_model_calls = 10
One model call could contain:
very large input
large reasoning budget
large output allowance
and cost far more than expected.
Therefore:
call count
and:
monetary budget
solve different problems.
The Budget Policy We Will Build
Each run will have a policy like:
MAX_SPEND_USD = 0.25
MAX_MODEL_CALLS = 8
MAX_TOOL_CALLS = 15
MAX_RUNTIME_SECONDS = 120
We will also support per-tool limits:
TOOL_LIMITS = {
"web_search": 3,
"database_lookup": 8,
"send_email": 1,
}
And maximum billable cost for each externally charged tool:
TOOL_MAX_COST_USD = {
"web_search": 0.02,
"database_lookup": 0.00,
"send_email": 0.005,
}
These numbers are examples.
Use the actual pricing of the services your application calls.
Do Not Hard-Code Provider Prices in Application Logic
Model and tool pricing changes.
Avoid:
if model == "some-model":
cost = tokens * 0.000002
scattered across your codebase.
Instead use configuration.
For example:
MODEL_INPUT_USD_PER_MILLION=...
MODEL_OUTPUT_USD_PER_MILLION=...
Your pricing configuration can then be updated independently.
Always verify current rates from the provider’s official pricing page before deploying.
Why We Will Store Money as Integer Micro-Dollars
Floating-point money is dangerous.
Avoid:
spent = 0.1 + 0.2
for accounting logic.
Instead:
$1
=
1,000,000 micro-dollars
So:
$0.25
=
250,000 micro-dollars
Our Redis ledger stores only integers.
That allows atomic operations such as:
HINCRBY
without floating-point accounting errors.
Project Architecture
Our runtime will look like:
Agent Run
│
▼
BudgetManager
│
▼
Redis
┌────────┴────────┐
│ │
Run State Reservations
│ │
▼ ▼
spent_micro_usd pending cost
reserved_usd
model_calls
tool_calls
killed
deadline
Before every billable operation:
reserve
After it completes:
settle
If it fails ambiguously:
forfeit reservation
If a known local failure happens before the request is sent:
release reservation
That distinction matters.
Step 1 — Start Redis
Using Docker:
docker run -d \
--name agent-budget-redis \
-p 6379:6379 \
redis:8
Verify:
docker exec \
agent-budget-redis \
redis-cli PING
Expected:
PONG
Redis is useful here because reservations need to remain correct when:
multiple workers
parallel tools
multiple containers
multiple agent branches
try to spend the same budget concurrently.
Step 2 — Create the Python Project
Create:
mkdir ai-agent-budget
cd ai-agent-budget
python -m venv .venv
Activate it.
macOS/Linux:
source .venv/bin/activate
Windows:
.venv\Scripts\activate
Install:
pip install \
openai \
redis \
python-dotenv
Create:
ai-agent-budget/
│
├── budget.py
├── pricing.py
├── model_client.py
├── tools.py
└── demo.py
Step 3 — Convert Dollars to Micro-Dollars Safely
Create:
pricing.py
from decimal import (
Decimal,
ROUND_CEILING,
)
MICRO_USD_PER_USD = (
Decimal("1000000")
)
def usd_to_micro(
dollars: str | Decimal,
) -> int:
value = (
Decimal(str(dollars))
* MICRO_USD_PER_USD
)
return int(
value.to_integral_value(
rounding=ROUND_CEILING
)
)
def micro_to_usd(
micro_usd: int,
) -> Decimal:
return (
Decimal(micro_usd)
/ MICRO_USD_PER_USD
)
Now:
usd_to_micro("0.25")
returns:
250000
Step 4 — Calculate Token Cost Conservatively
Provider pricing is usually expressed as:
USD per 1 million tokens
Interestingly, the numeric value can also be treated as:
micro-dollars per token
because:
$1 / 1,000,000 tokens
=
1 micro-dollar / token
Add:
def token_cost_micro(
input_tokens: int,
output_tokens: int,
input_usd_per_million: str,
output_usd_per_million: str,
) -> int:
input_cost = (
Decimal(input_tokens)
* Decimal(
input_usd_per_million
)
)
output_cost = (
Decimal(output_tokens)
* Decimal(
output_usd_per_million
)
)
total_micro_usd = (
input_cost
+ output_cost
)
return int(
total_micro_usd
.to_integral_value(
rounding=ROUND_CEILING
)
)
Suppose:
input price:
$2 / million tokens
output price:
$10 / million tokens
input:
10,000 tokens
maximum output:
2,000 tokens
Then:
input:
10,000 × 2 microUSD
=
20,000 microUSD
=
$0.02
output:
2,000 × 10 microUSD
=
20,000 microUSD
=
$0.02
maximum request cost:
$0.04
Use the Most Expensive Applicable Rate for Reservation
Prompt caching may reduce actual cost.
Do not depend on that discount when reserving budget.
For a conservative reservation:
assume uncached input
unless your billing contract makes another rate guaranteed.
Likewise, if your processing mode can introduce:
regional surcharge
fast-processing premium
cache-write charge
other billing dimensions
include the maximum applicable amount in the reservation calculation.
A hard limit only works when:
Every billable dimension has a known upper bound.
Step 5 — Define the Budget Run
Create:
budget.py
Start with:
import re
import time
import uuid
import redis
from pricing import (
usd_to_micro,
)
class BudgetExceeded(
RuntimeError
):
pass
class AgentKilled(
RuntimeError
):
pass
class BudgetManager:
def __init__(
self,
client: redis.Redis,
) -> None:
self.client = client
Step 6 — Create a Budget Ledger
Add:
def create_run(
self,
*,
max_spend_usd: str,
max_model_calls: int,
max_tool_calls: int,
max_runtime_seconds: int,
ttl_seconds: int = 86400,
) -> str:
run_id = str(
uuid.uuid4()
)
key = self._run_key(
run_id
)
now_ms = int(
time.time() * 1000
)
deadline_ms = (
now_ms
+ (
max_runtime_seconds
* 1000
)
)
self.client.hset(
key,
mapping={
"limit_micro_usd":
usd_to_micro(
max_spend_usd
),
"spent_micro_usd":
0,
"reserved_micro_usd":
0,
"model_calls":
0,
"tool_calls":
0,
"max_model_calls":
max_model_calls,
"max_tool_calls":
max_tool_calls,
"deadline_ms":
deadline_ms,
"killed":
0,
"kill_reason":
"",
"created_ms":
now_ms,
},
)
self.client.expire(
key,
ttl_seconds,
)
return run_id
Add the helpers:
def _run_key(
self,
run_id: str,
) -> str:
return (
f"agent:budget:run:"
f"{run_id}"
)
def _reservation_key(
self,
reservation_id: str,
) -> str:
return (
"agent:budget:reservation:"
+ reservation_id
)
Step 7 — Why Reservations Need to Be Atomic
Imagine two sub-agents run simultaneously.
Current budget:
remaining:
$0.05
Agent A checks:
Can I spend $0.04?
Yes.
At the same time Agent B checks:
Can I spend $0.04?
Yes.
Both execute.
Total:
$0.08
The limit has been violated.
The bug happened because:
check
and
reserve
were two separate operations.
They need to happen atomically.
Step 8 — Use a Redis Lua Script for Atomic Reservation
Add this near the top of budget.py:
RESERVE_LUA = """
local run_key = KEYS[1]
local reservation_key = KEYS[2]
local global_kill_key = KEYS[3]
local now_ms = tonumber(ARGV[1])
local amount = tonumber(ARGV[2])
local kind = ARGV[3]
local name = ARGV[4]
local specific_limit = tonumber(ARGV[5])
local reservation_ttl_ms = tonumber(ARGV[6])
if redis.call(
'GET',
global_kill_key
) == '1' then
return {0, 'GLOBAL_KILL'}
end
if redis.call(
'EXISTS',
run_key
) == 0 then
return {0, 'RUN_NOT_FOUND'}
end
if redis.call(
'HGET',
run_key,
'killed'
) == '1' then
return {0, 'RUN_KILLED'}
end
local deadline = tonumber(
redis.call(
'HGET',
run_key,
'deadline_ms'
)
)
if now_ms > deadline then
return {0, 'DEADLINE_EXCEEDED'}
end
local spent = tonumber(
redis.call(
'HGET',
run_key,
'spent_micro_usd'
) or '0'
)
local reserved = tonumber(
redis.call(
'HGET',
run_key,
'reserved_micro_usd'
) or '0'
)
local limit = tonumber(
redis.call(
'HGET',
run_key,
'limit_micro_usd'
)
)
if spent + reserved + amount > limit then
return {0, 'SPEND_LIMIT'}
end
if kind == 'model' then
local current = tonumber(
redis.call(
'HGET',
run_key,
'model_calls'
) or '0'
)
local maximum = tonumber(
redis.call(
'HGET',
run_key,
'max_model_calls'
)
)
if current + 1 > maximum then
return {0, 'MODEL_CALL_LIMIT'}
end
redis.call(
'HINCRBY',
run_key,
'model_calls',
1
)
elseif kind == 'tool' then
local total = tonumber(
redis.call(
'HGET',
run_key,
'tool_calls'
) or '0'
)
local total_max = tonumber(
redis.call(
'HGET',
run_key,
'max_tool_calls'
)
)
if total + 1 > total_max then
return {0, 'TOOL_CALL_LIMIT'}
end
local field =
'tool_calls:' .. name
local specific = tonumber(
redis.call(
'HGET',
run_key,
field
) or '0'
)
if specific + 1 > specific_limit then
return {0, 'SPECIFIC_TOOL_LIMIT'}
end
redis.call(
'HINCRBY',
run_key,
'tool_calls',
1
)
redis.call(
'HINCRBY',
run_key,
field,
1
)
else
return {0, 'INVALID_KIND'}
end
redis.call(
'HINCRBY',
run_key,
'reserved_micro_usd',
amount
)
redis.call(
'HSET',
reservation_key,
'run_key',
run_key,
'amount_micro_usd',
amount,
'kind',
kind,
'name',
name,
'status',
'reserved',
'created_ms',
now_ms
)
redis.call(
'PEXPIRE',
reservation_key,
reservation_ttl_ms
)
return {1, 'OK'}
"""
The key operation is:
spent
+
already reserved
+
new reservation
being tested inside one Redis transaction-like script.
No second worker can slip through between checking and reserving.
Step 9 — Create a Reservation Method
Add:
def reserve(
self,
*,
run_id: str,
amount_micro_usd: int,
kind: str,
name: str,
specific_limit: int = 1000000,
) -> str:
if not re.fullmatch(
r"[A-Za-z0-9_-]{1,100}",
name,
):
raise ValueError(
"Invalid budget operation name."
)
reservation_id = str(
uuid.uuid4()
)
result = self.client.eval(
RESERVE_LUA,
3,
self._run_key(
run_id
),
self._reservation_key(
reservation_id
),
"agent:budget:global_kill",
int(
time.time() * 1000
),
amount_micro_usd,
kind,
name,
specific_limit,
86400000,
)
allowed = int(
result[0]
)
reason = str(
result[1]
)
if not allowed:
if reason in {
"GLOBAL_KILL",
"RUN_KILLED",
}:
raise AgentKilled(
reason
)
raise BudgetExceeded(
reason
)
return reservation_id
With decode_responses=True on the Redis connection, the returned reason will already be a normal string.
Step 10 — Create the Redis Connection
For example:
import redis
redis_client = redis.Redis(
host="localhost",
port=6379,
decode_responses=True,
)
Then:
budget = BudgetManager(
redis_client
)
Step 11 — Settle a Reservation After the Work Finishes
Once a call completes, we know the actual cost.
We now want:
reserved
→ decrease
spent
→ increase
atomically.
Add:
SETTLE_LUA = """
local run_key = KEYS[1]
local reservation_key = KEYS[2]
local actual = tonumber(
ARGV[1]
)
if redis.call(
'EXISTS',
reservation_key
) == 0 then
return {0, 'RESERVATION_NOT_FOUND'}
end
local status = redis.call(
'HGET',
reservation_key,
'status'
)
if status ~= 'reserved' then
return {0, 'ALREADY_SETTLED'}
end
local reserved_amount = tonumber(
redis.call(
'HGET',
reservation_key,
'amount_micro_usd'
)
)
redis.call(
'HINCRBY',
run_key,
'reserved_micro_usd',
-reserved_amount
)
redis.call(
'HINCRBY',
run_key,
'spent_micro_usd',
actual
)
redis.call(
'HSET',
reservation_key,
'actual_micro_usd',
actual,
'status',
'settled'
)
if actual > reserved_amount then
redis.call(
'HSET',
run_key,
'killed',
1,
'kill_reason',
'reservation_underestimated'
)
return {
0,
'ACTUAL_EXCEEDED_RESERVATION'
}
end
return {1, 'OK'}
"""
Why an Actual Cost Greater Than the Reservation Is Serious
If:
reserved:
$0.02
actual:
$0.04
then your cost model was wrong.
Possible causes include:
incorrect pricing configuration
unexpected tool charges
provider pricing change
missing billing dimension
output limit not enforced
processing surcharge not included
The safest response is:
kill the run
because your guarantee is no longer trustworthy.
Step 12 — Add the Settlement Method
def settle(
self,
run_id: str,
reservation_id: str,
actual_micro_usd: int,
) -> None:
result = self.client.eval(
SETTLE_LUA,
2,
self._run_key(
run_id
),
self._reservation_key(
reservation_id
),
actual_micro_usd,
)
if int(result[0]) != 1:
raise RuntimeError(
str(
result[1]
)
)
Step 13 — Handle Ambiguous API Failures Conservatively
This is one of the most important details in the entire tutorial.
Suppose you call an API.
The provider completes the request and charges you.
But:
your network times out
before the response arrives.
Your application sees:
TimeoutError
You do not know whether the provider charged:
$0
or
$0.03
If you simply release the reservation:
reserved budget returns to pool
and the agent retries, you may overspend.
For a hard-cap design:
An ambiguous network failure should consume the reserved maximum until you can prove that it did not incur cost.
Step 14 — Add a Forfeit Operation
The simplest safe implementation is:
def forfeit(
self,
run_id: str,
reservation_id: str,
) -> None:
key = self._reservation_key(
reservation_id
)
amount = int(
self.client.hget(
key,
"amount_micro_usd",
)
)
self.settle(
run_id=
run_id,
reservation_id=
reservation_id,
actual_micro_usd=
amount,
)
This may overestimate the bill.
That is intentional.
For a hard budget:
false positive spending
is safer than:
unaccounted spending
You can reconcile the ledger later if your provider offers authoritative usage records.
When Is It Safe to Release a Reservation?
You can release a reservation only when you know the expensive operation never started.
Examples:
local input validation failed
tool argument schema rejected
budget middleware failed before HTTP dispatch
user canceled before request was sent
You should not automatically release it after:
HTTP timeout
connection reset after request transmission
worker crash during API call
unknown provider error
because the upstream service may already have performed the work.
Step 15 — Add Model Pricing Configuration
In .env:
OPENAI_API_KEY=...
MODEL_NAME=your-model
MODEL_INPUT_USD_PER_MILLION=...
MODEL_OUTPUT_USD_PER_MILLION=...
Use the current official pricing page to populate these values.
Do not copy old prices from tutorials.
Step 16 — Count Input Tokens Before the Model Call
One challenge remains.
To reserve model cost, we need to know the input-token count before execution.
For the OpenAI Responses API, there is a dedicated input-token counting endpoint.
Create:
model_client.py
import os
from openai import OpenAI
from pricing import (
token_cost_micro,
)
client = OpenAI()
MODEL = os.environ[
"MODEL_NAME"
]
INPUT_RATE = os.environ[
"MODEL_INPUT_USD_PER_MILLION"
]
OUTPUT_RATE = os.environ[
"MODEL_OUTPUT_USD_PER_MILLION"
]
Step 17 — Build the Exact Model Request First
A good pattern is:
def build_request(
user_input: str,
) -> dict:
return {
"model":
MODEL,
"instructions":
(
"You are a technical "
"assistant."
),
"input":
user_input,
}
The important part is that token counting and model execution should use the same:
model
instructions
input
tool definitions
conversation state
where applicable.
Step 18 — Reserve the Worst-Case Model Cost
def call_model_with_budget(
*,
budget,
run_id: str,
user_input: str,
max_output_tokens: int,
):
request = build_request(
user_input
)
token_count = (
client
.responses
.input_tokens
.count(
**request
)
)
input_tokens = (
token_count
.input_tokens
)
maximum_cost = (
token_cost_micro(
input_tokens=
input_tokens,
output_tokens=
max_output_tokens,
input_usd_per_million=
INPUT_RATE,
output_usd_per_million=
OUTPUT_RATE,
)
)
reservation_id = (
budget.reserve(
run_id=
run_id,
amount_micro_usd=
maximum_cost,
kind=
"model",
name=
MODEL,
)
)
try:
response = (
client
.responses
.create(
**request,
max_output_tokens=
max_output_tokens,
)
)
except Exception:
budget.forfeit(
run_id,
reservation_id,
)
raise
actual_cost = (
token_cost_micro(
input_tokens=
response
.usage
.input_tokens,
output_tokens=
response
.usage
.output_tokens,
input_usd_per_million=
INPUT_RATE,
output_usd_per_million=
OUTPUT_RATE,
)
)
budget.settle(
run_id,
reservation_id,
actual_cost,
)
return response
Now the model cannot start unless:
input cost
+
maximum possible output cost
fits into the budget.
Why max_output_tokens Is Critical
Without:
max_output_tokens
you do not have a bounded output cost.
A model might generate considerably more than expected.
For hard-budget enforcement:
maximum output
must be part of the reservation calculation.
The Responses API’s reported:
output_tokens
includes generated tokens beyond just visible prose, including relevant internal output categories.
That is why we budget against the API’s token limit rather than estimating visible words.
Do Not Estimate Tokens From Word Count
Avoid:
estimated_tokens = (
len(text.split())
* 1.3
)
This can be useful for approximate UI estimates.
It is not appropriate for strict budget enforcement.
Different:
languages
code
JSON
tool definitions
model tokenizers
can produce very different token counts.
Use the provider’s tokenizer or token-counting endpoint where a hard preflight calculation is required.
Cached Tokens Need Careful Accounting
Prompt caching may make the actual bill smaller.
For hard reservation purposes, you can conservatively charge all input tokens at the ordinary input rate.
Example:
Actual provider billing:
8,000 normal input tokens
2,000 cached tokens
Budget ledger:
10,000 input tokens
at normal rate
Your internal budget becomes conservative.
That means the agent may stop slightly earlier than necessary, but it will not overspend because you assumed a discount that failed to materialize.
Exact Billing vs Conservative Budgeting
There are two useful numbers:
provider_estimated_bill
conservative_budget_charge
They do not have to be identical.
For user-facing analytics you may want precise cost accounting.
For enforcement you often want:
worst-case accounting
The latter is intentionally pessimistic.
Step 19 — Create Tool Budgets
Models are only part of agent cost.
A tool may charge:
per request
per search
per browser minute
per container session
per image
per document page
per GB
per transaction
Define:
tools.py
from dataclasses import (
dataclass,
)
@dataclass(
frozen=True
)
class ToolBudget:
max_calls:
int
max_cost_usd_per_call:
str
TOOL_BUDGETS = {
"web_search":
ToolBudget(
max_calls=3,
max_cost_usd_per_call=
"0.02",
),
"database_lookup":
ToolBudget(
max_calls=10,
max_cost_usd_per_call=
"0",
),
"send_email":
ToolBudget(
max_calls=1,
max_cost_usd_per_call=
"0.005",
),
}
Again, these prices are illustrative.
Replace them with the real maximum cost of your integrations.
Step 20 — Wrap Every Paid Tool
Create:
from pricing import (
usd_to_micro,
)
def execute_budgeted_tool(
*,
budget,
run_id: str,
tool_name: str,
tool_function,
*args,
**kwargs,
):
policy = (
TOOL_BUDGETS[
tool_name
]
)
reservation_id = (
budget.reserve(
run_id=
run_id,
amount_micro_usd=
usd_to_micro(
policy
.max_cost_usd_per_call
),
kind=
"tool",
name=
tool_name,
specific_limit=
policy.max_calls,
)
)
try:
result = tool_function(
*args,
**kwargs
)
except Exception:
budget.forfeit(
run_id,
reservation_id,
)
raise
actual_cost = (
result.actual_cost_micro_usd
)
budget.settle(
run_id,
reservation_id,
actual_cost,
)
return result
Now the tool has two independent protections:
maximum call count
and
maximum money
Free Tools Still Need Tool Budgets
Suppose:
database_lookup
costs no external API money.
You might set:
maximum cost:
$0
But still limit:
maximum calls:
10
Why?
Because the tool still consumes:
database resources
network traffic
latency
context tokens
downstream rate limits
“Free” does not mean unlimited.
Separate Read and Write Tool Budgets
A useful policy might be:
read_database:
10 calls
web_search:
3 calls
send_email:
1 call
delete_record:
0 calls without approval
You can combine this with the approval-gate architecture from:
Build a Secure MCP Gateway for AI Agents
Budget enforcement controls:
how much
while authorization controls:
whether the action is allowed at all
They solve different problems.
Tool Budget vs Approval Gate
Consider:
send_email
You may enforce:
permission:
email:send
human approval:
required
tool limit:
1
maximum tool cost:
$0.005
The call only executes when every condition passes:
authorized
AND
approved
AND
within budget
AND
under call limit
That is defense in depth.
Step 21 — Build a Per-Run Kill Switch
Sometimes you want to stop a run manually.
Add:
def kill_run(
self,
run_id: str,
reason: str,
) -> None:
self.client.hset(
self._run_key(
run_id
),
mapping={
"killed":
1,
"kill_reason":
reason,
},
)
Now every new reservation fails.
Example:
budget.kill_run(
run_id,
"Operator requested stop"
)
A Kill Switch Does Not Undo an In-Flight Request
This limitation is important.
If the application has already sent:
model request
or
external tool request
a Redis flag cannot magically reverse that request.
The kill switch guarantees:
no new budgeted operations begin
after the flag is observed.
For strong containment you should also use:
request timeouts
provider cancellation where available
maximum output limits
bounded tools
Step 22 — Add a Global Emergency Kill Switch
Sometimes you need to stop:
every agent
immediately.
For example:
billing incident
runaway loop bug
compromised tool
provider pricing misconfiguration
Set:
redis-cli SET \
agent:budget:global_kill \
1
Every new reservation will fail because our Lua script checks:
agent:budget:global_kill
before authorizing work.
Re-Enable Agent Execution
After the incident is resolved:
redis-cli DEL \
agent:budget:global_kill
Do not build the global kill switch into an endpoint that any normal application user can trigger.
Protect it with:
administrator authentication
RBAC
audit logging
Add Tool-Specific Kill Switches
Sometimes only one integration is dangerous.
Example:
web search:
healthy
database:
healthy
email API:
accidentally sending duplicates
You should be able to disable:
send_email
without shutting down the whole platform.
A production design can maintain:
agent:kill:tool:send_email
and check it before tool reservation.
Useful Kill-Switch Hierarchy
A mature platform might support:
Global kill
↓
Tenant kill
↓
Agent-type kill
↓
Run kill
↓
Tool kill
Examples:
global:
stop everything
tenant:
stop customer ACME
agent:
disable research agents
run:
stop run abc-123
tool:
disable send_email
Step 23 — Add a Runtime Deadline
Our run already stores:
deadline_ms
and checks it before every reservation.
If:
current time > deadline
no more work begins.
This prevents:
hour-long agent loops
even if token costs remain low.
Runtime Limits Need Network Timeouts Too
A logical deadline is not enough.
Suppose:
agent deadline:
120 seconds
but a tool’s HTTP timeout is:
600 seconds
The agent may remain stuck in the tool call long after its runtime budget expires.
Set tool and model timeouts consistently.
For example:
overall agent:
120 sec
model request:
45 sec
tool request:
20 sec
Exact values depend on your workflow.
Step 24 — Add an Agent-Step Limit
Consider a loop:
Model
↓
Tool
↓
Model
↓
Tool
↓
Model
↓
Tool
...
Model-call and tool-call budgets already help.
It can still be useful to define:
MAX_AGENT_STEPS = 12
and increment it on each loop iteration.
This makes agent behavior easy to reason about.
OpenAI Agents SDK Also Supports Turn Limits
If you use the OpenAI Agents SDK, its runner already supports:
max_turns
A turn is an AI invocation in the agent loop, including the tool calls resulting from that model turn.
Exceeding the limit raises:
MaxTurnsExceeded
That is useful as another guardrail.
However:
A turn limit is not a monetary budget.
Use it alongside budget enforcement rather than instead of one.
Agents SDK Also Tracks Aggregate Token Usage
The Agents SDK exposes usage including:
requests
input_tokens
output_tokens
total_tokens
per-request usage
across the run.
That is useful for:
analytics
monitoring
billing reports
evaluations
But usage is observed after calls have occurred.
For strict pre-call dollar enforcement, you still need a reservation architecture around billable work.
Multi-Agent Systems Need One Shared Budget
Suppose your coordinator delegates to:
Research Agent
Data Agent
Writing Agent
Bad architecture:
Coordinator:
$0.25 budget
Research:
$0.25 budget
Data:
$0.25 budget
Writing:
$0.25 budget
One user request can now spend:
$1.00
even though someone thought the workflow budget was:
$0.25
Instead give every branch the same:
run_id
and therefore the same Redis ledger.
One Shared Budget Across A2A Agents
For the multi-agent architecture from:
MCP vs A2A: Build a Multi-Agent System in Practice
the budget should flow like:
User Request
│
▼
run_id = abc
│
▼
Coordinator
/ \
/ \
▼ ▼
Diagnostics Release
run=abc run=abc
│ │
└─────┬─────┘
▼
Shared Redis
Budget Ledger
Now concurrent sub-agents cannot independently exceed the workflow cap.
Delegation Should Have Its Own Limit
Even if every specialist shares the same dollar budget, prevent unlimited recursion.
For example:
MAX_DELEGATIONS = 4
You may also track:
maximum delegation depth
such as:
MAX_AGENT_DEPTH = 3
This prevents:
Agent A
→ Agent B
→ Agent C
→ Agent A
→ Agent B
...
Retries Must Consume Budget
This is critical.
Do not write:
for attempt in range(5):
try:
call_model()
break
except Exception:
retry()
while counting only the final successful request.
Every retry can incur real cost.
Each retry should create:
new reservation
new model-call count
If there is not enough budget remaining:
do not retry
Budget-Aware Retry Logic
Conceptually:
for attempt in range(
MAX_RETRIES
):
try:
return (
call_model_with_budget(
...
)
)
except BudgetExceeded:
raise
except TemporaryError:
if attempt == (
MAX_RETRIES - 1
):
raise
sleep_with_backoff()
The budget wrapper remains inside the retry loop.
Every attempt is accounted for.
For retry architecture, see:
REST API Rate Limiting and Exponential Backoff in Python
Do Not Give Retries a Separate Hidden Budget
Bad:
normal calls:
$0.25 budget
retries:
unlimited
Retries are still work.
They should draw from the same root budget.
Parallel Tool Calls Need Atomic Reservations
Suppose the model requests four tools simultaneously:
search web
query database
read GitHub
call analytics API
Each worker performs:
reserve()
against the same Redis run.
Redis serializes the atomic Lua operations.
If only three requests fit within the remaining budget:
three proceed
one receives BudgetExceeded
That is exactly what you want.
Do Not Reserve Only After Launching Parallel Work
Wrong:
launch 10 tools
↓
calculate whether they fit
Correct:
reserve tool 1
reserve tool 2
reserve tool 3
...
↓
only launch successful reservations
Budget authorization must precede work.
Hosted Tools Require Special Attention
Some model providers offer tools executed inside the model platform, such as:
web search
file search
code execution
browser execution
containers
Those calls may have separate charges.
If one model request can autonomously make an unknown number of paid hosted-tool calls, then a pure token reservation may not bound the request cost.
For a true hard cap you need one of these:
a provider-side maximum tool-call limit
a known maximum number of tool calls
a fixed maximum charge
or
disable that hosted tool for strict-budget runs
If the number of billable operations inside one provider request cannot be bounded:
You cannot honestly claim an exact per-run hard dollar limit for that configuration.
That is an important architectural limitation.
Prefer Application-Owned Tools for Strict Budgets
If hard budgeting matters, an architecture like:
Model
↓
requests tool
↓
Your application
↓
Budget reservation
↓
Tool executes
gives you more control than:
Model provider
↓
autonomously executes unknown paid tools
This is another reason MCP gateways can be useful.
Your application remains the policy enforcement point.
Budget MCP Tools at the Gateway
A secure MCP gateway can enforce:
authorization
approval
budget
rate limit
audit
before calling the actual tool.
Example:
Agent
↓
MCP Gateway
↓
Scope check
↓
Approval check
↓
Budget reservation
↓
Tool execution
↓
Settlement
See:
Build a Secure MCP Gateway for AI Agents
Step 25 — Inspect the Budget State
Add:
def get_status(
self,
run_id: str,
) -> dict:
raw = self.client.hgetall(
self._run_key(
run_id
)
)
if not raw:
raise KeyError(
"Budget run not found."
)
return {
key:
int(value)
if key in {
"limit_micro_usd",
"spent_micro_usd",
"reserved_micro_usd",
"model_calls",
"tool_calls",
"max_model_calls",
"max_tool_calls",
"deadline_ms",
"killed",
"created_ms",
}
else value
for key, value
in raw.items()
}
Show Remaining Budget to the Agent Runtime
Calculate:
def remaining_micro_usd(
status: dict,
) -> int:
return (
status[
"limit_micro_usd"
]
- status[
"spent_micro_usd"
]
- status[
"reserved_micro_usd"
]
)
You can use this to choose a cheaper strategy.
Budget-Aware Model Routing
Suppose:
remaining:
$0.20
You may use your preferred model.
Later:
remaining:
$0.015
The runtime could switch to:
smaller model
shorter context
smaller output limit
rather than immediately failing.
Architecture:
Remaining Budget
│
├── High
│ ↓
│ stronger model
│
├── Medium
│ ↓
│ economical model
│
└── Low
↓
final-answer mode
This is graceful degradation.
But Never Let the Model Choose Its Own Budget
Do not tell the model:
You have $0.20.
Please decide how much you are allowed to spend.
and trust its answer.
The model can participate in:
planning
but budget enforcement must remain deterministic.
Correct:
Model proposes next action
↓
BudgetManager decides whether it can execute
Add a Final-Answer Reserve
One subtle problem appears when an agent uses all its budget researching.
Then:
no budget remains
to generate the final response.
You can solve this by reserving a portion from the start.
Example:
Total run budget:
$0.25
Reserved final-answer budget:
$0.04
Research budget:
$0.21
The agent cannot consume the final-answer reserve for intermediate work.
Two-Bucket Budget Architecture
Run Budget
│
├── Exploration
│ $0.21
│
└── Final Response
$0.04
You can implement this as:
two separate Redis ledgers
or one ledger with:
reserved_final_budget
subtracted from the amount available for ordinary work.
Tool Budgets Can Also Reserve Critical Actions
Suppose an agent may:
search
search
search
send final email
If searches consume the entire budget, the actual business action cannot happen.
You might therefore create:
analysis budget
execution budget
separately.
This is particularly useful for transactional agents.
Budget by User Tier
Different product plans may have different policies.
Example:
| Tier | Cost/Run | Model Calls | Tool Calls |
|---|---|---|---|
| Free | $0.02 | 3 | 4 |
| Pro | $0.15 | 8 | 15 |
| Enterprise | $0.50 | 15 | 30 |
The policy should come from your application database.
Never let users submit:
{
"max_spend_usd": 1000
}
and have the backend trust it.
Budget by Workflow Type
Different tasks deserve different budgets.
Example:
simple classification:
$0.002
customer-support answer:
$0.03
deep research:
$0.50
code migration:
$1.00
The application can assign budget based on an approved workflow class.
A Budget Should Be Server-Side
Do not rely on:
const maxSpend = 0.25;
inside browser JavaScript.
A user can change it.
Budget enforcement belongs on trusted infrastructure:
API server
agent runtime
MCP gateway
with Redis or another trusted state store.
User-Facing Cost Estimates Are Still Useful
Before starting expensive workflows, you can show:
This task may use up to $0.50 of AI credits.
The actual run can still terminate earlier.
This improves transparency and helps users understand why:
deep research
costs more than:
simple summarization
Rate Limits and Spending Limits Are Different
A rate limit says:
100 requests per minute
A spending limit says:
$0.25 per run
An agent can stay under the rate limit and still spend too much.
It can also stay under its spending limit while flooding a backend with cheap calls.
Use both.
Provider-Level Spend Limits Are Defense in Depth
Your application-level budget protects:
one run
one user
one workflow
Your API provider may also support:
organization budget
project budget
billing alerts
hard monthly spend limits
Enable those as a second line of defense.
For example:
Per-run application budget:
$0.25
Daily internal budget:
$200
Provider project hard limit:
$5,000/month
Each layer protects against a different class of failure.
Provider Hard Limits Are Not a Substitute for Per-Run Budgets
A provider project limit such as:
$5,000/month
does not prevent one broken agent from spending:
$500
before the project limit is reached.
Your runtime still needs its own budget controls.
Provider Billing Enforcement May Not Be Instantaneous
Another reason to keep an application-level ledger is that provider account-level spend enforcement may use tracked billing information that is not instantaneous.
A project hard limit is excellent for containing aggregate account exposure.
It should not be treated as millisecond-level authorization for each individual tool or model call.
Step 26 — Build the Demo Run
Create:
demo.py
import redis
from budget import (
BudgetManager,
)
from model_client import (
call_model_with_budget,
)
from pricing import (
micro_to_usd,
)
redis_client = redis.Redis(
host="localhost",
port=6379,
decode_responses=True,
)
budget = BudgetManager(
redis_client
)
run_id = budget.create_run(
max_spend_usd=
"0.10",
max_model_calls=
4,
max_tool_calls=
6,
max_runtime_seconds=
90,
)
print(
"Run:",
run_id
)
response = (
call_model_with_budget(
budget=
budget,
run_id=
run_id,
user_input=
(
"Explain connection "
"pool exhaustion "
"in PostgreSQL."
),
max_output_tokens=
600,
)
)
print(
response.output_text
)
status = budget.get_status(
run_id
)
print(
"Spent:",
micro_to_usd(
status[
"spent_micro_usd"
]
)
)
print(
"Reserved:",
micro_to_usd(
status[
"reserved_micro_usd"
]
)
)
Step 27 — Test Budget Rejection
Set:
MAX_SPEND:
$0.000001
Then attempt a model call.
Expected:
BudgetExceeded:
SPEND_LIMIT
The important test is:
the model API was never called
That is the behavior we care about.
Step 28 — Test Model-Call Limits
Create:
max_model_calls = 2
Then attempt:
call 1
→ allowed
call 2
→ allowed
call 3
→ rejected
Expected reason:
MODEL_CALL_LIMIT
Step 29 — Test Tool Limits
Configure:
"web_search":
ToolBudget(
max_calls=2,
max_cost_usd_per_call=
"0.02",
)
Then:
search 1
→ allowed
search 2
→ allowed
search 3
→ SPECIFIC_TOOL_LIMIT
Step 30 — Test Parallel Reservations
This is one of the most important tests.
Suppose:
remaining budget:
$0.05
Launch two workers simultaneously.
Each requests:
$0.04
Expected:
Worker A
→ reservation succeeds
Worker B
→ SPEND_LIMIT
or vice versa.
Never:
both succeed
Step 31 — Test the Kill Switch
Start a run.
Then execute:
redis-cli SET \
agent:budget:global_kill \
1
Attempt another model/tool reservation.
Expected:
GLOBAL_KILL
Then:
redis-cli DEL \
agent:budget:global_kill
Step 32 — Test Deadline Enforcement
Create:
max_runtime_seconds = 2
Wait:
3 seconds
Then request another reservation.
Expected:
DEADLINE_EXCEEDED
Step 33 — Test Reservation Underestimation
Intentionally:
reserve:
1,000 microUSD
settle:
2,000 microUSD
Expected:
ACTUAL_EXCEEDED_RESERVATION
and:
run killed
If this ever occurs in production, investigate pricing immediately.
Monitor Budget Utilization
Useful metrics include:
average cost per run
p50 / p95 / p99 cost
budget rejection rate
model calls per run
tool calls per run
percentage of budget consumed
kill-switch events
reservation underestimation
ambiguous-call forfeits
retry cost
cost by workflow
cost by model
cost by tenant
These metrics show whether your configured limits are realistic.
For observability architecture, see:
Structured Logging and Monitoring for Cloud Services
Log Reservations as Structured Events
Example:
{
"event": "agent_budget_reserved",
"run_id": "abc-123",
"operation": "model_call",
"reserved_micro_usd": 42000,
"remaining_micro_usd": 71000
}
After completion:
{
"event": "agent_budget_settled",
"run_id": "abc-123",
"reserved_micro_usd": 42000,
"actual_micro_usd": 27400
}
Do not log:
API keys
authorization headers
sensitive prompt contents
just because you are auditing cost.
Audit Why the Agent Stopped
A terminated run should have an explicit reason.
Examples:
SPEND_LIMIT
MODEL_CALL_LIMIT
TOOL_CALL_LIMIT
SPECIFIC_TOOL_LIMIT
DEADLINE_EXCEEDED
GLOBAL_KILL
RUN_KILLED
MAX_AGENT_DEPTH
MAX_RETRIES
This is much easier to operate than:
agent failed
Return a Graceful User Message
If the budget expires, avoid exposing internal accounting errors.
Instead:
The task reached its execution budget before all planned steps were completed. Here is the result produced so far.
When possible, return:
partial result
completed evidence
unfinished steps
rather than discarding all work.
Budget-Aware Partial Completion
Suppose the agent planned:
1. search docs
2. analyze repository
3. inspect logs
4. create detailed report
Budget expires after step 3.
A good runtime can switch to:
finalize with available evidence
instead of simply raising an exception.
This is one reason a final-response reserve is valuable.
Context Compaction Can Reduce Cost
Long-running agents often spend increasing amounts on input context.
A conversation may grow:
10K tokens
→
30K
→
80K
→
150K
even if each individual answer is small.
Context compaction can reduce future input cost.
See:
Build Long-Term Memory for AI Agents with Redis, Vector Search, and Context Compaction
However:
compaction itself
may require a model call.
That call must also consume budget.
There are no “free internal agent operations.”
Budget Compaction Calls Too
Bad:
normal agent calls
→ budgeted
memory summarization
→ not budgeted
evaluation
→ not budgeted
Your real spending can now exceed the run limit through hidden infrastructure work.
Include:
compaction
memory extraction
reranking
sub-agent calls
evaluation
tool summarization
if they happen synchronously as part of the run.
Background Work Needs Its Own Budget
If you perform:
memory extraction
offline evaluation
analytics enrichment
after the user response, that may belong to:
background_budget
rather than the original interactive-run budget.
But it must still have a budget somewhere.
Do not allow asynchronous workers to become an unmetered cost path.
Think in Terms of Cost Trees
One user request may create:
Root Request
│
├── Coordinator model
│
├── Research Agent
│ ├── model
│ ├── search
│ └── model
│
├── Database Agent
│ ├── model
│ └── database
│
└── Final model
Every node should roll up to:
one root workflow budget
You should be able to answer:
How much did this user request cost?
not merely:
How much did each microservice cost?
Charge Sub-Agent Costs to the Parent
A useful metadata model is:
root_run_id
parent_run_id
agent_id
reservation_id
Then a trace can show:
root:
$0.18
├── coordinator:
$0.03
├── diagnostics:
$0.07
├── release:
$0.04
└── final answer:
$0.04
This combines well with the multi-agent architecture described in:
Do Not Trust Model-Reported Cost
Never ask the model:
How many tokens did you use?
and use its answer for billing.
Use:
provider usage metadata
official token counters
tool billing metadata
your own deterministic ledger
The model is not your accounting system.
Do Not Trust Tool-Reported Cost Without Validation
If a third-party tool responds:
{
"cost": 0.01
}
decide whether that value is authoritative.
For untrusted integrations, you may prefer:
preconfigured maximum price
rather than trusting arbitrary metadata returned by the tool.
Variable-Cost Tools Need Bounded Inputs
Imagine:
transcribe_audio(file)
where cost depends on audio duration.
Before execution, determine:
duration
×
maximum price per minute
and reserve it.
Likewise:
image generation
→ number / size / quality bound
browser service
→ maximum runtime bound
storage
→ maximum bytes bound
search
→ maximum requests bound
A hard budget requires bounded parameters.
Reject Unbounded Tool Arguments
Bad:
{
"crawl_depth": 100000
}
Good:
crawl_depth <= 3
pages <= 25
runtime <= 30 sec
This is both:
cost control
and
security control
Tool Schemas Are Part of Budget Enforcement
Suppose a tool accepts:
generate_images(
count: int
)
The schema should enforce:
1 <= count <= 4
rather than trusting the model to request a reasonable number.
Budget safety begins before pricing calculations.
Kill Switches Should Be Operationally Accessible
A kill switch hidden inside source code is not very useful during an incident.
Operators need a controlled way to trigger:
disable workflow
disable model
disable tool
disable tenant
without waiting for:
new deployment
Possible control surfaces include:
admin dashboard
feature-flag system
Redis key
configuration service
Kill-Switch Changes Need Audit Logs
Example:
{
"event": "tool_kill_switch_enabled",
"tool": "send_email",
"actor": "ops@example.com",
"reason": "duplicate-send incident",
"timestamp": "..."
}
A production team should be able to answer:
Who disabled the agent?
When?
Why?
When was it re-enabled?
Budget Policies Need Versioning
Suppose yesterday:
research-agent-v1
budget:
$0.10
Today:
research-agent-v2
budget:
$0.25
Store:
policy_version
with each run.
Then cost analytics can explain why behavior changed.
Example Budget Policy Object
from dataclasses import (
dataclass,
)
@dataclass(
frozen=True
)
class AgentBudgetPolicy:
version:
str
max_spend_usd:
str
max_model_calls:
int
max_tool_calls:
int
max_runtime_seconds:
int
max_agent_depth:
int
Example:
policy = AgentBudgetPolicy(
version=
"research-v3",
max_spend_usd=
"0.25",
max_model_calls=
8,
max_tool_calls=
15,
max_runtime_seconds=
120,
max_agent_depth=
3,
)
Store the Policy Version in Redis
Add:
policy_version
to your run hash.
That allows your logs to say:
run abc
used policy research-v3
instead of inferring configuration later.
Cost Controls Are Also a Reliability Feature
Budget enforcement is often discussed as:
financial protection
but it also protects reliability.
A runaway agent can cause:
thousands of API requests
database overload
external email flood
search quota exhaustion
rate-limit bans
worker saturation
Even if the monetary cost is small.
Budget controls limit the blast radius.
Combine Spending Limits with Rate Limits
A mature architecture might look like:
Agent Action
│
▼
Authentication
│
▼
Authorization
│
▼
Approval
│
▼
Rate Limit
│
▼
Budget Reservation
│
▼
Tool / Model
│
▼
Audit
Each control has a different responsibility.
Cost Controls Are Not Security Controls by Themselves
A budget of:
$0.10
does not make:
delete_customer
safe.
An attacker may need only:
one cheap request
to cause damage.
Use budget controls alongside:
authorization
human approval
tool restrictions
sandboxing
not instead of them.
Hard Spending Limit Checklist
Before calling your agent budget “hard,” verify:
| Control | Required |
|---|---|
| Per-run dollar limit | Yes |
| Pre-call reservation | Yes |
| Atomic reservation | Yes |
| Output token maximum | Yes |
| Accurate input token count | Yes |
| Conservative pricing | Yes |
| Model-call limit | Yes |
| Tool-call limit | Yes |
| Per-tool call limits | Recommended |
| Tool maximum cost | Required for paid tools |
| Retry accounting | Yes |
| Parallel-call accounting | Yes |
| Sub-agent shared budget | Yes |
| Runtime deadline | Yes |
| Kill switch | Yes |
| Ambiguous failures | Conservatively charged |
| Pricing changes | Monitored |
| Provider account limit | Recommended |
| Structured logging | Recommended |
| Final-answer reserve | Useful |
| Tool argument bounds | Required for variable cost |
| Hidden background work | Budgeted separately |
| Unbounded hosted tools | Avoid for strict cap |
What You Cannot Guarantee
It is important not to oversell this architecture.
An application can reliably refuse new work when its internal budget is exhausted.
But an exact external billing guarantee depends on the provider.
Possible sources of small differences include:
billing-accounting delays
pricing changes
hidden billing dimensions
network failures after dispatch
provider-side tool execution
regional processing charges
async work already in progress
That is why production cost control should combine:
application reservations
+
bounded provider requests
+
provider-level account limits
rather than relying on only one layer.
Provider Hard Limit vs Runtime Hard Limit
Think of them this way.
Runtime Budget
Protects:
one agent run
Example:
Never authorize more than $0.25
for this user request.
Provider Budget
Protects:
overall project/account
Example:
Stop API traffic after monthly tracked
spend reaches $5,000.
You want both.
A Production Architecture
A mature deployment might look like:
User Request
│
▼
Agent API
│
▼
Budget Policy
│
▼
Redis
Atomic Budget Ledger
│
┌────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
Model API MCP Gateway Sub-Agent
│ │ │
│ Tool Budget │
│ │ │
└────────────────┼─────────────────┘
│
▼
Usage Settlement
│
▼
Structured Logging
│
▼
Cost Dashboard
Provider Account
│
└── Project hard spend limit
Deploying the Budget Service
If your agent runtime runs on multiple Cloud Run instances, Kubernetes pods, or VMs:
do not keep budgets in process memory
Bad:
current_spend = {}
because each instance has a different copy.
Use:
Redis
or
another strongly coordinated shared store
for live reservations.
For Cloud Run architecture, see:
Deploy Python & Express Apps to Google Cloud Run
Budget Data and Long-Term Analytics Can Use Different Stores
Redis is excellent for:
live atomic reservations
You may still write final results to:
PostgreSQL
BigQuery
data warehouse
for analytics.
Architecture:
Redis
=
real-time enforcement
Warehouse
=
historical reporting
Do not make a slow analytics database part of every budget authorization if low latency matters.
Reconcile Budget Records Asynchronously
After a run ends:
application ledger
vs
provider usage records
can be reconciled.
Look for:
missing usage
underestimated cost
unexpected tool billing
pricing configuration drift
If discrepancies appear, alert before allowing the problem to affect many runs.
Alert on Reservation Underestimation Immediately
This event should be rare:
ACTUAL_EXCEEDED_RESERVATION
Treat it as:
high-severity operational incident
because it means your hard-budget assumption may no longer be valid.
Possible automated response:
kill affected run
disable pricing configuration
activate model kill switch
alert on-call engineer
Pricing Configuration Should Be Tested
Create automated tests such as:
known model
→ pricing exists
all enabled processing modes
→ pricing exists
all paid tools
→ max cost defined
all tool limits
→ finite
Fail deployment if a paid model or tool has:
unknown price
instead of silently treating it as free.
Unknown Price Should Mean Deny
Bad:
price = (
pricing.get(model)
or 0
)
That means:
unknown model
=
free
Correct:
if model not in pricing:
raise RuntimeError(
"Pricing not configured."
)
Fail closed.
Final Example Policy
A robust research agent might use:
Maximum run cost:
$0.25
Final-answer reserve:
$0.04
Model calls:
8
Tool calls:
12
Web searches:
3
Database reads:
5
Emails:
0
Sub-agent delegations:
3
Agent depth:
2
Retries:
2 per operation
Runtime:
120 seconds
That is dramatically safer than:
Keep going until you finish.
Internal DigitalStackRoom Guides
For securing the tools an agent can call:
Build a Secure MCP Gateway for AI Agents
For multi-agent orchestration:
MCP vs A2A: Build a Multi-Agent System in Practice
For long-running context and memory:
Build AI Agent Memory with Redis and Vector Search
For retry handling:
REST API Rate Limiting and Exponential Backoff in Python
For production monitoring:
Structured Logging and Monitoring for Cloud Services
For API security:
Secure API Endpoints with JWT, CORS & Environment Variables
For deploying the runtime:
Deploy Python & Express Apps to Google Cloud Run
For LLM tracing and cost observability:
Langfuse vs Arize Phoenix vs Weave
Final Thoughts
The biggest mistake in AI cost control is thinking:
track usage
and:
limit usage
are the same thing.
They are not.
If your architecture is:
call model
↓
calculate cost
↓
discover that budget was exceeded
you have built monitoring.
A real enforcement architecture works in the opposite order:
calculate maximum possible cost
↓
atomically reserve that amount
↓
execute only if reservation succeeds
↓
measure actual usage
↓
settle reservation
↓
continue only while budget remains
The same principle applies to:
model calls
web searches
paid APIs
browser sessions
sub-agents
code execution
image generation
any other billable operation
Then add independent behavioral limits:
maximum model calls
maximum tool calls
maximum retries
maximum agent depth
maximum runtime
Finally, add kill switches that operators can use when something goes wrong.
The complete pattern becomes:
Agent
│
▼
Next Action
│
▼
Kill Switch?
│ │
Yes No
│ │
Stop ▼
Call Limit?
│ │
Hit OK
│ │
Stop ▼
Budget Reserve
│
┌──────┴──────┐
│ │
Denied Approved
│ │
Stop ▼
Execute
│
▼
Settle Cost
│
▼
Continue
That turns cost control from:
"Please don't use too many tokens."
into deterministic infrastructure.
And that is what AI agents need.
Models can decide:
what they want to do next
but trusted application code should decide:
whether the organization is willing to pay for it.
Official Documentation and Technical References
OpenAI API
OpenAI Input Token Counting API
OpenAI Agents SDK — Running Agents
OpenAI Agents SDK — Usage Tracking
OpenAI Spend Controls
OpenAI — Managing API Projects and Spend Limits
OpenAI — Troubleshooting API Usage and Spend Limits

