Skip to main content
Architecture

LLM Cost Optimization in Production: Semantic Caching, Batching, and Smart Model Routing

Running a frontier model for every agent call at enterprise scale is a budget problem. Smart model routing, semantic caching, and request batching can cut inference costs substantially — how much depends on your workload, so measure it on your own traffic.

Inductivee Intelligence Team· AI EngineeringNovember 5, 2025(updated November 1, 2026)12 min read
TL;DR

Note on dates: the model names in this article are examples from 2025. Providers have since released newer models and changed their prices, so check current pricing pages before you size a budget; the levers below apply whichever models you use. A model routing system that correctly classifies a large share of queries as simple (routing them to a small model) cuts inference cost on that share by the price ratio between the tiers. Combined with semantic caching and output length control, the total saving can be large, but it depends on your workload mix, cache hit rate and quality bar — measure all three before you promise a number.

The Enterprise LLM Cost Problem

Inference cost scales with volume. A platform processing 50,000 requests per day at an average of 2,000 input tokens and 500 output tokens per request consumes 100 million input tokens and 25 million output tokens every day. Multiply those by your provider's current per-million-token prices, then by 365, and for a top-tier model the annual figure is usually an unbudgeted shock when the pilot scales to production.

The instinct to control costs by restricting AI usage is wrong. The correct response is to optimize the inference stack so that you are paying top-tier prices only for top-tier-quality tasks. List prices differ by an order of magnitude or more between top-tier and small models and change often, so check each provider's pricing page.

The optimization levers are well-understood: model routing (right model for right complexity), semantic caching (avoid re-running similar queries), request batching (improve throughput, reduce overhead), prompt compression (remove redundant tokens before sending), and output length control (stop generating when the task is done). Applying all five levers in a production system requires engineering investment, but the ROI is clear.

LLM Model Tiers — 2025 Examples (Check Current Models and Pricing)

Model (2025 example)Best ForRelative Quality
GPT-4oComplex reasoning, multi-step agentic tasksHighest
Claude 3.5 SonnetLong-context reasoning, coding, analysisHighest
Claude 3.5 HaikuFast classification, summarisation, extractionHigh
GPT-4o-miniSimple classification, formatting, routingGood
Llama 3.1 70B (self-hosted)High-volume simple tasks, data extractionGood
Llama 3.1 8B (self-hosted)Bulk classification, structured extractionModerate

The Five Cost Optimization Levers

Lever 1: Model Routing by Complexity

Route queries to the appropriate model tier based on estimated task complexity. Simple tasks — classification, keyword extraction, format conversion, FAQ-style lookups — route to GPT-4o-mini or Llama 3.1 70B. Complex tasks — multi-step reasoning, code generation, nuanced analysis, agent planning — route to GPT-4o or Claude 3.5 Sonnet. The router itself should be a lightweight classifier (fine-tuned BERT or GPT-4o-mini), not the expensive model. The share of requests that qualify as simple varies widely by workload; measure it on your own traffic before projecting savings.

Lever 2: Semantic Caching

Cache LLM responses indexed by semantic similarity of the input query, not exact string match. When a new query arrives, check if a semantically similar query has been answered before (for example, cosine similarity above 0.95 against cached query embeddings — tune the threshold on your own data). If a cache hit is found, return the cached response without an LLM call. Cache hit rates vary widely: repetitive FAQ, policy-lookup and documentation-search traffic tends to hit more often than open-ended queries, so measure your own hit rate before counting on savings. Use Redis with vector extensions or a dedicated semantic cache library (GPTCache).

Lever 3: Async Request Batching

Batch independent requests together and process them in parallel. Rather than making sequential LLM calls for 20 items to process, assemble them into a single batched request (OpenAI Batch API at 50% discount) or process concurrently with asyncio. The Batch API is specifically valuable for document processing pipelines where the Batch API's 50% discount (per OpenAI's documentation) comes with a completion window of up to 24 hours, which is acceptable for non-interactive workflows.

Lever 4: Prompt Compression

Long system prompts sent on every request are a significant hidden cost. A 2,000-token system prompt sent 50,000 times per day adds 100 million input tokens a day (50,000 × 2,000) before a single word of user input is counted. Techniques: compress prompts using a method such as LLMLingua (Jiang et al., arXiv 2310.05736), validating quality on your own tasks; use your provider's prompt caching for static system prompts (OpenAI enables it by default for supported models and bills reused prefix tokens at a reduced rate — check the current terms); and audit prompts quarterly to remove accumulated instructions that no longer apply.

Lever 5: Output Length Control

LLMs have a tendency to over-generate — providing more explanation than the task requires. For structured extraction and classification tasks, explicit max_tokens constraints and JSON response format forcing (response_format: {type: json_object}) can reduce average output length; check quality on your own evaluation set. For agentic tasks, instruct the agent to stop when the task is complete rather than summarising every step taken.

Model Router with Complexity Classification and Semantic Cache

python
import asyncio
import hashlib
import json
from dataclasses import dataclass
from enum import Enum
from typing import Optional
import numpy as np
from openai import AsyncOpenAI
from redis.asyncio import Redis


class ModelTier(str, Enum):
    BUDGET = "gpt-4o-mini"          # example 2025 budget-tier model
    STANDARD = "gpt-4o"             # example 2025 standard-tier model
    REASONING = "o1-mini"           # Reserved for explicit reasoning tasks


@dataclass
class RoutedRequest:
    prompt: str
    system: str
    model: ModelTier
    cache_hit: bool
    estimated_cost_usd: float


COMPLEXITY_CLASSIFIER_PROMPT = """Classify the complexity of this task.
Simple: classification, extraction, lookup, format conversion, yes/no questions.
Complex: multi-step reasoning, code generation, detailed analysis, creative generation.
Respond with exactly one word: simple or complex."""

# Illustrative per-token input prices from 2025: replace with your provider's current list prices
PRICING = {
    ModelTier.BUDGET: 0.00000015,    # illustrative
    ModelTier.STANDARD: 0.000005,    # illustrative
    ModelTier.REASONING: 0.000003,   # illustrative
}


class IntelligentModelRouter:
    """
    Routes LLM requests to appropriate model tier based on complexity,
    with semantic caching to avoid redundant API calls.
    """

    CACHE_SIMILARITY_THRESHOLD = 0.95
    CACHE_TTL_SECONDS = 3600  # 1 hour for most enterprise queries

    def __init__(self, redis_url: str = "redis://localhost:6379"):
        self.openai = AsyncOpenAI()
        self._redis_url = redis_url
        self._redis: Optional[Redis] = None
        self._cache_index: list[tuple[list[float], str]] = []  # (embedding, cache_key)
        self._stats = {"total": 0, "cache_hits": 0, "budget_routes": 0, "standard_routes": 0}

    async def connect(self):
        from redis.asyncio import from_url
        self._redis = await from_url(self._redis_url, decode_responses=True)

    async def _embed(self, text: str) -> list[float]:
        resp = await self.openai.embeddings.create(
            input=text[:2000],  # Truncate for cache key generation
            model="text-embedding-3-small",
            dimensions=768
        )
        return resp.data[0].embedding

    async def _check_semantic_cache(
        self, query_embedding: list[float]
    ) -> Optional[str]:
        if not self._cache_index:
            return None
        query_vec = np.array(query_embedding)
        best_score = 0.0
        best_key = None
        for cached_emb, cache_key in self._cache_index[-1000:]:  # Check last 1000
            cached_vec = np.array(cached_emb)
            score = float(np.dot(query_vec, cached_vec) /
                          (np.linalg.norm(query_vec) * np.linalg.norm(cached_vec)))
            if score > best_score:
                best_score = score
                best_key = cache_key
        if best_score >= self.CACHE_SIMILARITY_THRESHOLD and best_key:
            return best_key
        return None

    async def _store_cache(self, query_embedding: list[float],
                           response: str, ttl: int) -> None:
        cache_key = "llm:" + hashlib.sha256(
            str(query_embedding[:10]).encode()
        ).hexdigest()[:16]
        await self._redis.setex(cache_key, ttl, response)
        self._cache_index.append((query_embedding, cache_key))

    async def _classify_complexity(self, prompt: str) -> ModelTier:
        resp = await self.openai.chat.completions.create(
            model="gpt-4o-mini",  # Always use budget model for classification
            messages=[
                {"role": "system", "content": COMPLEXITY_CLASSIFIER_PROMPT},
                {"role": "user", "content": prompt[:500]},  # Truncate for speed
            ],
            max_tokens=5,
            temperature=0,
        )
        classification = resp.choices[0].message.content.strip().lower()
        return ModelTier.STANDARD if classification == "complex" else ModelTier.BUDGET

    def _estimate_cost(self, prompt: str, model: ModelTier) -> float:
        token_estimate = len(prompt.split()) * 1.3  # Rough token estimate
        return token_estimate * PRICING[model]

    async def complete(
        self,
        prompt: str,
        system: str = "",
        force_model: Optional[ModelTier] = None,
        cache_ttl: int = CACHE_TTL_SECONDS,
    ) -> RoutedRequest:
        self._stats["total"] += 1
        full_prompt = f"{system}\n\n{prompt}" if system else prompt

        # Check semantic cache first
        query_embedding = await self._embed(full_prompt)
        cache_key = await self._check_semantic_cache(query_embedding)
        if cache_key:
            cached = await self._redis.get(cache_key)
            if cached:
                self._stats["cache_hits"] += 1
                return RoutedRequest(prompt=prompt, system=system,
                                     model=ModelTier.BUDGET, cache_hit=True,
                                     estimated_cost_usd=0.0)

        # Route to appropriate model
        model = force_model or await self._classify_complexity(prompt)
        if model == ModelTier.BUDGET:
            self._stats["budget_routes"] += 1
        else:
            self._stats["standard_routes"] += 1

        messages = []
        if system:
            messages.append({"role": "system", "content": system})
        messages.append({"role": "user", "content": prompt})

        resp = await self.openai.chat.completions.create(
            model=model.value, messages=messages, temperature=0.2
        )
        response_text = resp.choices[0].message.content
        await self._store_cache(query_embedding, response_text, cache_ttl)

        return RoutedRequest(
            prompt=prompt, system=system, model=model, cache_hit=False,
            estimated_cost_usd=self._estimate_cost(full_prompt, model)
        )

    def print_stats(self):
        n = self._stats["total"] or 1
        print(f"Total: {n} | Cache hits: {self._stats['cache_hits']/n:.1%} | "
              f"Budget routes: {self._stats['budget_routes']/n:.1%} | "
              f"Standard routes: {self._stats['standard_routes']/n:.1%}")


async def main():
    router = IntelligentModelRouter()
    await router.connect()
    result = await router.complete(
        prompt="What is the refund policy for enterprise customers?",
        system="You are a helpful assistant for Acme Corp."
    )
    print(f"Model: {result.model.value}, Cache hit: {result.cache_hit}, "
          f"Est. cost: ${result.estimated_cost_usd:.6f}")
    router.print_stats()


if __name__ == "__main__":
    asyncio.run(main())

Model router with semantic cache and complexity classifier. The complexity classifier itself always runs on GPT-4o-mini to avoid the paradox of using an expensive model to decide when to use an expensive model. In production, the cache_index should be backed by Redis with vector extensions (Redis Stack) rather than the in-memory list shown here for simplicity.

Tip

OpenAI's Batch API processes requests asynchronously within a 24-hour completion window at a 50% discount compared with synchronous APIs (OpenAI documentation). For any workflow that processes documents, generates reports, or runs batch analyses — where real-time response is not required — the Batch API is usually worth it: a job that would cost X at standard API rates costs half of X via the Batch API. Route all non-interactive workloads through the Batch API as a default.

Cost Optimization Quick Wins (Implement This Week)

  • Audit your top-5 highest-volume agent prompts. Are any of them sending 2,000+ token system prompts on every call? Compress them with LLMLingua or restructure to use prompt caching.
  • Identify all classification and routing calls in your agent stack. These are prime candidates for downgrade to GPT-4o-mini — much cheaper, with comparable accuracy on many binary and categorical classification tasks (verify on your own data).
  • Implement response_format: json_object on all structured extraction calls. This can reduce output token usage by preventing verbose explanatory text.
  • Add max_tokens limits to all non-creative LLM calls. Many factual Q&A, classification, and extraction tasks complete within a few hundred output tokens. Unconstrained generation is a silent cost leak.
  • Set up cost monitoring with per-endpoint token tracking before implementing any other optimization. You cannot optimise what you do not measure — and most teams are surprised by where their token budget actually goes.

Inductivee Intelligence's Cost Optimization Framework

When auditing enterprise LLM spend, the two findings we see most often are: unconstrained system prompt length and GPT-4o used for classification tasks. In our experience these two are often among the largest sources of preventable cost. The fix for both is fast, non-destructive, and measurable within a week.

The more sophisticated optimization — semantic caching — requires up-front investment in infrastructure but pays out over months. For enterprise RAG systems where the same knowledge base is queried repeatedly by different users, cache hit rates can be meaningful. Model the saving as hit rate × request volume × per-request cost, less the cost of running the cache, using your own numbers.

For teams just starting down the cost optimization path: measure first, optimise second. A one-week instrumentation sprint adding per-call token logging to every LLM call will reveal the top five cost drivers with enough precision to prioritise which levers to pull first. Building a model router before understanding your cost distribution is premature optimisation.

Sources & Further Reading

Provider mechanics were checked against the primary sources below on 1 November 2026. Model names and prices change often; confirm on the provider's pricing page.

Frequently Asked Questions

How much can LLM inference costs be reduced in production?

Combining model routing (budget models for simple tasks), semantic caching, prompt compression, and output length control can reduce LLM inference costs substantially versus sending every request to a top-tier model. The size of the reduction depends on workload mix — repetitive knowledge-base query workloads with high cache hit rates save more than diverse creative-generation workloads — so measure it on your own traffic.

What is model routing in LLM applications?

Model routing classifies incoming requests by task complexity and directs them to the appropriate model tier — budget models (GPT-4o-mini, Llama 3.1 70B) for simple classification, extraction, and lookup tasks, and premium models (GPT-4o, Claude 3.5 Sonnet) for complex reasoning and generation. The router itself should be a lightweight classifier running on the budget tier. A well-tuned router sends a large share of requests to budget-tier models; validate the quality impact on your own evaluation set before rolling it out.

What is semantic caching for LLMs?

Semantic caching stores LLM responses indexed by the semantic content of the input query (via embedding similarity), not exact string match. When a new query arrives with cosine similarity above a threshold (0.95 is a common starting point) against a cached query, the cached response is returned without an LLM API call. Repetitive enterprise knowledge-base traffic can achieve meaningful cache hit rates, which makes semantic caching a strong candidate cost lever — measure your own hit rate.

How do I estimate LLM inference cost at enterprise scale?

Multiply your expected daily requests by average input and output tokens, then by your provider's current per-million-token prices. For example, 50,000 requests per day at 2,000 input and 500 output tokens each is 100 million input tokens and 25 million output tokens per day. Prices and model lineups change frequently, so take the per-token prices from the provider's pricing page on the day you build the model, then apply routing, caching and batching assumptions you have measured rather than assumed.

What is prompt caching and how does it reduce LLM costs?

Prompt caching (available via OpenAI and Anthropic) stores the KV (key-value) representation of a prompt prefix at the API level, so repeat requests with the same prompt prefix are billed at a reduced rate for the cached input tokens (the discount and conditions vary by provider and model; check the current terms). It is most effective for high-volume agents with long, static system prompts. A 2,000-token system prompt sent 50,000 times per day is 100 million input tokens a day that are eligible for the reduced cached rate.

Written By

Inductivee Intelligence Team — AI Engineering at Inductivee Intelligence

Inductivee Intelligence Team

Author

Agentic AI Engineering Team

The Inductivee Intelligence engineering team — multi-agent orchestration, RAG pipeline architecture, and data liquidity engineering. Our writing is grounded in what we actually build, break, and operate in production.

Agentic AI ArchitectureMulti-Agent OrchestrationLangChainLangGraphCrewAIMicrosoft Agent Framework
LinkedIn profile

Inductivee Intelligence is a remote-first agentic AI engineering studio. Our engineering content is written by active practitioners and technically reviewed before publication.

Ready to Build This Into Your Enterprise?

Inductivee Intelligence engineers agentic systems, RAG pipelines, and enterprise data liquidity solutions. Let's scope your project.

Start a Project