How to Build a Claude API Usage Quota System
How to Build a Claude API Usage Quota System
If you're exposing Claude to users, teams, or customers, you need a way to stop any single one of them from burning through your entire budget. A usage quota system tracks how much each account has consumed, compares it against a limit, and blocks or throttles requests once that limit is hit. This article walks through the moving parts you need and a working implementation pattern.
The short version: pick a unit of measurement (tokens, requests, or cost), track consumption per account in a fast, atomic store like Redis, check the counter before every call, and increment it with the real usage numbers Claude returns after every response. Everything else — reset windows, overage handling, alerts — builds on top of that core loop.
Pick what you're actually metering
Before writing any code, decide what "usage" means for your product:
- Requests per period — simplest to implement, but a 50-token request and a 50,000-token request count the same.
- Tokens (input + output) — more accurate, maps directly to Anthropic's own billing, and is what most teams end up using.
- Dollar cost — tokens converted to a price using the model's per-token rate. Best for billing customers directly, but requires keeping your rate table in sync with model pricing.
Most quota systems for internal tools or B2B SaaS use tokens, because the Claude API response already tells you exactly how many were consumed — no estimation needed.
{
"id": "msg_01...",
"usage": {
"input_tokens": 512,
"output_tokens": 128
}
}
The core loop
A quota system needs three operations, run on every API call:
- Check — is this account under its limit right now?
- Call — if yes, make the request to Claude.
- Record — increment the account's counter with the real tokens used.
The tricky part is making check-and-record atomic enough that concurrent requests from the same account don't slip through before the counter updates. Redis with INCR and a TTL handles this cleanly for most use cases.
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
const DAILY_TOKEN_LIMIT = 500_000;
async function checkQuota(accountId) {
const key = `quota:${accountId}:${todayKey()}`;
const used = parseInt((await redis.get(key)) || "0", 10);
return used < DAILY_TOKEN_LIMIT;
}
async function recordUsage(accountId, inputTokens, outputTokens) {
const key = `quota:${accountId}:${todayKey()}`;
const total = inputTokens + outputTokens;
const newTotal = await redis.incrby(key, total);
if (newTotal === total) {
// first write of the day — set expiry
await redis.expire(key, 60 * 60 * 24);
}
return newTotal;
}
function todayKey() {
return new Date().toISOString().slice(0, 10);
}
Wrap this around your Claude call:
async function callClaude(accountId, messages) {
const allowed = await checkQuota(accountId);
if (!allowed) {
throw new Error("quota_exceeded");
}
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages,
}),
});
const data = await response.json();
await recordUsage(
accountId,
data.usage.input_tokens,
data.usage.output_tokens
);
return data;
}
This is the skeleton. Everything from here is refinement.
Handling fixed windows vs sliding windows
The example above uses a fixed daily window — the counter resets at midnight UTC. This is simple but has a known flaw: a user can burn their entire quota in the last minute before reset and the first minute after, effectively doubling their limit in a short window.
If that matters for your use case, use a sliding window instead — store timestamped usage events and sum only the ones within the last 24 hours, or use a rolling bucket algorithm. It costs more in storage and compute, so only reach for it if burst abuse is a real risk.
Overage behavior: block, throttle, or bill
Once an account hits its limit, you have three options:
- Hard block — return a 429 and refuse further calls until the window resets. Simplest, but frustrating for users mid-task.
- Soft throttle — slow down response rate (e.g., queue requests, add delay) instead of rejecting outright.
- Overage billing — let the request through and charge for the extra usage at period end. Common for usage-based SaaS pricing.
Whatever you choose, always return machine-readable context so client apps can react:
{
"error": "quota_exceeded",
"limit": 500000,
"used": 503120,
"resets_at": "2024-06-02T00:00:00Z"
}
Per-key vs per-account quotas
If you issue separate API keys per environment, project, or team member, you'll likely want quotas enforced at the key level, with roll-up reporting at the account level. This mirrors how most API platforms structure limits — a team has an overall budget, and individual keys draw down against it with their own soft caps to catch runaway scripts before they eat the whole team's allowance.
Skipping the plumbing
Building this correctly — atomic counters, reset windows, per-key breakdowns, usage dashboards for your own users — is a real engineering project, not an afternoon script. If you're already routing Claude traffic through SubToAPI, you get per-key usage metadata on every response out of the box, which you can feed directly into the quota logic above instead of parsing and reconciling raw Anthropic responses yourself. Check the docs and messages API reference for the exact usage fields returned, or start with the quickstart if you're setting up keys for the first time. Plans start at Solo for solo projects and scale up to Team and Scale tiers with per-seat keys — see pricing for details, or sign up to try it with a free trial.
Testing your quota system
Before shipping, verify these cases explicitly:
- Concurrent requests from the same account near the limit don't both slip through
- The counter resets correctly at window boundaries (test across a UTC midnight)
- Error responses include enough detail for client apps to show a useful message
- Usage recording happens even if the downstream request fails partway (e.g., streaming cut off)
Questions
Should I track tokens or requests for quotas? Tokens, in almost all cases. Request counts ignore the fact that a single call can vary from a few dozen to tens of thousands of tokens, which makes cost control inaccurate.
What happens if a streamed response is cut off mid-generation? You won't get a final usage object if the connection drops before the stream completes. Record partial usage based on tokens received so far, or estimate conservatively, rather than skipping the increment entirely.
Do I need Redis specifically for this? No — any store with atomic increment and TTL support works (Postgres with SELECT ... FOR UPDATE, DynamoDB with conditional writes, etc.). Redis is popular because INCR and EXPIRE solve the atomicity and reset-window problems with almost no code.