Per-Tenant Cost Tracking and Quotas: AI SaaS FinOps

TL;DR — Per-tenant cost tracking and quotas are the FinOps foundation for AI SaaS. FinOps Foundation 2026: 73% of enterprises exceeded AI cost projections, 98% now manage AI spend. Reserve-commit pattern: reserve budget before LLM call (atomic Redis DECRBY), reconcile after. Multi-level quotas: per-minute (burst), per-hour (runaway), per-day (patterns), per-month (billing), per-agent (threads). Noisy neighbor prevention: per-tenant token budgets + rate limits + concurrency caps, sum of limits < 70% of provider ceiling. Atomic counters: Redis pipeline for concurrent accumulation, monthly reconciliation against provider invoices. Budgets (economic exposure, reserve/commit) vs quotas (policy boundaries, counter-based). Integrate with multi-tenant architecture, usage metering, BYOK, and white-label.

The FinOps Foundation's 2026 report found that "AI cost management is the number one skillset teams need to develop, and 98% of respondents now manage AI spend, up from 31% two years earlier." 73% of enterprises reported their AI costs exceeded original projections.

Cycles defines the core pattern: "Each tenant gets a defined budget. Every agent action reserves budget from the tenant's scope before execution. When a tenant's budget is exhausted, that tenant is denied. Other tenants are completely unaffected."

Cost Tracking Architecture

flowchart TD subgraph Request["Tenant Request"] Auth["JWT Auth
(tenant_id extracted)"] end subgraph PreCall["Pre-Call Enforcement"] CheckBudget{"Budget
Available?"} CheckQuota{"Quotas
OK?"} Reserve["Reserve Tokens
(atomic DECRBY)"] Reject["Reject
HTTP 429"] end subgraph LLM["LLM Call"] Dispatch["Dispatch to Provider"] Response["Response + Usage Block"] end subgraph PostCall["Post-Call Reconciliation"] Actual["Actual Tokens Used"] Reconcile["Reconcile vs Reserved"] Refund["Refund Overage"] Deduct["Deduct Shortfall"] end subgraph Tracking["Per-Tenant Tracking"] Redis["Redis Atomic Counters
(tokens, requests, cost)"] DB["Database
(historical usage, billing)"] end subgraph Quotas["Quota Horizons"] Min["Per-Minute
(burst protection)"] Hour["Per-Hour
(runaway prevention)"] Day["Per-Day
(usage patterns)"] Month["Per-Month
(billing period)"] Agent["Per-Agent
(thread limits)"] end subgraph Alerts["Alerting"] Budget80["Budget 80%
Warning"] Budget100["Budget 100%
Deny"] BurnRate["Burn Rate
Anomaly"] end Auth --> CheckBudget CheckBudget -->|yes| CheckQuota CheckBudget -->|no| Reject CheckQuota -->|yes| Reserve CheckQuota -->|no| Reject Reserve --> Dispatch Dispatch --> Response Response --> Actual Actual --> Reconcile Reconcile --> Refund Reconcile --> Deduct Refund --> Redis Deduct --> Redis Redis --> DB Redis --> Min Redis --> Hour Redis --> Day Redis --> Month Redis --> Agent Redis --> Budget80 Redis --> Budget100 Redis --> BurnRate

Budgets vs Quotas

Aspect Budgets Quotas
What it controls Total economic exposure (dollars, tokens) Policy boundaries (counts, rates, access)
How it works Real-time balance with reserve/commit Policy rule checked at request time
Enforcement Atomic — tracks cumulative spend Stateless or counter-based — tracks occurrences
Example "$500/month for this tenant" "Max 100 agent runs per day"
What it prevents Overspend, runaway costs Abuse, resource hogging, plan enforcement
When it triggers When balance is exhausted When count/rate is exceeded
Complexity Higher (atomic transactions) Lower (counter checks)

Quota Horizons

Horizon Purpose Enforcement Example
Per-minute Burst protection Redis counter + TTL 60 requests/min
Per-hour Runaway loop prevention Redis counter + TTL 500K tokens/hour
Per-day Usage pattern control Redis counter + TTL 1M tokens/day
Per-month Billing period alignment Database cumulative 10M tokens/month
Per-agent Thread-level limit In-memory counter 50K tokens/agent run

Implementation

import redis.asyncio as redis_async
from datetime import datetime, timezone

class PerTenantCostTracker:
    """Per-tenant cost tracking with reserve-commit pattern."""

    def __init__(self, redis_client, db, pricing):
        self.redis = redis_client
        self.db = db
        self.pricing = pricing  # Per-model token pricing

    async def check_and_reserve(
        self,
        tenant_id: str,
        model: str,
        estimated_tokens: int,
    ) -> dict:
        """Pre-call: check budget and quotas, reserve tokens."""

        # 1. Check per-minute rate limit
        rate_key = f"rate:{tenant_id}:min:{self._minute_key()}"
        current_rate = await self.redis.incr(rate_key)
        if current_rate == 1:
            await self.redis.expire(rate_key, 60)
        if current_rate > await self._get_rate_limit(tenant_id):
            raise RateLimitError(f"Rate limit exceeded for tenant {tenant_id}")

        # 2. Check per-tenant token budget (monthly)
        budget_key = f"budget:{tenant_id}:month:{self._month_key()}"
        remaining = int(await self.redis.get(budget_key) or 0)
        monthly_limit = await self._get_monthly_budget(tenant_id)

        if remaining + estimated_tokens > monthly_limit:
            raise BudgetExhaustedError(
                f"Monthly budget exhausted for tenant {tenant_id}: "
                f"{remaining}/{monthly_limit} tokens used"
            )

        # 3. Check daily quota
        daily_key = f"quota:{tenant_id}:day:{self._day_key()}"
        daily_used = int(await self.redis.get(daily_key) or 0)
        daily_limit = await self._get_daily_quota(tenant_id)

        if daily_used + estimated_tokens > daily_limit:
            raise QuotaExceededError(
                f"Daily quota exceeded for tenant {tenant_id}"
            )

        # 4. Reserve tokens atomically (reserve-commit pattern)
        # Use Redis pipeline for atomic multi-key operation
        pipe = self.redis.pipeline()
        pipe.incrby(budget_key, estimated_tokens)
        pipe.incrby(daily_key, estimated_tokens)
        pipe.incrby(f"reserved:{tenant_id}", estimated_tokens)
        results = await pipe.execute()

        return {
            "reserved": estimated_tokens,
            "budget_remaining": monthly_limit - results[0],
            "daily_remaining": daily_limit - results[1],
        }

    async def commit_usage(
        self,
        tenant_id: str,
        model: str,
        reserved_tokens: int,
        actual_input_tokens: int,
        actual_output_tokens: int,
        cache_read_tokens: int = 0,
        cache_write_tokens: int = 0,
    ):
        """Post-call: reconcile actual vs reserved, record usage."""
        actual_total = actual_input_tokens + actual_output_tokens
        difference = actual_total - reserved_tokens

        pipe = self.redis.pipeline()

        if difference > 0:
            # Used more than reserved — deduct shortfall
            pipe.incrby(f"budget:{tenant_id}:month:{self._month_key()}", difference)
            pipe.incrby(f"quota:{tenant_id}:day:{self._day_key()}", difference)
        elif difference < 0:
            # Used less than reserved — refund overage
            pipe.decrby(f"budget:{tenant_id}:month:{self._month_key()}", abs(difference))
            pipe.decrby(f"quota:{tenant_id}:day:{self._day_key()}", abs(difference))

        # Clear reservation
        pipe.decrby(f"reserved:{tenant_id}", reserved_tokens)
        await pipe.execute()

        # Calculate cost
        cost = self._calculate_cost(
            model, actual_input_tokens, actual_output_tokens,
            cache_read_tokens, cache_write_tokens,
        )

        # Record in database for billing and analytics
        await self.db.insert("usage_records", {
            "tenant_id": tenant_id,
            "model": model,
            "input_tokens": actual_input_tokens,
            "output_tokens": actual_output_tokens,
            "cache_read_tokens": cache_read_tokens,
            "cache_write_tokens": cache_write_tokens,
            "total_tokens": actual_total,
            "cost_usd": cost,
            "timestamp": datetime.now(timezone.utc),
        })

        # Check alert thresholds
        await self._check_alerts(tenant_id)

        return {
            "actual_tokens": actual_total,
            "cost_usd": cost,
            "reconciled": True,
        }

    async def get_tenant_usage(self, tenant_id: str, period: str = "month") -> dict:
        """Get usage summary for a tenant."""
        return await self.db.aggregate("usage_records", [
            {"$match": {
                "tenant_id": tenant_id,
                "timestamp": {"$gte": self._period_start(period)},
            }},
            {"$group": {
                "_id": "$model",
                "total_tokens": {"$sum": "$total_tokens"},
                "input_tokens": {"$sum": "$input_tokens"},
                "output_tokens": {"$sum": "$output_tokens"},
                "cost_usd": {"$sum": "$cost_usd"},
                "request_count": {"$sum": 1},
            }},
        ])

    async def reconcile_with_provider(self, provider: str, month: str):
        """Monthly reconciliation: compare per-tenant totals vs provider invoice."""
        provider_total = await self._get_provider_usage(provider, month)
        tenant_totals = await self.db.aggregate("usage_records", [
            {"$match": {
                "model": {"$regex": f"^{provider}"},
                "timestamp": {"$gte": self._month_start(month)},
            }},
            {"$group": {
                "_id": "$tenant_id",
                "total_tokens": {"$sum": "$total_tokens"},
            }},
        ])

        platform_total = sum(t["total_tokens"] for t in tenant_totals)
        gap = provider_total - platform_total

        if abs(gap) > provider_total * 0.01:  # >1% gap
            await self._alert_reconciliation_gap(provider, month, gap, tenant_totals)

        return {
            "provider_total": provider_total,
            "platform_total": platform_total,
            "gap": gap,
            "gap_pct": (gap / provider_total * 100) if provider_total else 0,
        }

    def _calculate_cost(self, model, input_tokens, output_tokens,
                        cache_read=0, cache_write=0) -> float:
        """Calculate cost based on model pricing."""
        pricing = self.pricing.get(model, {})
        return (
            (input_tokens / 1_000_000) * pricing.get("input", 0) +
            (output_tokens / 1_000_000) * pricing.get("output", 0) +
            (cache_read / 1_000_000) * pricing.get("cache_read", 0) +
            (cache_write / 1_000_000) * pricing.get("cache_write", 0)
        )

Tier-Based Budget Allocation

Tier Monthly Budget Daily Quota Rate Limit Concurrency Models
Free $10 (50K tokens) 10K tokens 10 req/min 2 GPT-4o-mini only
Starter $50 (200K tokens) 50K tokens 30 req/min 5 GPT-4o-mini, Claude Haiku
Pro $200 (1M tokens) 200K tokens 60 req/min 10 All models
Business $1,000 (5M tokens) 500K tokens 120 req/min 20 All models + fine-tunes
Enterprise Custom Custom Custom Custom All + BYOK

Per-Tenant Cost Tracking Checklist

  • [ ] Implement per-tenant token budget tracking (monthly, daily)
  • [ ] Use atomic Redis counters (DECRBY/INCRBY) for concurrent accumulation
  • [ ] Implement reserve-commit pattern: reserve before call, reconcile after
  • [ ] Enforce pre-call, not post-call — cost is incurred before response arrives
  • [ ] Set per-minute rate limits (burst protection)
  • [ ] Set per-hour budgets (runaway loop prevention)
  • [ ] Set per-day quotas (usage pattern control)
  • [ ] Set per-month budgets (billing period alignment)
  • [ ] Set per-agent-instance limits (thread-level caps)
  • [ ] Configure per-tenant concurrency caps (in-flight calls)
  • [ ] Keep sum of per-tenant limits below 70% of provider ceiling
  • [ ] Reserve 30% headroom for unmetered jobs and spikes
  • [ ] Record actual usage from response usage block — never estimate
  • [ ] Track: input tokens, output tokens, cache read, cache write, cost
  • [ ] Store usage records in database for billing and analytics
  • [ ] Implement monthly reconciliation against provider invoices
  • [ ] Alert on reconciliation gaps > 1%
  • [ ] Funnel all LLM calls through single metering chokepoint
  • [ ] Verify no LLM calls bypass the metering layer
  • [ ] Set up budget alerts: 50% warning, 80% alert, 100% deny
  • [ ] Implement burn rate anomaly detection
  • [ ] Configure tier-based budget allocation (Free, Starter, Pro, Business, Enterprise)
  • [ ] Set model access restrictions per tier
  • [ ] Implement per-tenant usage dashboard
  • [ ] Track cost per tenant, per model, per feature, per user
  • [ ] Implement cost attribution: tenant + user + feature + model tags
  • [ ] Set up usage metering for billing
  • [ ] Integrate with multi-tenant architecture
  • [ ] Support BYOK billing (platform fee, not token markup)
  • [ ] Integrate with white-label reseller billing
  • [ ] Apply zero-trust architecture to cost data
  • [ ] Set up platform monitoring for cost metrics
  • [ ] Track: cost per tenant, cost per request, cost per agent run
  • [ ] Implement graceful degradation: degrade to cheaper model when budget low
  • [ ] Consider semantic answer caching to reduce costs
  • [ ] Document failed request policy: do failed requests count against budget?
  • [ ] Test concurrent budget exhaustion: verify atomic isolation
  • [ ] Test noisy neighbor: verify one tenant's burst doesn't affect others
  • [ ] Quarterly review: audit cost allocation accuracy, adjust tier limits
  • [ ] Consider AI incident response for cost overruns

FAQ

What is per-tenant cost tracking in AI SaaS?

Per-tenant cost tracking is the system that attributes every token, API call, and compute resource to the specific tenant that consumed it, enabling accurate billing and budget enforcement. Unlike traditional SaaS where cost is per-seat, AI SaaS costs scale with usage (tokens), making per-tenant tracking critical. The FinOps Foundation's 2026 report found 73% of enterprises exceeded AI cost projections and 98% now manage AI spend. Implementation: every LLM response contains a usage block (input tokens, output tokens, cache metrics) — attribute it to the tenant on the spot. Use atomic Redis counters for concurrent accumulation. Reconcile per-tenant totals against provider invoices monthly. Never fill gaps with estimates — record only real numbers.

What is the reserve-commit pattern for token budgets?

The reserve-commit pattern prevents overspend by reserving budget before the LLM call and reconciling after. (1) Reserve — before dispatching to the LLM, atomically deduct an estimated token count from the tenant's budget using Redis DECRBY. (2) Execute — make the LLM call. (3) Commit — after the response, compare actual tokens vs estimated. If actual > reserved, deduct the difference. If actual < reserved, refund the overage. This ensures a tenant cannot exceed their budget even if the estimate is wrong. The reservation is atomic and scoped: Tenant A's reservation draws only from Tenant A's balance. Two tenants cannot race on the same budget. Enforce pre-call, not post-call — post-call enforcement means the cost is already incurred before the limit can be applied.

What quota horizons should AI SaaS enforce?

AI SaaS should enforce multiple quota horizons simultaneously: (1) Per-minute rate limits — burst protection, preventing a single agent from flooding the API. (2) Per-hour budgets — preventing runaway loops from accumulating costs before a human can intervene. (3) Per-day quotas — aligning with daily usage patterns and preventing budget exhaustion in a single session. (4) Per-month quotas — aligning with billing periods and committed-use contracts. (5) Per-agent-instance limits — ensuring individual agent threads can't exceed their allocation even within a shared tenant pool. Enforcing only monthly limits leaves burst attacks uncontrolled. Enforcing only per-minute limits ignores budget accumulation. All horizons should coexist and be enforced pre-call.

How do you prevent the noisy neighbor problem in AI SaaS?

The noisy neighbor problem occurs when one tenant's runaway agent exhausts the shared LLM API quota, causing 429 errors for all other tenants. Prevent it with: (1) Per-tenant token budgets — enforce before LLM dispatch, not after. (2) Per-tenant rate limits — requests per minute, independent of token count. (3) Per-tenant concurrency caps — simultaneous in-flight calls per tenant. (4) Reserved slices — allocate upstream headroom explicitly: reserved slice per tenant plus shared burst pool. (5) Keep sum of per-tenant limits below 70% of actual provider ceiling — 30% headroom for unmetered jobs and spikes. (6) Per-agent-instance limits — individual agent threads can't exceed allocation. Without per-tenant quotas, one tenant's burst eats everyone's capacity.

What is the difference between budgets and quotas?

Budgets control total economic exposure (dollars, tokens) with real-time balance tracking using reserve/commit. Example: "$500/month for this tenant." Budgets are atomic — they track cumulative spend and trigger when balance is exhausted. Quotas control policy boundaries (counts, rates, access) with stateless or counter-based checks. Example: "Max 100 agent runs per day" or "Max 3 concurrent agents." Quotas are simpler — they check a counter or policy rule, not a running financial balance. In practice, you need both. A tenant might have a $1,000/month budget AND a quota of 500 runs per day. The budget prevents total overspend. The quota prevents a burst pattern that could exhaust the monthly budget in a single day. Together, they define a predictable usage envelope.


Want a self-hosted AI company brain that does all of this out of the box?
Book a demo →