Introducing the 4-Month AI FrameworkRead the guide

How to Reduce LLM Costs Without Sacrificing Quality

LLM API costs crushing your budget? Learn the techniques we use to cut costs on production systems without sacrificing quality.

Muhammad Usman Ali

Muhammad Usman Ali

Co-Founder & Director of Engineering

· 13 min read

How to Reduce LLM Costs Without Sacrificing Quality
On this page · 11 sections
  1. 01The Cost Breakdown: Where Money Actually Goes
  2. 02Stage 1: Prompt Optimization
  3. 03Stage 2: Caching & Model Selection
  4. 04Stage 3: Advanced Optimizations
  5. 05The Complete Cost Optimization Playbook
  6. 06Cost Optimization by Use Case
  7. 07Monitoring and Measuring Cost Optimization
  8. 08Common Mistakes to Avoid
  9. 09The Economics: When Optimization Pays Off
  10. 10Beyond OpenAI: Alternative Providers
  11. 11Our Final Architecture (After All Optimizations)

LLM API bills grow faster than most teams expect. A prototype that costs a few dollars a day can turn into a serious line item once real users arrive. This guide walks through the techniques we use to bring LLM costs down on production systems, in the order we usually apply them.

We do not quote one headline savings number here. Savings depend on your traffic, your prompts, your model choices and your quality bar. What we can give you is a method: measure first, fix the biggest cost driver, then repeat.

Our worked example is Sokrateque.ai, an AI research assistant that searches a large library of academic papers. Retrieval-heavy products like this send a lot of text to the model on every query, so they are a good test bed for cost work.

The Cost Breakdown: Where Money Actually Goes

Before optimizing, understand where costs come from.

Typical LLM Application Cost Structure:

Per-query cost, before optimization:

Input tokens:  prompt plus retrieved context   (usually the largest share)
Output tokens: the generated response          (second largest)
Embeddings:    vector search                   (small)
Other:         reranking, validation, etc.     (small)

The insight: For retrieval-based apps, input tokens usually dominate cost. That is where to look first. Check current provider pricing for exact rates, because they change often.

The order we work in:

  1. Stage 1: Fix the prompts
  2. Stage 2: Add caching and pick the right model for each query
  3. Stage 3: Apply the advanced techniques if your scale justifies them

Let's walk through each stage.

Stage 1: Prompt Optimization

The problem: Many first versions send huge prompts on every query.

Original approach (expensive):

# Naive implementation (expensive)
def answer_query(user_query):
    # Retrieve relevant papers
    papers = vector_search(user_query, top_k=10)

    # Build a very large prompt
    prompt = f"""
    You are an academic research assistant...

    PAPERS:
    """

    # Include the FULL text of every paper
    for paper in papers:
        prompt += f"\nPaper {paper.id}:\n"
        prompt += f"Title: {paper.title}\n"
        prompt += f"Full text: {paper.full_text}\n"  # very large

    prompt += f"\nUser question: {user_query}\nAnswer:"

    response = llm.generate(
        model="large-model",
        messages=[{"role": "user", "content": prompt}]
    )

    return response

Problems:

  • Sending full papers on every query
  • Repetitive instructions taking up tokens
  • Including information the model does not need

Optimization 1.1: Compress Retrieved Context

Don't send full papers. Send only the relevant excerpts.

def answer_query_optimized(user_query):
    papers = vector_search(user_query, top_k=10)

    # Extract only the relevant sections
    relevant_excerpts = []
    for paper in papers:
        excerpts = extract_relevant_excerpts(
            paper=paper,
            query=user_query,
            max_excerpts=3,   # top paragraphs per paper
            max_tokens=500    # limit each excerpt
        )
        relevant_excerpts.extend(excerpts)

    prompt = f"""
    Answer based on these excerpts from academic papers:

    {format_excerpts(relevant_excerpts)}

    Question: {user_query}
    Answer:
    """

    response = llm.generate(
        model="large-model",
        messages=[{"role": "user", "content": prompt}]
    )

    return response

Implementation of excerpt extraction:

def extract_relevant_excerpts(paper, query, max_excerpts=3, max_tokens=500):
    """
    Find the most relevant sections of a paper for a query.
    """
    paragraphs = split_into_paragraphs(paper.full_text)

    # Score each paragraph by relevance to the query
    scored_paragraphs = []
    query_embedding = embed(query)

    for para in paragraphs:
        para_embedding = embed(para)
        similarity = cosine_similarity(query_embedding, para_embedding)

        scored_paragraphs.append({
            'text': para,
            'score': similarity,
            'token_count': count_tokens(para)
        })

    # Sort by relevance
    scored_paragraphs.sort(key=lambda x: x['score'], reverse=True)

    # Select top paragraphs that fit the token budget
    selected = []
    total_tokens = 0

    for para in scored_paragraphs:
        if len(selected) >= max_excerpts:
            break
        if total_tokens + para['token_count'] <= max_tokens:
            selected.append(para)
            total_tokens += para['token_count']

    return selected

Quality impact: Less irrelevant text often helps the model focus. Test this on your own data and compare answer quality before and after.

Optimization 1.2: System Prompt Efficiency

Move repeated instructions into the system prompt. Many providers offer discounted pricing for cached prompt prefixes, so check current provider pricing and caching rules.

# Before: instructions repeated in every user message
messages = [
    {
        "role": "user",
        "content": f"""
        You are an academic research assistant. [instructions]

        Papers: {papers}
        Question: {query}
        """
    }
]

# After: instructions in a stable system prompt
messages = [
    {
        "role": "system",
        "content": """
        You are an academic research assistant. [instructions]

        Always cite papers when answering.
        Be concise and accurate.
        """
    },
    {
        "role": "user",
        "content": f"""
        Papers: {papers}
        Question: {query}
        """
    }
]

Optimization 1.3: Reduce Output Verbosity

Shorter outputs cost less. Models are verbose by default.

# Before: no length control
prompt = "Explain this research paper in detail."

# After: explicit length control
prompt = "Explain this research paper in 2 to 3 sentences (max 100 words)."

# Alternative: use the max_tokens parameter as a hard limit
response = llm.generate(
    model="large-model",
    messages=[...],
    max_tokens=200
)

Stage 1 Results:

In our experience, Stage 1 gives the biggest return for the least effort. Track these three changes separately so you can see which one matters for your workload.

OptimizationCost ImpactQuality Impact
Compress context (excerpts)Largest of the three for retrieval appsTest on your data
System prompt efficiencySmallNo change expected
Reduce output verbosityModerateCheck that answers stay complete

Stage 2: Caching & Model Selection

Optimization 2.1: Caching

The insight: Many queries are similar or identical.

Level 1: Exact query cache (Redis)

def answer_with_cache(user_query):
    # Check if this exact query was asked before
    cache_key = f"query:{hash(user_query)}"
    cached_response = redis.get(cache_key)

    if cached_response:
        return json.loads(cached_response)  # no API cost

    # Not cached, generate an answer
    response = generate_answer(user_query)

    # Cache for 24 hours
    redis.setex(cache_key, 86400, json.dumps(response))

    return response

How much this saves depends on how many of your queries are exact repeats. Support bots tend to see many. Research tools see fewer.

Level 2: Semantic similarity cache

# Check for semantically similar queries
def semantic_cache_lookup(user_query):
    query_embedding = embed(user_query)

    # Search the cache for similar queries
    similar_queries = vector_search_cache(
        query_embedding,
        similarity_threshold=0.95  # very high threshold
    )

    if similar_queries:
        return similar_queries[0].response

    return None

Set the threshold high. A loose threshold returns wrong answers to questions that only look similar.

Level 3: Cache retrieved documents

# Don't re-retrieve the same papers for similar queries
def retrieve_with_cache(query):
    cache_key = f"retrieval:{hash(query)}"
    cached_papers = redis.get(cache_key)

    if cached_papers:
        return json.loads(cached_papers)

    papers = vector_search(query, top_k=10)

    # Cache for 1 hour
    redis.setex(cache_key, 3600, json.dumps(papers))

    return papers

Optimization 2.2: Model Selection (Tiered Strategy)

Not every query needs the largest model. Smaller models cost much less per token, so route simple queries to them. Check current provider pricing for exact rates.

Tiered strategy:

def route_to_appropriate_model(user_query, complexity_score):
    """
    Route queries to the cheapest model that can handle them.
    """

    if complexity_score < 0.3:    # simple query
        model = "small-model"
    elif complexity_score < 0.7:  # medium query
        model = "mid-model"
    else:                         # complex query
        model = "large-model"

    return model

def calculate_query_complexity(query, retrieved_docs):
    """
    Score query complexity from 0 to 1.
    """
    complexity = 0

    # Factor 1: query length
    if count_tokens(query) > 100:
        complexity += 0.2

    # Factor 2: number of documents
    if len(retrieved_docs) > 5:
        complexity += 0.2

    # Factor 3: technical difficulty (keywords)
    technical_keywords = ['analyze', 'compare', 'synthesize', 'evaluate']
    if any(kw in query.lower() for kw in technical_keywords):
        complexity += 0.3

    # Factor 4: multi-document synthesis required
    if len(retrieved_docs) > 3:
        complexity += 0.3

    return min(complexity, 1.0)

The scoring rules above are a starting point. Tune the thresholds against your own traffic.

Quality impact: Run a side-by-side test before you route real traffic. Compare answers from each tier on your own questions, and keep a quality threshold you will not go below.

Stage 2 Results:

OptimizationCost ImpactQuality Impact
Caching (multi-level)Depends on repeat trafficNo change if thresholds are strict
Model selection (tiered)Often the largest lever at scaleTest per tier
Batch processingSmallNo change

Stage 3: Advanced Optimizations

Optimization 3.1: Streaming Responses

Don't wait for the entire response. Stream tokens as they are generated.

# Before: wait for the complete response
response = llm.generate(
    model="large-model",
    messages=[...]
)
# User waits and sees nothing

# After: stream the response
stream = llm.generate(
    model="large-model",
    messages=[...],
    stream=True
)

for chunk in stream:
    token = chunk.choices[0].delta.content
    yield token  # send to the user immediately

Cost benefit:

  • Direct: None, because the token cost is the same
  • Indirect: Users feel it is faster, so they accept smaller models
  • Indirect: You can stop generation early if the user navigates away, which saves output tokens

Optimization 3.2: Prompt Compression Techniques

Technique 1: Compact formatting

# Before: verbose metadata
prompt = f"""
Paper Title: {paper.title}
Authors: {', '.join(paper.authors)}
Publication Year: {paper.year}
Journal: {paper.journal}
Abstract: {paper.abstract}
"""

# After: compressed format
prompt = f"""
{paper.title} ({paper.year})
{paper.authors[0]} et al. | {paper.journal}
{paper.abstract}
"""

Technique 2: Remove filler words

# Before:
"Please analyze the following papers and provide a thorough summary..."

# After:
"Analyze papers, summarize:"

Models do not need politeness. Test that your shorter prompt still gives the same quality.

Optimization 3.3: Query Preprocessing

Filter out junk before calling the model.

def preprocess_query(user_query):
    """
    Validate and, where possible, handle the query without the LLM.
    """

    # Filter 1: empty or too short
    if len(user_query.strip()) < 10:
        return {"response": "Please provide a more detailed question.", "skip_llm": True}

    # Filter 2: obvious spam or abuse
    if is_spam(user_query):
        return {"response": "Invalid query.", "skip_llm": True}

    # Filter 3: FAQ (exact match)
    faq_answer = check_faq_database(user_query)
    if faq_answer:
        return {"response": faq_answer, "skip_llm": True}

    # Filter 4: out of scope
    if not is_academic_query(user_query):
        return {
            "response": "I can only help with academic research questions.",
            "skip_llm": True
        }

    # Passed all filters, proceed to the LLM
    return {"skip_llm": False}

Optimization 3.4: Adaptive Context Window

Don't always use the maximum context. Adjust it to the query.

def get_optimal_context_size(query, available_docs):
    """
    Decide how many documents are actually needed.
    """

    # Simple query: fewer docs needed
    if is_simple_query(query):
        return min(3, len(available_docs))

    # Comparative query: more docs needed
    elif is_comparative_query(query):
        return min(7, len(available_docs))

    # Complex synthesis: many docs needed
    else:
        return min(10, len(available_docs))

Stage 3 Results:

OptimizationCost ImpactQuality Impact
Streaming (indirect)SmallBetter UX
Prompt compressionSmallTest before shipping
Query preprocessingSmallBetter (spam filtered)
Adaptive contextSmall to moderateTest before shipping

The Complete Cost Optimization Playbook

Here is the framework we use on new projects. We give no savings percentages because they vary too much between systems.

Phase 1: Low-Hanging Fruit (Weeks 1 to 2)

Immediate wins with minimal effort:

Compress prompts

  • Remove verbose instructions
  • Use system prompts for repeated text
  • Eliminate filler words

Cache exact queries

  • Simple Redis cache for duplicates
  • 24-hour TTL

Control output length

  • Add the max_tokens parameter
  • Request concise responses in the prompt

Phase 2: Strategic Optimizations (Months 2 to 3)

These need more engineering but usually pay back well:

Implement tiered model routing

  • Classify query complexity
  • Route to the right model tier

Semantic caching

  • Cache similar queries, not just exact ones
  • Vector search in the cache

Optimize retrieval

  • Send only relevant excerpts
  • Use hierarchical retrieval

Phase 3: Advanced Techniques (Month 4 and later)

For high-scale systems where the added complexity is worth it:

Custom compression

  • Domain-specific abbreviations
  • Structured formats (JSON)

Adaptive context

  • Variable document count
  • Query-dependent retrieval

Template reuse

  • Identify common patterns
  • Generate templates

Cost Optimization by Use Case

Different applications need different strategies.

Use Case 1: Customer Support Chatbot

Characteristics:

  • High query volume
  • Many duplicate questions
  • Real-time responses required

Recommended optimizations (priority order):

  1. Exact caching (highest impact): Support queries repeat often, so caching tends to pay off fast
  2. FAQ database: Answer common questions without calling the LLM
  3. Model tiering: Simple questions go to a small model and complex ones to a larger model

Use Case 2: Content Generation

Characteristics:

  • Long outputs
  • Creative, unique content
  • Lower query volume

Recommended optimizations:

  1. Prompt compression: Input tokens matter less here
  2. Streaming (UX and cost): Stop early if the user navigates away
  3. Output length control: Request exactly what is needed

Use Case 3: Document Analysis (RAG Systems)

Characteristics:

  • Large context windows with many documents
  • Moderate query volume
  • High input token costs

Recommended optimizations:

  1. Context compression (critical): Send excerpts, not full documents
  2. Retrieval caching: Cache retrieved documents
  3. Semantic caching: Similar queries often need similar documents

Monitoring and Measuring Cost Optimization

You can't optimize what you don't measure.

Metrics to Track:

# Cost per query tracking
cost_metrics = {
    'input_tokens': 0,
    'output_tokens': 0,
    'embedding_tokens': 0,
    'total_cost': 0.0,
    'model_used': '',
    'cache_hit': False
}

# Track every query
def track_query_cost(query_id, metrics):
    db.insert('query_costs', {
        'query_id': query_id,
        'timestamp': datetime.now(),
        'input_tokens': metrics['input_tokens'],
        'output_tokens': metrics['output_tokens'],
        'total_cost': metrics['total_cost'],
        'model': metrics['model_used'],
        'cached': metrics['cache_hit']
    })

# Daily dashboard
def generate_cost_dashboard():
    today = datetime.now().date()

    metrics = {
        'total_queries': count_queries(today),
        'total_cost': sum_costs(today),
        'avg_cost_per_query': avg_cost(today),

        # Breakdown by source
        'cached_queries': count_cached(today),
        'cache_hit_rate': cache_hit_rate(today),

        # Model distribution
        'large_model_queries': count_by_model(today, 'large-model'),
        'small_model_queries': count_by_model(today, 'small-model'),

        # Cost by component
        'input_cost': sum_input_costs(today),
        'output_cost': sum_output_costs(today),
        'embedding_cost': sum_embedding_costs(today)
    }

    return metrics

Dashboard should show:

  • Daily cost trends (identify spikes)
  • Cost per query (track optimization impact)
  • Cache hit rate (measure caching effectiveness)
  • Model distribution (make sure tiering works)
  • Token usage breakdown (input vs output)

Common Mistakes to Avoid

Mistake 1: Optimizing Without Measuring

Wrong: "Let's compress prompts, it should help."

Right: Measure the baseline, implement a change, measure again, compare.

# Before optimizing: establish a baseline
baseline_cost = calculate_average_cost(days=7)
baseline_quality = measure_quality_metrics(days=7)

# Make the change
implement_optimization()

# Wait long enough for a fair comparison
time.sleep(7 * 86400)

# Measure the impact
new_cost = calculate_average_cost(days=7)
new_quality = measure_quality_metrics(days=7)

# Compare
cost_change = (new_cost - baseline_cost) / baseline_cost
quality_change = (new_quality - baseline_quality) / baseline_quality

print("Cost change:", round(cost_change * 100, 1))
print("Quality change:", round(quality_change * 100, 1))

Mistake 2: Sacrificing Quality for Cost

Wrong: Use the cheapest model for everything.

Right: Use the cheapest model that stays above your quality threshold.

Set a quality threshold for each use case before you start. Customer-facing answers need a higher bar than internal tools or experiments. Measure it on your own test set.

If an optimization drops quality below your threshold, don't ship it.

Mistake 3: Over-Engineering Too Early

Wrong: Build complex caching, routing and compression on day one.

Right: Implement in stages and prioritize by return.

ROI = annual savings / implementation cost

Rank each optimization by this ratio.
Start with the highest. Stop when the ratio no longer justifies the work.

Mistake 4: Not Planning for Scale

Wrong: "Our bill is small, so it doesn't matter."

Right: Optimize early, before costs grow.

Cost grows with traffic. A bill that is easy to ignore at 1,000 queries a day can become a real problem at 50 times that volume. Optimizing while costs are small makes changes easier to test and cheaper to roll back.

Optimize when costs are small and easy to test, before they become a crisis.

The Economics: When Optimization Pays Off

Should you optimize costs?

Decision framework:

  • Small API bill: Focus on the product. Cost is not your problem yet.
  • Growing API bill: Do the Phase 1 quick wins.
  • Large API bill: Do Phases 1 and 2, and measure the return on each.
  • Very large API bill: Do all phases and consider fine-tuning or self-hosting.

Where the lines fall depends on your margins and your engineering costs.

Break-even calculation:

Payback period = implementation cost / annual savings

Do the work if:
- Payback is short for your business
- API costs are trending upward
- You plan to scale significantly

Beyond OpenAI: Alternative Providers

Sometimes the best optimization is switching or mixing providers.

Provider prices change often, so we do not list rates here. Check current provider pricing for OpenAI, Anthropic, Google and any self-hosted option you are considering, then compare the cost per task on your own prompts.

Decision matrix:

  • Low volume: Stay with your current provider. Switching cost is not worth it.
  • Medium volume: Test a second provider on your own workload.
  • High volume: Consider a multi-provider setup and route by use case.
  • Very high volume: Consider self-hosted open models. Include infrastructure and staffing in the comparison.

Switching considerations:

  • Quality differences (test thoroughly)
  • API compatibility (different interfaces)
  • Rate limits (vary by provider)
  • Geographic availability (latency matters)

Our Final Architecture (After All Optimizations)

Here is what a production system looks like with these pieces together:

class OptimizedLLMSystem:
    def __init__(self):
        self.cache = MultiLevelCache()  # Redis plus in-memory
        self.models = ModelRouter()     # small, mid and large tiers
        self.metrics = CostTracker()    # monitor everything

    async def answer_query(self, user_query, user_id):
        # Step 1: Preprocessing
        preprocessed = preprocess_query(user_query)
        if preprocessed['skip_llm']:
            return preprocessed['response']

        # Step 2: Check cache (exact and semantic)
        cached = await self.cache.get(user_query)
        if cached:
            self.metrics.record_cache_hit(user_id)
            return cached

        # Step 3: Retrieve relevant documents (with caching)
        docs = await self.retrieve_with_cache(user_query)

        # Step 4: Extract relevant excerpts (compress context)
        excerpts = extract_relevant_excerpts(docs, user_query)

        # Step 5: Route to the appropriate model
        complexity = calculate_query_complexity(user_query, excerpts)
        model = self.models.select_model(complexity)

        # Step 6: Construct an optimized prompt
        prompt = construct_compressed_prompt(user_query, excerpts)

        # Step 7: Generate with streaming
        response = await self.generate_streaming(
            model=model,
            prompt=prompt,
            max_tokens=self.calculate_optimal_length(user_query)
        )

        # Step 8: Cache the result
        await self.cache.set(user_query, response)

        # Step 9: Track metrics
        self.metrics.record_query(
            user_id=user_id,
            model=model,
            input_tokens=count_tokens(prompt),
            output_tokens=count_tokens(response),
            cost=self.calculate_cost(model, prompt, response),
            cached=False
        )

        return response

Takeaways: The Cost Optimization Framework

The order of work:

Stage 1: Prompt Optimization Compress context, tighten instructions, control output length

Stage 2: Infrastructure Multi-level caching, model routing, batch processing

Stage 3: Advanced Query preprocessing, adaptive context, template reuse

Savings depend on your traffic, so measure each stage against your own baseline.

Key principles:

  1. Measure everything (you can't optimize blindly)
  2. Prioritize by return (highest impact first)
  3. Maintain a quality threshold (don't sacrifice accuracy for cost)
  4. Optimize early (before costs grow)
  5. Test incrementally (A/B test every change)

Start here:

If you are just beginning cost optimization:

  • Week 1: Add exact caching
  • Week 2: Compress prompts
  • Weeks 3 to 4: Add model routing

Measure cost per query before and after each step.

Need Help Optimizing Your LLM Costs?

We build cost-aware LLM applications, and we can review yours.

We offer a LLM cost review where we will:

  • Analyze your current cost structure
  • Identify the highest-impact optimizations
  • Recommend an implementation roadmap

Book a Call →

Work with EdgeFirm: see how we build cost-efficient custom LLM applications, or check our pricing to see how we scope and cost a project.

Topics

  • LLM costs
  • OpenAI optimization
  • API cost reduction
  • AI economics
  • prompt engineering
  • caching
  • enterprise AI solutions
  • AI solutions for enterprise
Muhammad Usman Ali

Written by

Muhammad Usman Ali

Co-Founder & Director of Engineering

Usman brings 8+ years of experience building enterprise systems. He specializes in system architecture, DevOps, and data pipelines that power production AI.

Work with EdgeFirm

Ship this in your stack in 4 to 5 months.

A 30-minute call with the founders who will write your code. Fixed price, full IP ownership, no offshore handoff.

Book Strategy CallOr get the 4-Month Framework
Talk to the founders

Get AI live in months, not years.

Book a 30-minute strategy call with the founders who will write your code.