Set spending limits
What you get: a daily limit for each team and for each person in it. When a limit is used up, the gateway refuses new requests with HTTP 429 until midnight UTC.
A Budget belongs to one group. It can limit requests, estimated dollars, or both. This guide shows both: a request limit for engineering and a dollar limit for sales.
Before you start
You need:
- the cluster from the Quick start, installed with budgets on (
limits.enabled: true). Different models for different teams shows the upgrade command; - the groups and people of that guide: Alice and Dan in
engineering, Bob insales; - the models of that guide,
free-modelfor engineering andsales-modelfor sales, and a personal API key for each person (Connect Claude Code and Cursor). Save the keys inALICE_KEY,DAN_KEYandBOB_KEY.
Budgets count requests and dollars once a day, in UTC. If you repeat this guide on the same day, the counters keep their values until midnight UTC. Use new limits or wait.
1. Limit requests for engineering
Engineering gets 5 requests a day for the whole group, and each person gets 3. The numbers are small only so that you reach them quickly.
kubectl -n ambit-system apply -f - <<'EOF'
apiVersion: ambitproject.dev/v1alpha1
kind: Budget
metadata: { name: engineering }
spec:
group: engineering
groupRequestsPerDay: 5
userRequestsPerDay: 3
EOF
A group has one Budget. A person in several budgeted groups is counted against each of them.
2. Limit dollars for sales
Dollar limits need a price. Ambit counts each answer as input tokens × input price + output tokens × output price, from the price on the Model. A model without a price is refused to every group that has a dollar limit.
Add a price to sales-model. The prices are dollars per million tokens. They are made up for this guide, because the model is free. Then create the budget:
kubectl -n ambit-system apply -f - <<'EOF'
apiVersion: ambitproject.dev/v1alpha1
kind: Model
metadata: { name: sales-model }
spec:
displayName: Sales model
provider: Openrouter
model: nvidia/nemotron-3.5-lightning:free
credentialsRef: { name: ambit-llm-openrouter }
price: { inputPerMillionTokens: "1", outputPerMillionTokens: "4" }
---
apiVersion: ambitproject.dev/v1alpha1
kind: Budget
metadata: { name: sales }
spec:
group: sales
groupDollarsPerDay: "0.01"
userDollarsPerDay: "0.001"
EOF
kubectl -n ambit-system get budgets.ambitproject.dev
Dollar amounts are estimates from list prices. They are not an invoice.
A change to a Budget takes up to about a minute to reach the gateway. Wait a minute before the next step.
Check it works
This function makes one call and prints the start of the answer and the HTTP status.
ask() { # ask KEY MODEL
curl -s -w ' HTTP %{http_code}\n' http://llm.localtest.me:8080/v1/chat/completions \
-H "Authorization: Bearer $1" -H 'Content-Type: application/json' \
-d "{\"model\":\"$2\",\"messages\":[{\"role\":\"user\",\"content\":\"Say hello in five words.\"}]}"
}
Per person. Alice calls four times. The fourth call is over her limit of 3.
for i in 1 2 3 4; do ask "$ALICE_KEY" nvidia/nemotron-3-super-120b-a12b:free | cut -c1-200; done
The first three calls end with HTTP 200. The fourth is refused with HTTP 429 and an error body in the format of the API you called (here OpenAI's; Anthropic's for /v1/messages). The body says that a request limit or a spending limit was reached, but not which one. The last step below shows a full refusal.
Per group. Dan has made no call yet, so his own count is 0. He calls three times:
for i in 1 2 3; do ask "$DAN_KEY" nvidia/nemotron-3-super-120b-a12b:free | cut -c1-200; done
His first two calls end with HTTP 200. The third is refused with HTTP 429. The group limit of 5 is used up, because the whole group shares it: Alice's three answers and Dan's two. Refused requests do not count, so Alice's fourth call used none of the five.
Dollars. Bob calls the sales model three times:
for i in 1 2 3; do ask "$BOB_KEY" nvidia/nemotron-3.5-lightning:free | cut -c1-200; sleep 3; done
The first call ends with HTTP 200. It costs more than the limit of $0.001 for one person, so the second and third calls are refused with HTTP 429. Ambit checks the total before a request and adds the cost after the answer. A call that starts while the person is under the limit finishes and is charged in full.
The headers of a refused request tell the client how long to wait. retry-after is the number of seconds until midnight UTC:
curl -s -i http://llm.localtest.me:8080/v1/chat/completions -H "Authorization: Bearer $BOB_KEY" -H 'Content-Type: application/json' \
-d '{"model":"nvidia/nemotron-3.5-lightning:free","messages":[{"role":"user","content":"hi"}]}'
The answer looks like this (the header order and the number of seconds differ):
HTTP/1.1 429 Too Many Requests
retry-after: 56493
content-type: application/json
{"error":{"code":"budget_exceeded","message":"A daily Budget limit was reached: the request limit or the spending limit, for you or for your group. Requests are refused until midnight UTC (see the Retry-After header).","type":"insufficient_quota"}}
Use retry-after to know when the limit ends. The gateway sends no x-ratelimit-* headers.
See the use in the console
Open Budgets as an admin. Each budget shows today's requests or dollars, for the group and for each person, and the time the counters restart. A refused request is recorded in Audit like any other call.
You can also see the same numbers with the console API, signed in as an admin: GET /api/budgets.
Related
- Budgets: what is counted, streams, models without a price, and how prices are kept up to date.
- Different models for different teams: the groups and models used here.
- Alerts: alerts for the budget store.