TAI BUI
Back to curriculum
BuildPython

Caching, Rate Limiting & Cost Optimization

Most AI startups do not die from bad models. They die from bad unit economics. A single GPT-4o call costs fractions of a cent. Ten thousand users making ten calls per day costs $250 in input tokens alone, before you charge a single dollar. The companies that survive treat each API call as a financial transaction, not a function call.

TIME: ~45 minutesPREREQUISITES: Phase 11 Lesson 09 (Function Calling)

Learning Objectives

  • Implement semantic caching that serves repeated or similar queries from cache instead of making a new API call
  • Calculate per-request costs across providers and implement token-aware rate limiting and budget alerts
  • Build a cost optimization layer with prompt compression, model routing (expensive vs cheap), and response caching
  • Design a tiered caching strategy using exact match, semantic similarity, and prefix caching for different query types

The Problem

You build a RAG chatbot. It works beautifully. Users love it.

Then the invoice arrives.

GPT-5 costs $5 per million input tokens and $15 per million output. Claude Opus 4.7 costs $15 input / $75 output. Gemini 3 Pro costs $1.25 input / $5 output. GPT-5-mini is $0.25/$2. Prices below are illustrative; check the provider's current pricing page.

Here is the math that kills startups, using GPT-4o pricing ($2.50 input, $10 output per million tokens):

  • 10,000 daily active users
  • 10 queries per user per day
  • 1,000 input tokens per query (system prompt + context + user message)
  • 500 output tokens per response

Daily input cost: 10,000 x 10 x 1,000 / 1,000,000 x $2.50 = $250/day Daily output cost: 10,000 x 10 x 500 / 1,000,000 x $10.00 = $500/day Monthly total: $22,500/month

That is just the LLM. Add embeddings, vector database hosting, infrastructure. You are looking at $30,000/month for a chatbot.

The brutal part: 40-60% of those queries are near-duplicates. Users ask the same questions in slightly different words. Your system prompt is identical across requests and gets billed on each one. Context documents retrieved by RAG repeat across users who ask about the same topic.

You are paying full price for redundant computation.

Warm-up0/3
1. An agent from the function-calling lesson resends the same 1,200-token system prompt and tool definitions on every call, and one task takes 8 calls. What happens to input-token spend?Warm-up
Eight request rows for one agent task. Every row starts with the same 1,200-token block labelled system prompt plus tool definitions, followed by a dashed block of new content. A question mark stands for the input-token spend.
Schematic. The 1,200 tokens and the 8 calls come from the question; the size of the new content is not given.
2. Two function-calling requests have identical messages and tool definitions, and both use temperature 0. What is the most reasonable expectation?Warm-up
Two request cards, A and B. Both list messages identical, tools identical and temperature 0. Under each is an output box with a question mark asking whether it matches the other.
Schematic of the two requests the question describes.
3. A slow, pure function is called thousands of times with the same few arguments. What is the standard way to avoid repeating the work?Warm-up
A call log of eight calls to a function named slow with arguments 7 and 3. Each call has a long bar labelled full work every time.
Illustrative call stream. The function and arguments are made up.

The Concept

The Cost Anatomy of an LLM Call

Every API call has five cost components.

Mermaid diagram

System prompts are the silent killer. A 1,500-token system prompt sent with every request costs $3,750 per million requests just for that prefix. At 100K requests per day, that is $375/day, or $11,250/month, for text that doesn't change.

Provider Caching: Built-in Discounts

All three major providers offer provider-side prompt caching in 2026, but the mechanics differ. See Phase 11 · 15 for the deep dive.

ProviderMechanismDiscountMinimumCache Duration
AnthropicExplicit cache_control markers90% on cache hits (pay 25% extra on write)1,024 tokens (Sonnet/Opus), 2,048 (Haiku)5 min default; 1h extended (2x write premium)
OpenAIAutomatic prefix matching50% on cache hits1,024 tokensBest-effort up to 1 hour
Google GeminiExplicit CachedContent API~75% reduction (plus storage)4,096 (Flash) / 32,768 (Pro)User-configurable TTL

Anthropic's approach is explicit. You mark sections of your prompt with cache_control: {"type": "ephemeral"}. The first request pays a 25% write premium. Subsequent requests with the same prefix get a 90% discount. A 2,000-token system prompt that costs $0.005 normally costs $0.0005 on cache hits. Over 100K requests, that saves $450/day.

OpenAI's approach is automatic. Any prompt prefix that matches a previous request gets a 50% discount. No markers needed. The tradeoff: less discount, less control, but zero implementation effort.

Semantic Caching: Your Custom Layer

Provider caching only works for identical prefixes. Semantic caching handles the harder case: different queries with the same meaning.

"What is the return policy?" and "How do I return an item?" are different strings but identical intent. A semantic cache embeds both queries, computes cosine similarity, and returns the cached response if similarity exceeds a threshold (typically 0.92-0.95).

Mermaid diagram

The embedding costs are negligible. OpenAI's text-embedding-3-small costs $0.02 per million tokens. Checking the cache costs almost nothing compared to a full LLM call.

Exact Caching: Hash and Match

For deterministic calls (temperature=0, same model, same prompt), exact caching is simpler and faster. Hash the full prompt, check the cache, return if found.

This works well for:

  • System prompt + fixed context + identical user queries
  • Function calling with identical tool definitions
  • Batch processing where the same document gets processed multiple times

Rate Limiting: Protecting Your Budget

Rate limiting protects your budget as much as it keeps usage fair.

Token bucket algorithm: each user gets a bucket of N tokens that refills at rate R per second. A request consumes tokens from the bucket. If the bucket is empty, the request is rejected. This allows bursts (use the full bucket at once) while enforcing an average rate.

Per-user quotas: set daily/monthly token limits per user tier.

TierDaily Token LimitMax Requests/minModel Access
Free50,00010GPT-4o-mini only
Pro500,00060GPT-4o, Claude Sonnet
Enterprise5,000,000300All models

Model Routing: Right Model for the Right Job

Not every query needs GPT-4o.

"What time does the store close?" does not require a $10/M-output model. GPT-4o-mini at $0.60/M output handles it fine. Claude Haiku at $1.25/M output handles it. A simple classifier routes cheap queries to cheap models and complex queries to expensive models.

Mermaid diagram

A well-tuned router saves 40-70% on model costs alone.

Cost Tracking: Know Where the Money Goes

You cannot optimize what you do not measure. Log every API call with:

  • Timestamp
  • Model name
  • Input tokens
  • Output tokens
  • Latency (ms)
  • Computed cost ($)
  • User ID
  • Cache hit/miss
  • Request category

This data reveals which features are expensive, which users are heavy consumers, and where caching has the most impact.

Batching: Bulk Discounts

OpenAI's Batch API processes requests asynchronously at a 50% discount. You submit a batch of up to 50,000 requests, and results come back within 24 hours.

Use batching for:

  • Nightly document processing
  • Bulk classification
  • Evaluation runs
  • Data enrichment pipelines

Not for: real-time user-facing queries (latency matters).

Budget Alerts and Circuit Breakers

A circuit breaker stops spending when you hit a limit. Without one, a bug or abuse can burn through your monthly budget in hours.

Set three thresholds:

  1. Warning (70% of budget): send an alert
  2. Throttle (85% of budget): switch to cheaper models only
  3. Stop (95% of budget): reject new requests, return cached responses only

The Optimization Stack

Apply these techniques in order. Each layer compounds on the previous ones.

LayerTechniqueTypical SavingsImplementation Effort
1Provider prompt caching30-50%Low (add cache markers)
2Exact caching10-20%Low (hash + dict)
3Semantic caching15-30%Medium (embeddings + similarity)
4Model routing40-70%Medium (classifier)
5Rate limitingBudget protectionLow (token bucket)
6Prompt compression10-30%Medium (rewrite prompts)
7Batching50% on eligibleLow (batch API)

A RAG app applying layers 1-5 typically reduces costs from $22,500/month to $4,000-6,000/month. That is the difference between burning runway and building a business.

Real Savings: Before and After

Here is an example breakdown for a RAG chatbot serving 10,000 DAU.

MetricBefore OptimizationAfter OptimizationSavings
Monthly LLM cost$22,500$5,20077%
Avg cost per query$0.0075$0.001777%
Cache hit rate0%52%--
Queries routed to mini0%65%--
P95 latency2,800ms900ms (cache hits: 50ms)68%
Monthly embedding cost$0$180(new cost)
Total monthly cost$22,500$5,38076%

The embedding cost for semantic caching ($180/month) pays for itself within the first hour of cache hits.

Check: The Concept0/30
1. What is prompt caching?Define
An Anthropic messages request with a system block that carries a cache_control ephemeral marker, and below it three usage lines whose values are question marks.
Excerpt typed from the lesson's Use It example. Values left as ?.
2. What does a semantic cache do when a new query arrives?Define
A cache box with one stored entry, the query What is the return policy? and its response. An incoming query How do I return an item? points at the cache and a question mark asks for the result.
Entry and queries are the lesson's demo examples.
3. What is exact caching?Define
A log of three requests to gpt-4o-mini at temperature 0.0: the return-policy question twice, then a store-hours question. Each row has a question mark under the heading model call made.
Queries are the lesson's demo examples.
4. What is a token bucket?Define
A full gauge reading 50,000 of 50,000 tokens, with capacity 50,000 and refill_rate 500 per second written below, and a 1,000-token request arrow ending in a question mark.
Free-tier values from the lesson's TokenBucketRateLimiter.
5. What is model routing?Define
Two queries on the left, a store-hours question and a request to write code for a binary search tree, and two models on the right with output prices: gpt-4o-mini at $0.60 and gpt-4o at $10.00 per million. A question mark sits between them.
Queries are the lesson's demo examples; prices are from the lesson text.
6. What is cost tracking in an LLM application?Define
Four calls with model names and token counts flowing out of the app into a box titled what happens to each call afterwards, which holds a question mark.
Models and token counts are from the lesson's cost-breakdown demo.
7. What is a circuit breaker in LLM cost control?Define
A line chart of cumulative spend over a month rising towards a dashed monthly budget limit, with an arrow and a question mark at its latest point.
Illustrative curve, not real data.
8. What is the Batch API?Define
Three lines of a JSONL file, each a request with a custom_id and method, followed by a call to client.batches.create. An arrow leads to a results label with a question mark.
JSONL lines typed from the lesson's Use It example (bodies elided).
9. What is prompt compression?Define
A text box holding a wordy system prompt that asks the model to be polite to customers, with its word count and a question mark for the input tokens billed.
Illustrative prompt written for this question; words counted, not tokens.
10. What does cache hit rate measure?Define
Ten request tiles numbered 1 to 10; four are labelled cache and six are labelled LLM. Below is hit_rate equals a question mark.
Illustrative requests, not from the lesson.
11. Why does exact caching apply only to deterministic calls at temperature 0?Explain
Two request cards with the same model and message but temperature 0.0 on the left and 0.7 on the right. Each has a question mark asking whether a stored answer can be reused.
Model and message are the lesson's demo values.
12. Why does a semantic cache use embeddings and a similarity threshold instead of hashing the query?Explain
Four queries arriving in order: return policy, store hours, how to return an item, and when the store opens. The last two rows have a question mark asking whether an earlier answer is reused.
Queries are the lesson's demo examples.
13. Why does provider prompt caching not replace a semantic cache?Explain
Two request stacks. Each has a system prompt block with identical text and a user question block, one asking about the return policy and one asking how to send an item back. A question mark by each block asks what input price it pays.
Questions are the lesson's examples of one intent in different words.
14. Which trade-off separates Anthropic's prompt caching from OpenAI's?Explain
Two panels, Anthropic and OpenAI, each showing its API call and model name and four rows to compare: setup in the request, discount on a cache hit, extra charge on first write, and cache lifetime, each with a question mark.
Call and model names are from the lesson's Use It examples.
15. Why does the lesson call a long, unchanging system prompt the silent killer of cost?Explain
Five request rows, each starting with the same 1,500-token system prompt block followed by a dashed user message block, then the line 100K requests per day. A question mark stands for the cost of the prompt.
Schematic. The 1,500 tokens and 100K requests per day are from the lesson.
16. Why does a token bucket refill steadily instead of resetting once per day?Explain
Two small charts of tokens available across a day. Both start full and drop to empty at a dashed line at 10:00. After that the first policy climbs gradually, while the second stays empty until 24:00 and then jumps to full.
Schematic of the two policies, not measured.
17. Why does the tier table give lower tiers smaller token limits and cheaper-model access only?Explain
A table of three tiers, Free, Pro and Enterprise, with daily token limits of 50,000, 500,000 and 5,000,000, requests per minute of 10, 60 and 300, and model access.
Copied from the lesson's Rate Limiting table.
18. Why does the lesson log cache hit or miss and request category with each API call?Explain
A six-row call log with model, input and output tokens, cache hit or miss, request category and cost, followed by a total cost line.
Illustrative calls, priced with the lesson's calculate_cost.
19. Why does the circuit breaker throttle to cheaper models at 85% before it stops requests at 95%?Explain
A budget bar with ticks at 85 percent and 95 percent, and two lines: at 85 percent throttle to cheaper models, at 95 percent stop requests.
Levels as stated in the question.
20. Why is rate limiting listed in the optimization stack as 'Budget protection' instead of a percentage saving?Explain
The lesson's seven-layer table with technique and typical savings; the rate-limiting row is highlighted and reads Budget protection where the others give percentages.
Copied from the lesson's Optimization Stack table.
21. Your support bot gets 'What is the return policy?' and 'How do I send an item back?' all day, with different wording each time. Provider prefix caching is already on. Which layer catches the repeated intent?Apply
Four reworded versions of a return-policy question, each row with a question mark under the heading answer served by. A note says provider prefix caching is on.
The first two questions are from the stem; the other two wordings are typed for illustration.
22. A chatbot sends every query to GPT-4o. About 65% are FAQ-style questions like store hours. What is the best cost move for those?Apply
A grid of 100 squares, 65 filled for FAQ-style questions such as store hours and 35 outlined for other queries, all with an arrow into one box for gpt-4o.
Schematic. 65% is from the question; prices are from the lesson.
23. Monthly spend crosses 85% of budget. According to Budget Alerts and Circuit Breakers, what should the system do?Apply
A budget bar filled to 85 percent labelled 85% spent, and the line The system's action now followed by a question mark.
Setup from the question.
24. Which workload is a good fit for OpenAI's Batch API, per Batching in The Concept?Apply
Four lines of Python calling client.batches.create with an input file id and the chat completions endpoint, where the completion_window argument is a question mark.
Typed from the lesson's Use It example with the window value hidden.
25. Your system prompt and tool definitions total 5,000 tokens and are identical across requests, but each user question is different. Which technique pays off most directly?Apply
Three request rows, each with an identical 5,000-token system prompt and tools block and a different dashed user question block. A line asks for the technique that pays off most.
Schematic. The 5,000 tokens come from the question.
26. You add cache_control to a 600-token system prompt for Claude Sonnet, yet cache_read_input_tokens is always 0. What is the most likely cause?Analyze
A system block of 600 tokens carrying a cache_control ephemeral marker, and three calls whose usage shows cache_read_input_tokens 0.
Typed from the question; the three calls are an example of 'always 0'.
27. A support bot has one fixed 3,000-token system prompt, thousands of reworded FAQ questions, and a nightly job that tags 50,000 tickets. Which plan fits best?Design
Three rows describing a support bot's traffic: a fixed 3,000-token system prompt, thousands of differently worded FAQ questions, and a nightly job tagging 50,000 tickets.
Setup from the question.
28. You are ordering lookups in a tiered cache for a chat app. Which order fits the lesson's design?Design
A request box on the left, a response box on the right, and four unordered boxes between them: exact cache, semantic cache, model routing and LLM call. A question mark asks for the order.
Components from the lesson's pipeline.
29. Free-tier users should be able to send a quick burst of questions but not exceed a steady average. Which limiter design fits?Design
A time axis with a tight cluster of request ticks labelled quick burst, allowed, followed by evenly spaced ticks labelled held to a steady average. A question mark asks for the limiter design.
Illustrative; request times are made up.
30. A bug in a retry loop could burn a month of budget in hours. Which combination best limits the damage?Design
A chart of spend against hours since a bug started: the line climbs steeply and reaches the dashed monthly budget line within the first part of the axis.
Illustrative curve, not real data.

Build It

Step 1: Cost Calculator

Build a token cost calculator that knows current pricing for major models.

python
import hashlib
import time
import json
import math
from dataclasses import dataclass, field
 
 
MODEL_PRICING = {
    "gpt-4o": {"input": 2.50, "output": 10.00, "cached_input": 1.25},
    "gpt-4o-mini": {"input": 0.15, "output": 0.60, "cached_input": 0.075},
    "gpt-4.1": {"input": 2.00, "output": 8.00, "cached_input": 0.50},
    "gpt-4.1-mini": {"input": 0.40, "output": 1.60, "cached_input": 0.10},
    "gpt-4.1-nano": {"input": 0.10, "output": 0.40, "cached_input": 0.025},
    "o3": {"input": 2.00, "output": 8.00, "cached_input": 0.50},
    "o3-mini": {"input": 1.10, "output": 4.40, "cached_input": 0.55},
    "o4-mini": {"input": 1.10, "output": 4.40, "cached_input": 0.275},
    "claude-opus-4": {"input": 15.00, "output": 75.00, "cached_input": 1.50},
    "claude-sonnet-4": {"input": 3.00, "output": 15.00, "cached_input": 0.30},
    "claude-haiku-3.5": {"input": 0.80, "output": 4.00, "cached_input": 0.08},
    "gemini-2.5-pro": {"input": 1.25, "output": 10.00, "cached_input": 0.3125},
    "gemini-2.5-flash": {"input": 0.15, "output": 0.60, "cached_input": 0.0375},
}
 
 
def calculate_cost(model, input_tokens, output_tokens, cached_input_tokens=0):
    if model not in MODEL_PRICING:
        return {"error": f"Unknown model: {model}"}
    pricing = MODEL_PRICING[model]
    non_cached = input_tokens - cached_input_tokens
    input_cost = (non_cached / 1_000_000) * pricing["input"]
    cached_cost = (cached_input_tokens / 1_000_000) * pricing["cached_input"]
    output_cost = (output_tokens / 1_000_000) * pricing["output"]
    total = input_cost + cached_cost + output_cost
    return {
        "model": model,
        "input_tokens": input_tokens,
        "output_tokens": output_tokens,
        "cached_input_tokens": cached_input_tokens,
        "input_cost": round(input_cost, 6),
        "cached_input_cost": round(cached_cost, 6),
        "output_cost": round(output_cost, 6),
        "total_cost": round(total, 6),
    }

Step 2: Exact Cache

Hash the full prompt and return cached responses for identical requests.

python
class ExactCache:
    def __init__(self, max_size=1000, ttl_seconds=3600):
        self.cache = {}
        self.max_size = max_size
        self.ttl = ttl_seconds
        self.hits = 0
        self.misses = 0
 
    def _hash(self, model, messages, temperature):
        key_data = json.dumps({"model": model, "messages": messages, "temperature": temperature}, sort_keys=True)
        return hashlib.sha256(key_data.encode()).hexdigest()
 
    def get(self, model, messages, temperature=0.0):
        if temperature > 0:
            self.misses += 1
            return None
        key = self._hash(model, messages, temperature)
        if key in self.cache:
            entry = self.cache[key]
            if time.time() - entry["timestamp"] < self.ttl:
                self.hits += 1
                entry["access_count"] += 1
                return entry["response"]
            del self.cache[key]
        self.misses += 1
        return None
 
    def put(self, model, messages, temperature, response):
        if temperature > 0:
            return
        if len(self.cache) >= self.max_size:
            oldest_key = min(self.cache, key=lambda k: self.cache[k]["timestamp"])
            del self.cache[oldest_key]
        key = self._hash(model, messages, temperature)
        self.cache[key] = {
            "response": response,
            "timestamp": time.time(),
            "access_count": 1,
        }
 
    def stats(self):
        total = self.hits + self.misses
        return {
            "hits": self.hits,
            "misses": self.misses,
            "hit_rate": round(self.hits / total, 4) if total > 0 else 0,
            "cache_size": len(self.cache),
        }

Step 3: Semantic Cache

Embed queries and return cached responses when similarity exceeds a threshold.

python
def simple_embed(text):
    words = text.lower().split()
    vocab = {}
    for w in words:
        vocab[w] = vocab.get(w, 0) + 1
    norm = math.sqrt(sum(v * v for v in vocab.values()))
    if norm == 0:
        return {}
    return {k: v / norm for k, v in vocab.items()}
 
 
def cosine_similarity(a, b):
    if not a or not b:
        return 0.0
    all_keys = set(a) | set(b)
    dot = sum(a.get(k, 0) * b.get(k, 0) for k in all_keys)
    return dot
 
 
class SemanticCache:
    def __init__(self, similarity_threshold=0.85, max_size=500, ttl_seconds=3600):
        self.entries = []
        self.threshold = similarity_threshold
        self.max_size = max_size
        self.ttl = ttl_seconds
        self.hits = 0
        self.misses = 0
 
    def get(self, query):
        query_embedding = simple_embed(query)
        now = time.time()
        best_match = None
        best_sim = 0.0
        for entry in self.entries:
            if now - entry["timestamp"] > self.ttl:
                continue
            sim = cosine_similarity(query_embedding, entry["embedding"])
            if sim > best_sim:
                best_sim = sim
                best_match = entry
        if best_match and best_sim >= self.threshold:
            self.hits += 1
            best_match["access_count"] += 1
            return {"response": best_match["response"], "similarity": round(best_sim, 4), "original_query": best_match["query"]}
        self.misses += 1
        return None
 
    def put(self, query, response):
        if len(self.entries) >= self.max_size:
            self.entries.sort(key=lambda e: e["timestamp"])
            self.entries.pop(0)
        self.entries.append({
            "query": query,
            "embedding": simple_embed(query),
            "response": response,
            "timestamp": time.time(),
            "access_count": 1,
        })
 
    def stats(self):
        total = self.hits + self.misses
        return {
            "hits": self.hits,
            "misses": self.misses,
            "hit_rate": round(self.hits / total, 4) if total > 0 else 0,
            "cache_size": len(self.entries),
        }

Step 4: Rate Limiter

Token bucket rate limiter with per-user quotas.

python
class TokenBucketRateLimiter:
    def __init__(self):
        self.buckets = {}
        self.tiers = {
            "free": {"capacity": 50_000, "refill_rate": 500, "max_requests_per_min": 10},
            "pro": {"capacity": 500_000, "refill_rate": 5_000, "max_requests_per_min": 60},
            "enterprise": {"capacity": 5_000_000, "refill_rate": 50_000, "max_requests_per_min": 300},
        }
 
    def _get_bucket(self, user_id, tier="free"):
        if user_id not in self.buckets:
            tier_config = self.tiers.get(tier, self.tiers["free"])
            self.buckets[user_id] = {
                "tokens": tier_config["capacity"],
                "capacity": tier_config["capacity"],
                "refill_rate": tier_config["refill_rate"],
                "last_refill": time.time(),
                "request_timestamps": [],
                "max_rpm": tier_config["max_requests_per_min"],
                "tier": tier,
                "total_tokens_used": 0,
            }
        return self.buckets[user_id]
 
    def _refill(self, bucket):
        now = time.time()
        elapsed = now - bucket["last_refill"]
        refill = int(elapsed * bucket["refill_rate"])
        if refill > 0:
            bucket["tokens"] = min(bucket["capacity"], bucket["tokens"] + refill)
            bucket["last_refill"] = now
 
    def check(self, user_id, tokens_needed, tier="free"):
        bucket = self._get_bucket(user_id, tier)
        self._refill(bucket)
        now = time.time()
        bucket["request_timestamps"] = [t for t in bucket["request_timestamps"] if now - t < 60]
        if len(bucket["request_timestamps"]) >= bucket["max_rpm"]:
            return {"allowed": False, "reason": "rate_limit", "retry_after_seconds": 60 - (now - bucket["request_timestamps"][0])}
        if bucket["tokens"] < tokens_needed:
            deficit = tokens_needed - bucket["tokens"]
            wait = deficit / bucket["refill_rate"]
            return {"allowed": False, "reason": "token_limit", "tokens_available": bucket["tokens"], "retry_after_seconds": round(wait, 1)}
        return {"allowed": True, "tokens_available": bucket["tokens"]}
 
    def consume(self, user_id, tokens_used, tier="free"):
        bucket = self._get_bucket(user_id, tier)
        bucket["tokens"] -= tokens_used
        bucket["request_timestamps"].append(time.time())
        bucket["total_tokens_used"] += tokens_used
 
    def get_usage(self, user_id):
        if user_id not in self.buckets:
            return {"error": "User not found"}
        b = self.buckets[user_id]
        return {
            "user_id": user_id,
            "tier": b["tier"],
            "tokens_remaining": b["tokens"],
            "capacity": b["capacity"],
            "total_tokens_used": b["total_tokens_used"],
            "utilization": round(b["total_tokens_used"] / b["capacity"], 4) if b["capacity"] else 0,
        }

Step 5: Cost Tracker

Log every call and compute running totals.

python
class CostTracker:
    def __init__(self, monthly_budget=1000.0):
        self.logs = []
        self.monthly_budget = monthly_budget
        self.alerts = []
 
    def log_call(self, model, input_tokens, output_tokens, cached_input_tokens=0, latency_ms=0, user_id="anonymous", cache_status="miss"):
        cost = calculate_cost(model, input_tokens, output_tokens, cached_input_tokens)
        entry = {
            "timestamp": time.time(),
            "model": model,
            "input_tokens": input_tokens,
            "output_tokens": output_tokens,
            "cached_input_tokens": cached_input_tokens,
            "latency_ms": latency_ms,
            "cost": cost["total_cost"],
            "user_id": user_id,
            "cache_status": cache_status,
        }
        self.logs.append(entry)
        self._check_budget()
        return entry
 
    def _check_budget(self):
        total = self.total_cost()
        pct = total / self.monthly_budget if self.monthly_budget > 0 else 0
        if pct >= 0.95 and not any(a["level"] == "stop" for a in self.alerts):
            self.alerts.append({"level": "stop", "message": f"Budget 95% consumed: ${total:.2f}/${self.monthly_budget:.2f}", "timestamp": time.time()})
        elif pct >= 0.85 and not any(a["level"] == "throttle" for a in self.alerts):
            self.alerts.append({"level": "throttle", "message": f"Budget 85% consumed: ${total:.2f}/${self.monthly_budget:.2f}", "timestamp": time.time()})
        elif pct >= 0.70 and not any(a["level"] == "warning" for a in self.alerts):
            self.alerts.append({"level": "warning", "message": f"Budget 70% consumed: ${total:.2f}/${self.monthly_budget:.2f}", "timestamp": time.time()})
 
    def total_cost(self):
        return round(sum(e["cost"] for e in self.logs), 6)
 
    def cost_by_model(self):
        by_model = {}
        for e in self.logs:
            m = e["model"]
            if m not in by_model:
                by_model[m] = {"calls": 0, "cost": 0, "input_tokens": 0, "output_tokens": 0}
            by_model[m]["calls"] += 1
            by_model[m]["cost"] = round(by_model[m]["cost"] + e["cost"], 6)
            by_model[m]["input_tokens"] += e["input_tokens"]
            by_model[m]["output_tokens"] += e["output_tokens"]
        return by_model
 
    def cache_savings(self):
        cache_hits = [e for e in self.logs if e["cache_status"] == "hit"]
        if not cache_hits:
            return {"saved": 0, "cache_hits": 0}
        saved = 0
        for e in cache_hits:
            full_cost = calculate_cost(e["model"], e["input_tokens"], e["output_tokens"])
            saved += full_cost["total_cost"]
        return {"saved": round(saved, 4), "cache_hits": len(cache_hits)}
 
    def summary(self):
        if not self.logs:
            return {"total_calls": 0, "total_cost": 0}
        total_latency = sum(e["latency_ms"] for e in self.logs)
        cache_hits = sum(1 for e in self.logs if e["cache_status"] == "hit")
        return {
            "total_calls": len(self.logs),
            "total_cost": self.total_cost(),
            "avg_cost_per_call": round(self.total_cost() / len(self.logs), 6),
            "avg_latency_ms": round(total_latency / len(self.logs), 1),
            "cache_hit_rate": round(cache_hits / len(self.logs), 4),
            "cost_by_model": self.cost_by_model(),
            "cache_savings": self.cache_savings(),
            "budget_remaining": round(self.monthly_budget - self.total_cost(), 2),
            "budget_utilization": round(self.total_cost() / self.monthly_budget, 4) if self.monthly_budget > 0 else 0,
            "alerts": self.alerts,
        }

Step 6: Model Router

Route queries to the cheapest model that can handle them.

python
SIMPLE_KEYWORDS = ["what time", "hours", "address", "phone", "price", "return policy", "hello", "hi", "thanks", "yes", "no"]
COMPLEX_KEYWORDS = ["analyze", "compare", "explain why", "write code", "debug", "architect", "design", "trade-off", "evaluate"]
 
 
def classify_complexity(query):
    q = query.lower()
    if len(q.split()) <= 5 or any(kw in q for kw in SIMPLE_KEYWORDS):
        return "simple"
    if any(kw in q for kw in COMPLEX_KEYWORDS):
        return "complex"
    return "medium"
 
 
def route_model(query, tier="pro"):
    complexity = classify_complexity(query)
    routing_table = {
        "simple": {"free": "gpt-4.1-nano", "pro": "gpt-4o-mini", "enterprise": "gpt-4o-mini"},
        "medium": {"free": "gpt-4o-mini", "pro": "claude-sonnet-4", "enterprise": "claude-sonnet-4"},
        "complex": {"free": "gpt-4o-mini", "pro": "gpt-4o", "enterprise": "claude-opus-4"},
    }
    model = routing_table[complexity].get(tier, "gpt-4o-mini")
    return {"query": query, "complexity": complexity, "model": model, "tier": tier}

Step 7: Run the Demo

python
def simulate_llm_call(model, query):
    input_tokens = len(query.split()) * 4 + 500
    output_tokens = 150 + (len(query.split()) * 2)
    latency = 200 + (output_tokens * 2)
    return {
        "model": model,
        "response": f"[Simulated {model} response to: {query[:50]}...]",
        "input_tokens": input_tokens,
        "output_tokens": output_tokens,
        "latency_ms": latency,
    }
 
 
def run_demo():
    print("=" * 60)
    print("  Caching, Rate Limiting & Cost Optimization Demo")
    print("=" * 60)
 
    print("\n--- Model Pricing ---")
    for model, pricing in list(MODEL_PRICING.items())[:6]:
        cost_1k = calculate_cost(model, 1000, 500)
        print(f"  {model}: ${cost_1k['total_cost']:.6f} per 1K in + 500 out")
 
    print("\n--- Cost Comparison: 100K Requests ---")
    for model in ["gpt-4o", "gpt-4o-mini", "claude-sonnet-4", "claude-haiku-3.5"]:
        cost = calculate_cost(model, 1000 * 100_000, 500 * 100_000)
        print(f"  {model}: ${cost['total_cost']:.2f}")
 
    print("\n--- Anthropic Cache Savings ---")
    no_cache = calculate_cost("claude-sonnet-4", 2000, 500, 0)
    with_cache = calculate_cost("claude-sonnet-4", 2000, 500, 1500)
    saving = no_cache["total_cost"] - with_cache["total_cost"]
    print(f"  Without cache: ${no_cache['total_cost']:.6f}")
    print(f"  With 1500 cached tokens: ${with_cache['total_cost']:.6f}")
    print(f"  Savings per call: ${saving:.6f} ({saving/no_cache['total_cost']*100:.1f}%)")
 
    exact_cache = ExactCache(max_size=100, ttl_seconds=300)
    semantic_cache = SemanticCache(similarity_threshold=0.75, max_size=100)
    rate_limiter = TokenBucketRateLimiter()
    tracker = CostTracker(monthly_budget=100.0)
 
    print("\n--- Exact Cache ---")
    messages_1 = [{"role": "user", "content": "What is the return policy?"}]
    result = exact_cache.get("gpt-4o-mini", messages_1, 0.0)
    print(f"  First lookup: {'HIT' if result else 'MISS'}")
    exact_cache.put("gpt-4o-mini", messages_1, 0.0, "You can return items within 30 days.")
    result = exact_cache.get("gpt-4o-mini", messages_1, 0.0)
    print(f"  Second lookup: {'HIT' if result else 'MISS'} -> {result}")
    result = exact_cache.get("gpt-4o-mini", messages_1, 0.7)
    print(f"  With temp=0.7: {'HIT' if result else 'MISS (non-deterministic, skip cache)'}")
    print(f"  Stats: {exact_cache.stats()}")
 
    print("\n--- Semantic Cache ---")
    test_queries = [
        ("What is the return policy?", "Items can be returned within 30 days with receipt."),
        ("How do I return an item?", None),
        ("What are your store hours?", "We are open 9am-9pm Monday through Saturday."),
        ("When does the store open?", None),
        ("Tell me about quantum computing", "Quantum computers use qubits..."),
        ("Explain quantum mechanics", None),
    ]
    for query, response in test_queries:
        cached = semantic_cache.get(query)
        if cached:
            print(f"  '{query[:40]}' -> CACHE HIT (sim={cached['similarity']}, original='{cached['original_query'][:40]}')")
        elif response:
            semantic_cache.put(query, response)
            print(f"  '{query[:40]}' -> MISS (stored)")
        else:
            print(f"  '{query[:40]}' -> MISS (no match)")
    print(f"  Stats: {semantic_cache.stats()}")
 
    print("\n--- Rate Limiting ---")
    for i in range(12):
        check = rate_limiter.check("user_1", 1000, "free")
        if check["allowed"]:
            rate_limiter.consume("user_1", 1000, "free")
        status = "OK" if check["allowed"] else f"BLOCKED ({check['reason']})"
        if i < 5 or not check["allowed"]:
            print(f"  Request {i+1}: {status}")
    print(f"  Usage: {rate_limiter.get_usage('user_1')}")
 
    print("\n--- Model Routing ---")
    routing_queries = [
        "What time do you close?",
        "Summarize this quarterly earnings report",
        "Analyze the trade-offs between microservices and monoliths",
        "Hello",
        "Write code for a binary search tree with deletion",
    ]
    for q in routing_queries:
        route = route_model(q, "pro")
        print(f"  '{q[:50]}' -> {route['model']} ({route['complexity']})")
 
    print("\n--- Full Pipeline: Before vs After Optimization ---")
    queries = [
        "What is the return policy?",
        "How do I return something?",
        "What are your hours?",
        "When do you open?",
        "Explain the difference between TCP and UDP",
        "Compare TCP vs UDP protocols",
        "Hello",
        "What is your phone number?",
        "Write a Python function to sort a list",
        "Analyze the pros and cons of serverless architecture",
    ]
 
    print("\n  [Before: no caching, single model (gpt-4o)]")
    tracker_before = CostTracker(monthly_budget=1000.0)
    for q in queries:
        result = simulate_llm_call("gpt-4o", q)
        tracker_before.log_call("gpt-4o", result["input_tokens"], result["output_tokens"], latency_ms=result["latency_ms"], cache_status="miss")
    before = tracker_before.summary()
    print(f"  Total cost: ${before['total_cost']:.6f}")
    print(f"  Avg cost/call: ${before['avg_cost_per_call']:.6f}")
    print(f"  Avg latency: {before['avg_latency_ms']}ms")
 
    print("\n  [After: caching + routing + rate limiting]")
    exact_c = ExactCache()
    semantic_c = SemanticCache(similarity_threshold=0.75)
    tracker_after = CostTracker(monthly_budget=1000.0)
 
    for q in queries:
        messages = [{"role": "user", "content": q}]
        cached = exact_c.get("gpt-4o", messages, 0.0)
        if cached:
            tracker_after.log_call("gpt-4o-mini", 0, 0, latency_ms=5, cache_status="hit")
            continue
        sem_cached = semantic_c.get(q)
        if sem_cached:
            tracker_after.log_call("gpt-4o-mini", 0, 0, latency_ms=15, cache_status="hit")
            continue
        route = route_model(q)
        result = simulate_llm_call(route["model"], q)
        tracker_after.log_call(route["model"], result["input_tokens"], result["output_tokens"], latency_ms=result["latency_ms"], cache_status="miss")
        exact_c.put(route["model"], messages, 0.0, result["response"])
        semantic_c.put(q, result["response"])
 
    after = tracker_after.summary()
    print(f"  Total cost: ${after['total_cost']:.6f}")
    print(f"  Avg cost/call: ${after['avg_cost_per_call']:.6f}")
    print(f"  Avg latency: {after['avg_latency_ms']}ms")
    print(f"  Cache hit rate: {after['cache_hit_rate']:.0%}")
 
    if before["total_cost"] > 0:
        savings_pct = (1 - after["total_cost"] / before["total_cost"]) * 100
        print(f"\n  SAVINGS: {savings_pct:.1f}% cost reduction")
        print(f"  Latency improvement: {(1 - after['avg_latency_ms'] / before['avg_latency_ms']) * 100:.1f}% faster")
 
    print("\n--- Budget Alerts Demo ---")
    alert_tracker = CostTracker(monthly_budget=0.01)
    for i in range(5):
        alert_tracker.log_call("gpt-4o", 5000, 2000, latency_ms=500)
    print(f"  Total spent: ${alert_tracker.total_cost():.6f} / ${alert_tracker.monthly_budget}")
    for alert in alert_tracker.alerts:
        print(f"  ALERT [{alert['level'].upper()}]: {alert['message']}")
 
    print("\n--- Cost Breakdown by Model ---")
    multi_tracker = CostTracker(monthly_budget=500.0)
    for _ in range(50):
        multi_tracker.log_call("gpt-4o-mini", 800, 200, latency_ms=150)
    for _ in range(30):
        multi_tracker.log_call("claude-sonnet-4", 1500, 500, latency_ms=400)
    for _ in range(10):
        multi_tracker.log_call("gpt-4o", 2000, 800, latency_ms=600)
    for _ in range(10):
        multi_tracker.log_call("claude-opus-4", 3000, 1000, latency_ms=1200)
    breakdown = multi_tracker.cost_by_model()
    for model, data in sorted(breakdown.items(), key=lambda x: x[1]["cost"], reverse=True):
        print(f"  {model}: {data['calls']} calls, ${data['cost']:.6f}, {data['input_tokens']:,} in / {data['output_tokens']:,} out")
    print(f"  Total: ${multi_tracker.total_cost():.6f}")
 
    print("\n" + "=" * 60)
    print("  Demo complete.")
    print("=" * 60)
 
 
if __name__ == "__main__":
    run_demo()
Check: Build It0/11
1. A user's token bucket holds 50,000 tokens and refills at a steady rate. They send three 15,000-token requests in a quick burst, then a fourth right away. What happens?Apply
A bucket gauge showing 50,000 tokens and four request chips of 15,000 tokens each, sent in a burst, with a question mark under each for its outcome.
Setup from the question.
2. With the Step 1 pricing table, what does calculate_cost('claude-sonnet-4', 2000, 500, 1500) return as total_cost?Apply
The claude-sonnet-4 pricing entry with input 3.00, output 15.00 and cached_input 0.30, the signature of calculate_cost, and the call with 2000, 500 and 1500 whose total_cost is a question mark.
Pricing and signature copied from the lesson's code.
3. A free-tier user has made 10 requests of 1,000 tokens each within the last minute, all allowed, and has about 40,000 tokens left. What does the rate limiter's check() return for an 11th request of 1,000 tokens?Apply
Ten tiles for earlier requests of 1,000 tokens each, all allowed, then a dashed eleventh request of 1,000 tokens whose check return value is a question mark.
Setup from the question.
4. On an empty ExactCache, code calls get (temperature 0.0), then put, then get (0.0), then get (0.7) with the same model and messages. What does stats() report?Apply
Six lines of code: create an ExactCache, get at temperature 0.0, put, get at 0.0, get at 0.7, then stats() with a question mark.
Typed from the question.
5. With the Build It SemanticCache at threshold 0.75, 'What is the return policy?' is stored. What does get('How do I return an item?') do?Apply
Code: a SemanticCache with similarity_threshold 0.75, a put of What is the return policy?, and a get of How do I return an item? whose result is a question mark.
Typed from the question.
6. What does route_model('Compare TCP vs UDP protocols', 'pro') return as the model?Apply
A table mapping simple to gpt-4o-mini, medium to claude-sonnet-4 and complex to gpt-4o for the pro tier, and the call route_model for Compare TCP vs UDP protocols whose model is a question mark.
Table copied from the lesson's route_model.
7. The Build It router sends 'Analyze the trade-offs between microservices and monoliths' to the simple tier. What causes this, as explained in Author's Note?Analyze
Terminal output of route_model for the query Analyze the trade-offs between microservices and monoliths on the pro tier, giving complexity simple and model gpt-4o-mini.
Captured from the lesson's route_model.
8. The demo's semantic cache reports a 0% hit rate even though the test queries are paraphrases of each other. Which part explains this?Analyze
Terminal output of the demo's semantic cache section: six queries are all misses, and the stats line shows hits 0 and hit_rate 0.0.
Captured by running the lesson's caching_cost.py.
9. In the budget demo with a $0.01 budget, the first call crosses 70%, 85% and 95%, yet the alerts list reads stop, throttle, warning. What explains that order?Analyze
The budget demo code: a tracker with budget 0.01 and five gpt-4o calls of 5000 in and 2000 out, then the printed total and three alerts in the order STOP, THROTTLE, WARNING.
Output captured from the lesson's demo. The percentages in the messages are the threshold names, not the real consumption.
10. ExactCache hit rate stays near zero in production even though users repeat the same questions. The calls use temperature 0.7. Why?Analyze
Three log rows with the same model and messages at temperature 0.7, a note that users repeat questions, and a box reading hit rate close to 0 with a question mark.
Illustrative log; times are made up.
11. The Build It caches and rate limiter run behind four worker processes. Cache hits fall and a user exceeds their quota. What explains both symptoms?Analyze
One stream of requests from a user fanning out to four worker boxes, each labelled with the Build It caches and rate limiter. A line notes cache hits fall and a user exceeds their quota.
Setup from the question.

Use It

Anthropic Prompt Caching

python
# import anthropic
#
# client = anthropic.Anthropic()
#
# response = client.messages.create(
#     model="claude-sonnet-4-20250514",
#     max_tokens=1024,
#     system=[
#         {
#             "type": "text",
#             "text": "You are a helpful customer support agent for Acme Corp...",
#             "cache_control": {"type": "ephemeral"},
#         }
#     ],
#     messages=[{"role": "user", "content": "What is the return policy?"}],
# )
#
# print(f"Input tokens: {response.usage.input_tokens}")
# print(f"Cache creation tokens: {response.usage.cache_creation_input_tokens}")
# print(f"Cache read tokens: {response.usage.cache_read_input_tokens}")

The first call writes to the cache (25% premium). Later calls with the same system prompt prefix read from the cache (90% discount). The cache lasts 5 minutes and resets the timer on every hit.

OpenAI Automatic Caching

python
# from openai import OpenAI
#
# client = OpenAI()
#
# response = client.chat.completions.create(
#     model="gpt-4o",
#     messages=[
#         {"role": "system", "content": "You are a helpful customer support agent..."},
#         {"role": "user", "content": "What is the return policy?"},
#     ],
# )
#
# print(f"Prompt tokens: {response.usage.prompt_tokens}")
# print(f"Cached tokens: {response.usage.prompt_tokens_details.cached_tokens}")
# print(f"Completion tokens: {response.usage.completion_tokens}")

OpenAI caches automatically. Any prompt prefix of 1,024+ tokens that matches a recent request gets a 50% discount. No code changes are needed. Check prompt_tokens_details.cached_tokens in the response to confirm it is working.

OpenAI Batch API

python
# import json
# from openai import OpenAI
#
# client = OpenAI()
#
# requests = []
# for i, query in enumerate(queries):
#     requests.append({
#         "custom_id": f"request-{i}",
#         "method": "POST",
#         "url": "/v1/chat/completions",
#         "body": {
#             "model": "gpt-4o-mini",
#             "messages": [{"role": "user", "content": query}],
#         },
#     })
#
# with open("batch_input.jsonl", "w") as f:
#     for r in requests:
#         f.write(json.dumps(r) + "\n")
#
# batch_file = client.files.create(file=open("batch_input.jsonl", "rb"), purpose="batch")
# batch = client.batches.create(input_file_id=batch_file.id, endpoint="/v1/chat/completions", completion_window="24h")
# print(f"Batch ID: {batch.id}, Status: {batch.status}")

Batch API gives a flat 50% discount on all tokens. Results arrive within 24 hours. A good fit for non-real-time workloads: evaluations, data labeling, bulk summarization.

Production Semantic Cache with Redis

python
# import redis
# import numpy as np
# from openai import OpenAI
#
# r = redis.Redis()
# client = OpenAI()
#
# def get_embedding(text):
#     response = client.embeddings.create(model="text-embedding-3-small", input=text)
#     return response.data[0].embedding
#
# def semantic_cache_lookup(query, threshold=0.95):
#     query_emb = np.array(get_embedding(query))
#     keys = r.keys("cache:emb:*")
#     best_sim, best_key = 0, None
#     for key in keys:
#         stored_emb = np.frombuffer(r.get(key), dtype=np.float32)
#         sim = np.dot(query_emb, stored_emb) / (np.linalg.norm(query_emb) * np.linalg.norm(stored_emb))
#         if sim > best_sim:
#             best_sim, best_key = sim, key
#     if best_sim >= threshold and best_key:
#         response_key = best_key.decode().replace("cache:emb:", "cache:resp:")
#         return r.get(response_key).decode()
#     return None

In production, replace the linear scan with a vector index (Redis Vector Search, Pinecone, or pgvector). Linear scan works for <1,000 entries. Beyond that, use ANN (approximate nearest neighbor) for O(log n) lookup.

Check: Use It0/1
1. The Redis semantic cache works at 300 entries, but lookups slow down badly past 100,000 entries. Which part is the cause?Analyze
Two boxes of cache entries, 300 entries labelled works and past 100,000 entries labelled slow, and below them a new query flowing into a lookup step with a question mark.
Numbers are from the question.

Ship It

This lesson produces outputs/prompt-cost-optimizer.md, a reusable prompt that analyzes your LLM application and recommends specific cost optimizations with projected savings.

It also produces outputs/skill-cost-patterns.md, a decision framework for choosing the right caching strategy, rate limiting configuration, and model routing rules for your use case.

Author's Note

I'd run caching_cost.py before changing anything. It simulates the LLM calls, so it needs no API key, and it prints each layer's behavior in turn.

Then read the output with some suspicion, because the toy is rougher than the prose around it. The semantic cache uses word-count vectors instead of learned embeddings, so none of the paraphrase pairs hit at the 0.75 threshold. "How do I return an item?" shares only the word "return" with "What is the return policy?". The pipeline comparison prints a 0% cache hit rate, which means its cost drop comes from routing alone. A real embedding model is what lets that layer earn its place.

The router is the first thing I'd fix. Keywords match as substrings, so "hi" fires inside "this" and "no" fires inside "monoliths", and "Analyze the trade-offs between microservices and monoliths" lands in the simple tier. Matching whole words would send it down the complex path. The budget demo has a smaller quirk: with a $0.01 budget the first call crosses all three thresholds, but the elif chain records one level per call, so the alerts print as stop, throttle, warning. In a real breaker I'd act on the highest level crossed right away and not wait for later calls to record the lower ones. Everything also lives in process memory with no locking, so running several workers means moving the caches and buckets into something shared like Redis.

Exercises

  1. Implement LRU eviction for the semantic cache. Replace the oldest-first eviction with least-recently-used. Track the last access time for each entry and evict the entry with the oldest access time when the cache is full. Compare hit rates between the two strategies over 100 queries.

  2. Build a cost projection tool. Given a log of API calls (the CostTracker logs), project the monthly cost based on the trailing 7-day average. Account for weekday/weekend patterns. Trigger an alert if the projected monthly cost exceeds the budget by more than 20%.

  3. Implement tiered semantic caching. Use two similarity thresholds: 0.98 for high-confidence hits (return immediately) and 0.90 for medium-confidence hits (return with a disclaimer: "Based on a similar previous question..."). Track which tier each hit came from and measure user satisfaction differences.

  4. Build a model routing classifier. Replace the keyword-based classifier with an embedding-based one. Embed 50 labeled queries (simple/medium/complex), then classify new queries by finding the nearest labeled example. Measure classification accuracy against a test set of 20 queries.

  5. Implement a circuit breaker with degradation levels. At 70% budget, log a warning. At 85%, automatically switch all routing to the cheapest model (gpt-4o-mini). At 95%, serve only cached responses and reject new queries. Test by simulating 1,000 requests against a $1.00 budget and verify each threshold triggers correctly.

Key Terms

TermWhat people sayWhat it actually means
Prompt caching"Cache the system prompt"Provider-level caching where repeated prompt prefixes get a discount (90% Anthropic, 50% OpenAI) -- no code changes for OpenAI, explicit markers for Anthropic
Semantic caching"Smart caching"Embedding the query, computing similarity to past queries, and returning the cached response if similarity exceeds a threshold -- catches paraphrases that exact matching misses
Exact caching"Hash caching"Hashing the full prompt (model + messages + temperature) and returning the cached response for identical inputs -- only works for temperature=0 deterministic calls
Token bucket"Rate limiter"An algorithm where each user has a bucket of N tokens that refills at rate R per second -- allows bursts up to N while enforcing an average rate of R
Model routing"Cheapskate routing"Using a classifier to send simple queries to cheap models (GPT-4o-mini, Haiku) and complex queries to expensive models (GPT-4o, Opus) -- saves 40-70% on model costs
Cost tracking"Metering"Logging every API call with model, tokens, latency, cost, and user ID so you know exactly where money goes and which features are expensive
Circuit breaker"Kill switch"Automatically degrading service (cheaper models, cached-only) or stopping requests entirely when spending approaches the budget limit
Batch API"Bulk discount"OpenAI's asynchronous processing at 50% discount -- submit up to 50,000 requests, get results within 24 hours
Prompt compression"Token diet"Rewriting system prompts and context to use fewer tokens while preserving meaning -- shorter prompts cost less and often perform better
Cache hit rate"Cache efficiency"The percentage of requests served from cache instead of calling the LLM -- 40-60% is typical for production chatbots, saves proportionally on cost

Further Reading