方寸 Portal

AI Engineer Portal

Your personal operating system for career transition.

Private mode
Back to Learning Paths
Lesson

Cost, latency, and failure budgets

Make tradeoffs visible instead of treating providers like magical black boxes.

45 min
llm-app-foundationsphase-1portfolio

Cost, latency, and failure budgets

Why this matters

AI features have an operational cost profile that is fundamentally different from traditional software. A slow database query might add 20ms to a request. A slow LLM call adds 2–10 seconds. A single GPT-4o call can cost more than 1,000 database queries. If you do not actively manage cost, latency, and failure rates, they will manage you — usually in the form of a surprising invoice or a degraded user experience that takes weeks to diagnose.

Senior AI engineers think in budgets. Every feature has a cost-per-request budget, a latency budget (what the UX can tolerate), and a failure budget (how many errors per hour is acceptable before alerting). These budgets are engineering constraints, not afterthoughts.

Core concepts

Token cost calculation. Cost is a function of model, token counts, and current provider pricing. Build a cost calculator into your application layer:

# Representative pricing (check provider docs for current rates)
COST_PER_MILLION_TOKENS = {
    "claude-haiku-4-5":   {"input": 0.80,  "output": 4.00},
    "claude-sonnet-4-5":  {"input": 3.00,  "output": 15.00},
    "gpt-4o-mini":        {"input": 0.15,  "output": 0.60},
    "gpt-4o":             {"input": 2.50,  "output": 10.00},
    "llama-3.1-70b":      {"input": 0.00,  "output": 0.00},  # self-hosted
}

def calculate_cost_usd(model: str, input_tokens: int, output_tokens: int) -> float:
    rates = COST_PER_MILLION_TOKENS.get(model)
    if not rates:
        return 0.0
    return (input_tokens * rates["input"] + output_tokens * rates["output"]) / 1_000_000

Model routing. Not every request needs your best model. A routing layer saves significant cost:

  • Use the cheap model (Haiku, GPT-4o mini) for: short simple queries, classification tasks, reformatting, summarization of clear text
  • Use the expensive model (Sonnet, GPT-4o) for: complex reasoning, code generation, multi-step analysis, tasks where quality directly impacts revenue

Route based on measurable signals: input length, task type, user tier, or even a lightweight classifier.

Latency budgets. Break down where time goes in your feature:

StageTypical rangeTarget
Context assembly5–50ms<50ms
Provider call (non-streaming)1–15s<8s
Response parsing + validation1–10ms<20ms
Persistence5–30ms<50ms

Streaming for perceived speed. For conversational UIs, streaming changes the user experience dramatically. Instead of waiting 5 seconds for a complete response, the user sees text appearing within 300ms. Implement streaming as early as possible for any user-facing text generation feature.

Failure budgets. Define error rate thresholds before you ship:

  • Acceptable: <0.5% of requests fail after retries
  • Warning: 0.5–2% failure rate — investigate
  • Critical: >2% failure rate — alert on-call

Track failures by error type (provider errors, timeout, validation failure) separately. A spike in validation failures means your prompt changed and broke the output format. A spike in 429 errors means you are hitting rate limits and need to throttle or upgrade your tier.

Working example

A cost tracker middleware that logs tokens and alerts on budget overruns:

import time
import threading
from dataclasses import dataclass, field
from collections import defaultdict
from typing import Callable, Optional
import logging

logger = logging.getLogger(__name__)


@dataclass
class CostRecord:
    model: str
    input_tokens: int
    output_tokens: int
    cost_usd: float
    latency_ms: int
    timestamp: float = field(default_factory=time.time)
    feature: str = "unknown"


class CostTracker:
    """Thread-safe cost tracker with budget enforcement."""

    def __init__(
        self,
        daily_budget_usd: float = 10.0,
        alert_callback: Optional[Callable[[float, float], None]] = None,
    ):
        self.daily_budget_usd = daily_budget_usd
        self.alert_callback = alert_callback or self._default_alert
        self._records: list[CostRecord] = []
        self._lock = threading.Lock()

    def _default_alert(self, spent: float, budget: float) -> None:
        logger.warning(f"Cost alert: ${spent:.4f} spent of ${budget:.2f} daily budget")

    def record(self, record: CostRecord) -> None:
        with self._lock:
            self._records.append(record)
            daily_spent = self._daily_total_locked()
            if daily_spent >= self.daily_budget_usd * 0.8:
                self.alert_callback(daily_spent, self.daily_budget_usd)

    def _daily_total_locked(self) -> float:
        cutoff = time.time() - 86400  # last 24 hours
        return sum(r.cost_usd for r in self._records if r.timestamp >= cutoff)

    def daily_total(self) -> float:
        with self._lock:
            return self._daily_total_locked()

    def report(self) -> dict:
        with self._lock:
            by_model: dict = defaultdict(lambda: {"calls": 0, "input_tokens": 0, "output_tokens": 0, "cost_usd": 0.0, "latency_ms": []})
            cutoff = time.time() - 86400
            for r in self._records:
                if r.timestamp < cutoff:
                    continue
                m = by_model[r.model]
                m["calls"] += 1
                m["input_tokens"] += r.input_tokens
                m["output_tokens"] += r.output_tokens
                m["cost_usd"] += r.cost_usd
                m["latency_ms"].append(r.latency_ms)

            return {
                model: {
                    **stats,
                    "avg_latency_ms": int(sum(stats["latency_ms"]) / len(stats["latency_ms"])) if stats["latency_ms"] else 0,
                    "p95_latency_ms": int(sorted(stats["latency_ms"])[int(len(stats["latency_ms"]) * 0.95)]) if len(stats["latency_ms"]) > 1 else 0,
                    "latency_ms": None,  # exclude raw list from report
                }
                for model, stats in by_model.items()
            }

    def is_over_budget(self) -> bool:
        return self.daily_total() >= self.daily_budget_usd


# Global tracker instance (one per application)
tracker = CostTracker(daily_budget_usd=50.0)


def tracked_llm_call(
    call_fn: Callable,
    model: str,
    feature: str = "unknown",
    **call_kwargs,
):
    """Wrapper that instruments any LLM call with cost and latency tracking."""
    if tracker.is_over_budget():
        raise RuntimeError(
            f"Daily budget of ${tracker.daily_budget_usd:.2f} exceeded. "
            "Request blocked."
        )

    start = time.monotonic()
    response = call_fn(**call_kwargs)
    elapsed_ms = int((time.monotonic() - start) * 1000)

    # Extract token usage from response (works for both OpenAI and Anthropic shapes)
    usage = getattr(response, "usage", None)
    if usage:
        input_tokens = getattr(usage, "input_tokens", 0) or getattr(usage, "prompt_tokens", 0)
        output_tokens = getattr(usage, "output_tokens", 0) or getattr(usage, "completion_tokens", 0)
    else:
        input_tokens = output_tokens = 0

    cost = calculate_cost_usd(model, input_tokens, output_tokens)
    tracker.record(CostRecord(
        model=model,
        input_tokens=input_tokens,
        output_tokens=output_tokens,
        cost_usd=cost,
        latency_ms=elapsed_ms,
        feature=feature,
    ))

    return response

The tracker is thread-safe, covers 24-hour rolling windows, alerts at 80% budget utilization, and produces a per-model report you can expose on an internal admin dashboard.

Common mistakes

  1. No cost visibility until the invoice. By the time a monthly billing invoice shows an unexpected number, weeks of data are gone. Track cost per request in application logs from day one.

  2. Using your most powerful model for everything. A feature that runs Sonnet on every keypress in an autocomplete widget will burn money fast. Match model capability to task complexity.

  3. No latency budget on the user-facing path. A 12-second response is technically correct but practically broken. Set a hard timeout on the user-facing call and either stream the response or show a loading state.

  4. Tracking only failures, not error rates. Absolute failure counts tell you nothing about whether the error rate is acceptable. Track failures as a percentage of total requests.

  5. Ignoring output token growth. Input tokens are predictable (you control the context). Output tokens are not. If a prompt change or a new use case causes the model to produce much longer responses, your cost doubles without an obvious cause. Monitor average output token counts as a separate metric.

Try it yourself

Extend the CostTracker to support per-feature budgets. Different features in your app should have separate daily budgets — the code review assistant gets $20/day, the email drafter gets $5/day. When a feature exceeds its budget, log a warning and fall back to a cheaper model instead of blocking the request entirely.

Lesson Deep Dive

Ask a follow-up question about this lesson and get an AI-powered explanation.

Loading previous questions...