Set OpenAI's hard limit on a project
- In the OpenAI platform, open the project and go to Project settings → Limits.
- Under Spend, choose Edit spend limit and enter a monthly amount.
- Turn on Enforce a hard limit and save.
From then on, a request billed to that project over the limit fails with 429 and code project_spend_limit_exceeded. The organization-wide limit works the same way across every project and fails with organization_spend_limit_exceeded. Both reset with the next monthly cycle.
Read three details before relying on it:
- It is not exact. OpenAI says enforcement is not instantaneous, so recorded spend can run slightly past the limit. Set it below the number you cannot exceed.
- Alerts do not stop anything. Spend alerts notify you; only the enforced hard limit refuses requests.
- Your SDK will retry it. The official SDKs retry every
429twice by default, this one included. Handleproject_spend_limit_exceededyourself, as in the 429 guide.
What OpenAI's limit cannot express
| You need | OpenAI's built-in limit | How to get it |
|---|---|---|
| A cap for the whole company | Yes, organization hard limit, monthly | Built in |
| A cap per team, service or environment | Yes, if each has its own project | One project per service; give each its own keys |
| A cap per API key | No; limits are per project | One project per key, or a check in your code |
| A cap per end customer | No | A check in front of the API (below) |
| A daily or weekly cap | No; monthly only | A check in front of the API (below) |
| One cap across OpenAI and Anthropic | No; each provider caps only itself | A check in front of both |
| No overshoot under concurrency | No; enforcement lags | Reserve the worst case before each call |
400 invalid_request_error, not 429.Projects are the right first move for teams and environments. For anything finer than a project, the limit has to live in your code or in a gateway.
A cap per customer and per day, checked before the call
The pattern that holds under concurrency is reserve, call, settle:
- Reserve the most the call can cost (its prompt plus
max_completion_tokensat your rates) against the customer's remaining budget. If it does not fit, refuse without calling OpenAI. - Call OpenAI.
- Settle: replace the reservation with the real cost from
usage.
Checking spend after the call instead lets ten concurrent requests all pass a check only one of them fits under. Reserving first closes that gap, which is the same gap OpenAI's own limit has.
import OpenAI, { APIError } from "openai";
// No SDK retries: it would retry the spend-limit 429 below too (see the 429 guide).
const client = new OpenAI({ maxRetries: 0 });
const MODEL = "gpt-5.4-mini";
// USD per 1M tokens. Use the rates your own contract carries.
const RATES = { input: 0.75, output: 4.5 };
const costUsd = (inputTokens: number, outputTokens: number) =>
(inputTokens * RATES.input + outputTokens * RATES.output) / 1_000_000;
export class CapReached extends Error {}
/**
* Spend per customer per UTC day. This keeps the ledger in memory, which is
* correct for one process only: with several servers, keep it in Redis or
* Postgres and make reserve() a single atomic increment-and-check.
*/
export class DailyCap {
private spent = new Map<string, number>();
private readonly limitUsd: number;
constructor(limitUsd: number) {
this.limitUsd = limitUsd;
}
private key(customer: string) {
return `${customer}:${new Date().toISOString().slice(0, 10)}`;
}
reserve(customer: string, usd: number) {
const used = this.spent.get(this.key(customer)) ?? 0;
if (used + usd > this.limitUsd) {
throw new CapReached(`not sent: ${customer} has used $${used.toFixed(4)} of its $${this.limitUsd} daily cap`);
}
this.spent.set(this.key(customer), used + usd);
}
// Replace the reservation with what the call actually cost.
settle(customer: string, reservedUsd: number, actualUsd: number) {
const used = this.spent.get(this.key(customer)) ?? 0;
this.spent.set(this.key(customer), Math.max(0, used - reservedUsd + actualUsd));
}
}
export async function answerFor(customer: string, prompt: string, cap: DailyCap, maxTokens = 500) {
// Reserve the most this call can cost BEFORE making it, so ten concurrent
// calls cannot all pass a check that only one of them fits under.
// Two characters per token overestimates the prompt, on purpose.
const reserved = costUsd(Math.ceil(prompt.length / 2), maxTokens);
cap.reserve(customer, reserved);
try {
const res = await client.chat.completions.create({
model: MODEL,
max_completion_tokens: maxTokens,
messages: [{ role: "user", content: prompt }],
});
cap.settle(customer, reserved, costUsd(res.usage?.prompt_tokens ?? 0, res.usage?.completion_tokens ?? 0));
return res.choices[0]?.message.content ?? "";
} catch (err) {
cap.settle(customer, reserved, 0);
// OpenAI's own project hard limit: stop, do not retry, tell a human.
if (err instanceof APIError && err.code === "project_spend_limit_exceeded") {
throw new CapReached("OpenAI project hard limit reached");
}
throw err;
}
}
const cap = new DailyCap(0.005);
for (let i = 1; i <= 3; i++) {
try {
await answerFor("cus_123", "Write a one-line welcome for a new user.", cap);
console.log(`call ${i}: served`);
} catch (err) {
if (!(err instanceof CapReached)) throw err;
console.log(`call ${i}: refused, ${err.message}`);
}
}import datetime
import math
import threading
import openai
from openai import OpenAI
# No SDK retries: it would retry the spend-limit 429 below too (see the 429 guide).
client = OpenAI(max_retries=0)
MODEL = "gpt-5.4-mini"
# USD per 1M tokens. Use the rates your own contract carries.
RATES = {"input": 0.75, "output": 4.5}
def cost_usd(input_tokens: int, output_tokens: int) -> float:
return (input_tokens * RATES["input"] + output_tokens * RATES["output"]) / 1_000_000
class CapReached(Exception):
pass
class DailyCap:
"""Spend per customer per UTC day.
This keeps the ledger in memory, which is correct for one process only:
with several servers, keep it in Redis or Postgres and make reserve() a
single atomic increment-and-check.
"""
def __init__(self, limit_usd: float):
self.limit_usd = limit_usd
self.spent: dict[str, float] = {}
self.lock = threading.Lock()
def _key(self, customer: str) -> str:
return f"{customer}:{datetime.datetime.now(datetime.timezone.utc).date()}"
def reserve(self, customer: str, usd: float) -> None:
with self.lock:
used = self.spent.get(self._key(customer), 0.0)
if used + usd > self.limit_usd:
raise CapReached(f"not sent: {customer} has used ${used:.4f} of its ${self.limit_usd} daily cap")
self.spent[self._key(customer)] = used + usd
def settle(self, customer: str, reserved_usd: float, actual_usd: float) -> None:
"""Replace the reservation with what the call actually cost."""
with self.lock:
used = self.spent.get(self._key(customer), 0.0)
self.spent[self._key(customer)] = max(0.0, used - reserved_usd + actual_usd)
def answer_for(customer: str, prompt: str, cap: DailyCap, max_tokens: int = 500) -> str:
# Reserve the most this call can cost BEFORE making it, so ten concurrent
# calls cannot all pass a check that only one of them fits under.
# Two characters per token overestimates the prompt, on purpose.
reserved = cost_usd(math.ceil(len(prompt) / 2), max_tokens)
cap.reserve(customer, reserved)
try:
res = client.chat.completions.create(
model=MODEL,
max_completion_tokens=max_tokens,
messages=[{"role": "user", "content": prompt}],
)
except openai.APIStatusError as err:
cap.settle(customer, reserved, 0.0)
# OpenAI's own project hard limit: stop, do not retry, tell a human.
if err.code == "project_spend_limit_exceeded":
raise CapReached("OpenAI project hard limit reached") from err
raise
except Exception:
cap.settle(customer, reserved, 0.0)
raise
usage = res.usage
cap.settle(customer, reserved, cost_usd(usage.prompt_tokens, usage.completion_tokens) if usage else 0.0)
return res.choices[0].message.content or ""
if __name__ == "__main__":
cap = DailyCap(0.005)
for i in range(1, 4):
try:
answer_for("cus_123", "Write a one-line welcome for a new user.", cap)
print(f"call {i}: served")
except CapReached as err:
print(f"call {i}: refused, {err}")The ledger here is in memory, which is correct only within one process. With more than one server, keep it in Redis or Postgres and make the reservation one atomic increment-and-check. Two more decisions to make on purpose:
- Models you have no rate for. A cap cannot count what it cannot price. Refuse them while a cap applies, or give them a rate.
- Your rates, not list price. If your contract carries a negotiated rate, cap on that, or your cap and your invoice will disagree.
When a limit is reached
- Do not retry
- if the error is
project_spend_limit_exceeded,organization_spend_limit_exceededor your own cap. The same request fails the same way until the limit resets or is raised. - Do not fail over to another provider
- if a spend cap refused the request. Moving it to Anthropic spends around the limit you set.
- Tell the user something specific
- if a per-customer cap refused them: what happened and when it resets.
- Alert a human
- if the organization or project limit is reached; production traffic for that project has stopped.
Questions
- Can I set a spending limit per API key in OpenAI?
- Not directly. OpenAI's hard limits are per organization and per project. Give each key its own project, or check spend per key in your own code or a gateway before each call.
- What error does OpenAI return when the hard limit is reached?
- A 429 with code project_spend_limit_exceeded for a project limit, or organization_spend_limit_exceeded for the organization limit. Retrying will not succeed until the limit resets or is raised.
- Does OpenAI's hard limit stop spend exactly at the limit?
- No. OpenAI says enforcement is not instantaneous, so recorded spend can slightly exceed the configured amount. Set the limit below the figure you cannot pass.