Skip to content
prune.

How to limit OpenAI usage per user in your SaaS app

Give every user a monthly dollar budget, checked before the call, with the provider limit as a backstop.

Quick answer

OpenAI cannot see your users, so it cannot cap them. Attach a user ID to every call, convert each response's token usage to dollars, and check the user's monthly spend before the next call. Block or downgrade when they cross their budget. Keep an OpenAI project hard limit on as the backstop for everyone at once.

What OpenAI gives you

OpenAI has two kinds of spend controls. Spend alerts send a notification and let traffic continue. Hard spend limits make requests fail with 429 and project_spend_limit_exceeded or organization_spend_limit_exceeded once tracked spend reaches the amount. Both work on a whole project or organization, per month. OpenAI also says enforcement is not instantaneous, so spend can slightly exceed the limit.

That protects your bill. It does not stop one heavy user from eating the budget meant for everyone else, and when it trips, every user is locked out at once.

1. Budget in dollars, not tokens

A token cap sounds simpler, but output tokens cost several times more than input, and models differ by 10x or more. A user on your premium model can blow through a token cap's worth of money in a few long answers. Turn every response's usage into a cost with your model's price, and budget that.

2. Check before the call, record after

Read the user's spend for the current month before you call the model. If they are over budget, do not call: show an upgrade prompt or switch them to a cheaper model. After the call, add the real cost from the response. Key the counter by month so it resets on the 1st without a cron job.

per_user_budget.py
from datetime import datetime, timezone
from openai import OpenAI

client = OpenAI()

# USD per 1M tokens for the models you call. Keep this in config.
MODEL = "gpt-5.4-mini"
PRICES = {MODEL: (0.75, 4.50)}

def month_key(user_id: str) -> str:
    now = datetime.now(timezone.utc)
    return f"spend:{user_id}:{now:%Y-%m}"   # resets on the 1st, UTC

def ask(user_id: str, budget_usd: float, messages: list) -> str:
    spent = float(redis.get(month_key(user_id)) or 0)
    if spent >= budget_usd:
        raise BudgetExceeded(user_id, spent, budget_usd)   # show an upgrade prompt

    resp = client.chat.completions.create(model=MODEL, messages=messages)

    inp, out = PRICES[MODEL]
    cost = (resp.usage.prompt_tokens * inp + resp.usage.completion_tokens * out) / 1e6
    redis.incrbyfloat(month_key(user_id), cost)
    return resp.choices[0].message.content

Two traps. Concurrent requests from the same user can all pass the check before any of them records spend, so expect small overshoots or add a per-user lock. And streaming responses only report usage at the end, so ask for it with stream_options: {"include_usage": true}.

3. Pick what happens at the limit

  • Free users: block and show the upgrade path. This is your conversion moment.
  • Paid users: soft limit first (email at 80%), hard limit at a multiple of plan price.
  • Internal tools: downgrade to a cheaper model instead of blocking.

4. Keep the project hard limit on

Your per-user check lives in your code, so a bug in your code can skip it. Set an OpenAI project hard limit at a number you could survive. If you serve a few large customers, one project per customer gives each one its own hard limit. That does not scale to thousands of self-serve users.

Doing this with Prune

Prune is an AI gateway, so the per-user check runs before the call reaches OpenAI, outside your app. Send your user ID in X-Prune-Customer-Id and set a budget per customer, in the dashboard or with one API call:

set-budget.sh
curl -X PUT https://api.withprune.com/v1/usage/customer-budgets \
  -H "Authorization: Bearer $PRUNE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customer_id": "user_123", "budget_usd": 5}'

Budgets are monthly and reset on the 1st (UTC). When a customer crosses theirs, the call is rejected with 429 and X-Prune-Shield-Code: customer_budget, and the body includes spent_usd, cap_usd and period, so you can tell the user exactly where they stand. You can also budget a single feature for a customer by adding feature_id.

ask.py
import os
from openai import OpenAI, RateLimitError

client = OpenAI(
    base_url="https://api.withprune.com/v1",
    api_key=os.environ["PRUNE_API_KEY"],   # OpenAI key stays in the Prune vault
)

def ask(user_id: str, messages: list) -> str:
    try:
        resp = client.chat.completions.create(
            model="gpt-5.4-mini",
            messages=messages,
            extra_headers={"X-Prune-Customer-Id": user_id},
        )
    except RateLimitError as e:
        if e.response.headers.get("X-Prune-Shield-Code") == "customer_budget":
            raise BudgetExceeded(user_id) from e   # show an upgrade prompt
        raise
    return resp.choices[0].message.content

Every call is also attributed to that customer, so the Customers page shows cost per user next to what they pay you. A per-key daily spend cap in Shield stays on as the backstop. See all the codes on error codes.

With a single line of code, Prune cuts your AI bill and protects your team from runaway spend, leaked keys and dangerous calls.

Related

Have an account? Sign in · PyPI.