Your AI prototype cost $50/month. Your production system costs $50,000/month. What happened?
LLM costs scale faster than most teams expect. Without optimization, AI can become economically unsustainable. This post documents how we cut our LLM bill by 70% while maintaining quality.
| Component | Typical Share | Optimization Potential |
|---|
| LLM API calls | 60-80% | High |
| Embedding generation | 10-20% | High |
| Vector database | 5-10% | Medium |
| Compute/infrastructure | 5-15% | Medium |
| Driver | Impact | Controllable |
|---|
| Model choice | 10-100x difference | Yes |
| Prompt length | Linear with tokens | Yes |
| Output length | Linear with tokens | Partially |
| Request volume | Linear | Partially |
| Retry rate | Multiplier | Yes |
| Model | Input (1K tokens) | Output (1K tokens) | Typical Request |
|---|
| GPT-4 | $0.03 | $0.06 | $0.15 |
| GPT-3.5 | $0.0005 | $0.0015 | $0.003 |
| Claude Opus | $0.015 | $0.075 | $0.12 |
| Claude Haiku | $0.00025 | $0.00125 | $0.002 |
| Llama 70B (self-hosted) | ~$0.001 | ~$0.002 | $0.005 |
Not every query needs GPT-4. Route based on complexity:
| Query Type | Percentage | Model | Cost Impact |
|---|
| Simple | 50% | GPT-3.5/Haiku | 98% savings |
| Medium | 35% | Claude Sonnet | 70% savings |
| Complex | 15% | GPT-4/Opus | Baseline |
Diagram
flowchart LR
A[User Query] --> B{Complexity<br/>Classifier}
B -->|Simple| C[GPT-3.5<br/>$0.002]
B -->|Medium| D[Claude Sonnet<br/>$0.015]
B -->|Complex| E[GPT-4<br/>$0.12]
C --> F[Response]
D --> F
E --> F
style C fill:#22c55e,color:#fff
style D fill:#f59e0b,color:#fff
style E fill:#ef4444,color:#fff
| Approach | Accuracy | Latency | Cost |
|---|
| Keyword rules | 70% | 0ms | Free |
| Small classifier | 85% | 10ms | Minimal |
| LLM classifier | 95% | 200ms | Low |
| Hybrid | 92% | 15ms | Minimal |
Python
from enum import Enum
from typing import Tuple
class QueryComplexity(Enum):
SIMPLE = "simple"
MEDIUM = "medium"
COMPLEX = "complex"
class ModelRouter:
def __init__(self):
self.models = {
QueryComplexity.SIMPLE: "gpt-3.5-turbo",
QueryComplexity.MEDIUM: "claude-3-sonnet",
QueryComplexity.COMPLEX: "gpt-4-turbo",
}
# Keywords that indicate complexity
self.complex_indicators = [
"analyze", "compare", "synthesize", "evaluate",
"code review", "architecture", "design system"
]
self.simple_indicators = [
"what is", "define", "list", "when was",
"how many", "yes or no"
]
def classify(self, query: str) -> Tuple[QueryComplexity, str]:
query_lower = query.lower()
# Rule-based fast path
if any(ind in query_lower for ind in self.simple_indicators):
if len(query) < 100:
return QueryComplexity.SIMPLE, self.models[QueryComplexity.SIMPLE]
if any(ind in query_lower for ind in self.complex_indicators):
return QueryComplexity.COMPLEX, self.models[QueryComplexity.COMPLEX]
# Fallback: Use token count as heuristic
token_estimate = len(query.split())
if token_estimate < 20:
return QueryComplexity.SIMPLE, self.models[QueryComplexity.SIMPLE]
elif token_estimate < 100:
return QueryComplexity.MEDIUM, self.models[QueryComplexity.MEDIUM]
else:
return QueryComplexity.COMPLEX, self.models[QueryComplexity.COMPLEX]
async def route_and_call(self, query: str, messages: list) -> dict:
complexity, model = self.classify(query)
response = await self.llm_client.chat(
model=model,
messages=messages
)
# Track for analytics
self.metrics.record_routing(complexity, model, response.usage)
return {
"response": response.content,
"model_used": model,
"complexity": complexity.value,
"cost": self.calculate_cost(model, response.usage)
}
| Metric | Before Routing | After Routing | Improvement |
|---|
| Average cost/request | $0.12 | $0.035 | 71% reduction |
| Quality score | 92% | 91% | -1% (acceptable) |
| p95 latency | 5.2s | 3.1s | 40% faster |
| Content | Cache Strategy | Hit Rate | Savings |
|---|
| Identical queries | Exact match | 15-25% | 100% per hit |
| Similar queries | Semantic (0.95+) | 25-40% | 100% per hit |
| Embeddings | Permanent | 90%+ | 100% per hit |
| Partial results | Component cache | 30-50% | 50-80% per hit |
Diagram
flowchart TB
A[User Query] --> B{Exact Match<br/>Cache?}
B -->|Hit| C[Return Cached]
B -->|Miss| D{Semantic<br/>Cache?}
D -->|Similar Found| E{Similarity<br/>Above 0.95?}
D -->|No Match| F[Call LLM]
E -->|Yes| C
E -->|No| F
F --> G[Store Response]
G --> H[Store Embedding]
H --> I[Return Response]
style C fill:#22c55e,color:#fff
style F fill:#f59e0b,color:#fff
| Similarity Threshold | Hit Rate | Quality Risk |
|---|
| 0.99 | 10% | None |
| 0.97 | 25% | Minimal |
| 0.95 | 40% | Low |
| 0.90 | 60% | Medium |
Python
import hashlib
import numpy as np
from typing import Optional, Tuple
class SemanticCache:
def __init__(self, redis_client, vector_db, embedding_model):
self.redis = redis_client
self.vector_db = vector_db
self.embedder = embedding_model
self.similarity_threshold = 0.95
self.ttl_seconds = 86400 # 24 hours
async def get(self, query: str) -> Optional[Tuple[str, float]]:
# Layer 1: Exact match (fastest)
cache_key = self._hash_query(query)
exact_hit = await self.redis.get(cache_key)
if exact_hit:
return exact_hit, 1.0
# Layer 2: Semantic match (slower but more hits)
query_embedding = await self.embedder.embed(query)
results = await self.vector_db.search(
vector=query_embedding,
top_k=1,
include_metadata=True
)
if results and results[0].score >= self.similarity_threshold:
cached_response = results[0].metadata["response"]
# Warm exact cache for next time
await self.redis.setex(cache_key, self.ttl_seconds, cached_response)
return cached_response, results[0].score
return None
async def set(self, query: str, response: str):
# Store in exact cache
cache_key = self._hash_query(query)
await self.redis.setex(cache_key, self.ttl_seconds, response)
# Store in semantic cache
query_embedding = await self.embedder.embed(query)
await self.vector_db.upsert(
id=cache_key,
vector=query_embedding,
metadata={"query": query, "response": response}
)
def _hash_query(self, query: str) -> str:
normalized = query.lower().strip()
return hashlib.sha256(normalized.encode()).hexdigest()[:16]
# Usage in API endpoint
cache = SemanticCache(redis, pinecone, openai_embedder)
async def chat_with_cache(query: str) -> dict:
# Check cache first
cached = await cache.get(query)
if cached:
response, similarity = cached
return {
"response": response,
"cached": True,
"similarity": similarity,
"cost": 0
}
# Cache miss - call LLM
response = await llm.chat(query)
# Store for future
await cache.set(query, response.content)
return {
"response": response.content,
"cached": False,
"cost": response.usage.total_cost
}
| Layer | TTL | Storage | Cost |
|---|
| In-memory | 5 min | Redis | $50/month |
| Semantic | 24 hours | Vector DB | $100/month |
| Persistent | 7 days | PostgreSQL | $50/month |
| Metric | Before | After | Improvement |
|---|
| Cache hit rate | 0% | 42% | - |
| API calls | 100K/day | 58K/day | 42% reduction |
| Monthly cost | $15,000 | $8,700 | 42% savings |
| Technique | Token Savings | Quality Impact |
|---|
| Remove redundant instructions | 10-20% | None |
| Shorten examples | 20-30% | Minimal |
| Use abbreviations in system prompt | 5-10% | None |
| Dynamic context inclusion | 30-50% | None |
| Compression prompts | 40-60% | Low risk |
| Prompt Component | Before | After | Savings |
|---|
| System prompt | 800 tokens | 400 tokens | 50% |
| Examples | 1200 tokens | 600 tokens | 50% |
| Context | 2000 tokens | 1000 tokens | 50% |
| Total | 4000 tokens | 2000 tokens | 50% |
| Technique | Implementation | Savings |
|---|
| Max tokens | Hard limit | Prevents runaway |
| Concise instructions | "Be brief" | 20-30% |
| Structured output | JSON schema | 10-20% |
| Stop sequences | Early termination | Variable |
| Scenario | Batch? | Benefit |
|---|
| Real-time chat | No | Latency critical |
| Document processing | Yes | 20-30% savings |
| Analytics | Yes | 30-40% savings |
| Nightly jobs | Yes | Off-peak pricing |
| Batch Size | Throughput | Cost per Item |
|---|
| 1 | Baseline | Baseline |
| 10 | 8x | 80% of baseline |
| 50 | 30x | 70% of baseline |
| 100 | 50x | 65% of baseline |
| Monthly Volume | API Cost | Self-Hosted Cost | Winner |
|---|
| 10M tokens | $500 | $2,500 | API |
| 50M tokens | $2,500 | $2,500 | Tie |
| 100M tokens | $5,000 | $2,500 | Self-hosted |
| 500M tokens | $25,000 | $3,000 | Self-hosted |
| Component | Monthly Cost | Notes |
|---|
| GPU instance (A100) | $2,000-3,000 | Primary cost |
| Storage | $100-200 | Model weights |
| Networking | $50-100 | Egress |
| Operations | $500-1,000 | Engineering time |
| Workload | Deployment | Reasoning |
|---|
| High volume, simple | Self-hosted | Cost efficiency |
| Low volume, complex | API | Capability |
| Spiky traffic | API | Elasticity |
| Sensitive data | Self-hosted | Privacy |
| Optimization | Savings | Implementation |
|---|
| Debouncing | 20-40% | Wait for user pause |
| Deduplication | 10-20% | Hash recent requests |
| Prefetching | 5-10% | Predict next query |
| Cancellation | 10-15% | Cancel abandoned |
| Issue | Bad Practice | Good Practice | Savings |
|---|
| Rate limits | Immediate retry | Exponential backoff | 30% |
| Timeouts | Full retry | Resume/checkpoint | 50% |
| Errors | Retry same model | Fallback model | 20% |
| Pattern | Description | Savings |
|---|
| Cache-first | Check cache before API | 30-50% |
| Tiered processing | Simple to complex escalation | 40-60% |
| Lazy evaluation | Generate on demand | 20-30% |
| Pre-computation | Offline processing | Variable |
| Step | Action | Cost |
|---|
| 1 | Check cache | Free |
| 2 | Try small model | $0.002 |
| 3 | Validate quality | $0.001 |
| 4 | Escalate if needed | $0.10 |
| Average | - | $0.025 |
| Action | Effort | Savings |
|---|
| Add caching layer | Low | 20-30% |
| Optimize prompts | Low | 10-20% |
| Set output limits | Low | 5-10% |
| Action | Effort | Savings |
|---|
| Implement routing | Medium | 30-50% |
| Semantic caching | Medium | 10-20% |
| Batch processing | Medium | 10-15% |
| Action | Effort | Savings |
|---|
| Self-hosting evaluation | High | 30-50% |
| Custom fine-tuning | High | 20-40% |
| Architecture redesign | High | Variable |
| Metric | Target | Alert |
|---|
| Cost per request | Track trend | 20% increase |
| Cache hit rate | Over 30% | Under 20% |
| Routing accuracy | Over 90% | Under 85% |
| Token efficiency | Improving | Degrading |
| Dimension | Why Track |
|---|
| By feature | Identify expensive features |
| By user segment | Understand usage patterns |
| By model | Optimize routing |
| By time | Identify spikes |
- 1Model routing is the biggest lever - Using the right model for each query can save 60-70%.
- 2Caching compounds - Exact + semantic caching can eliminate 40%+ of API calls.
- 3Prompts are money - Every token costs. Optimize ruthlessly.
- 4Self-hosting makes sense at scale - Above 50M tokens/month, run the numbers.
- 5Measure everything - You cannot optimize what you do not measure.
- 6Quality trade-offs exist - 1-2% quality reduction for 70% cost reduction is often worth it.
Cost optimization is not a one-time project. It is an ongoing discipline. Build cost awareness into your development process from day one.