teachyou.ai academy
← All posts
LangChain

LangChain Caching: Reducing Redundant LLM Calls

Pramod Dutta · Jul 2, 2026 · 15 min read

Why Your LLM Bill Keeps Climbing (And What To Do About It)

Picture this: a support chatbot gets asked "what's your refund policy?" forty times a day, in forty slightly different phrasings, and forty identical answers get generated from scratch every single time. Each one hits the API, burns tokens, and adds a second or two of latency the user has to sit through. Multiply that across a product with real traffic and you get a bill that grows faster than your user base and a UX that feels sluggish even though nothing about the question ever changes.

This is the problem LangChain caching exists to solve. Caching is one of those unglamorous engineering decisions — nobody puts "implemented an LLM cache" on a highlight reel — but it is often the single highest-leverage change you can make to a production LLM application. It reduces cost, it reduces latency, and in some architectures it even reduces the blast radius of provider outages, because a cached response doesn't care if the upstream API is having a bad day.

In this article we'll walk through how LangChain's caching layer actually works under the hood, the different backends available (in-memory, SQLite, Redis, and semantic caching), when each one makes sense, and the gotchas that trip people up — like caching non-deterministic outputs or forgetting that cache keys are sensitive to prompt formatting. By the end you'll have working code you can drop into a real project today.

How LangChain Caching Actually Works

LangChain's caching sits at the LLM/chat model layer, not at the application layer. That distinction matters. When you call a chat model through LangChain, the framework computes a cache key from the serialized prompt plus the model parameters (temperature, model name, stop sequences, and so on), checks whether that key already exists in the configured cache backend, and if it does, returns the stored response without touching the network at all.

If there's no hit, the call proceeds normally, and the response gets written back into the cache for next time. This happens transparently — your application code calling llm.invoke(...) doesn't need to know whether a cache exists underneath it.

The cache key generation is important to understand because it explains why caching sometimes "doesn't work" when people expect it to. Two prompts that differ by even a single whitespace character, or two calls with different temperature values, produce different cache keys and therefore different cache entries. Caching in LangChain is exact-match by default — it is not doing any semantic understanding of "these two prompts mean roughly the same thing." That's a feature layered on top (semantic caching, covered later), not the default behavior.

Here's the global cache setup, which is the simplest way to get started:

from langchain_core.globals import set_llm_cache
from langchain_community.cache import InMemoryCache
from langchain_openai import ChatOpenAI

set_llm_cache(InMemoryCache())

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# First call: hits the API
response1 = llm.invoke("What is the capital of France?")

# Second call, identical prompt: served from cache, no API call
response2 = llm.invoke("What is the capital of France?")

print(response1.content == response2.content)  # True

Notice the set_llm_cache call happens once, globally, and every LLM instance created afterward respects it. This is convenient for prototyping but can be a footgun in larger applications where you want different caching behavior for different chains — we'll get to per-call overrides shortly.

In-Memory Caching: The Quick Win

InMemoryCache is exactly what it sounds like — a Python dictionary living in process memory. It's the fastest possible cache because there's no serialization, no network hop, no disk I/O. It's also the least durable: restart your process and the cache is gone.

This makes InMemoryCache ideal for three scenarios: local development, unit tests that shouldn't hit real APIs repeatedly, and short-lived scripts or notebooks where you're iterating on a prompt and don't want to pay for the same generation five times while you tweak formatting downstream.

from langchain_core.globals import set_llm_cache
from langchain_community.cache import InMemoryCache
from langchain_openai import ChatOpenAI
import time

set_llm_cache(InMemoryCache())

llm = ChatOpenAI(model="gpt-4o-mini")

start = time.time()
llm.invoke("Summarize the plot of Hamlet in two sentences.")
print(f"First call: {time.time() - start:.2f}s")

start = time.time()
llm.invoke("Summarize the plot of Hamlet in two sentences.")
print(f"Second call (cached): {time.time() - start:.2f}s")

Run this and the second call will complete in a fraction of a millisecond compared to the first, because it never leaves the process. That gap is the entire value proposition of caching made visible.

The obvious limitation: InMemoryCache doesn't survive a process restart, and it doesn't share state across multiple worker processes if you're running something like Gunicorn with several workers. For anything beyond a single-process script, you need a persistent backend.

Persistent Caching With SQLite

SQLiteCache solves the durability problem by writing cache entries to a local SQLite database file. It's still a single-machine solution — it won't help you if you're running multiple servers behind a load balancer — but for a single-server deployment, a cron job, or a batch pipeline that runs repeatedly against overlapping inputs, it's a huge upgrade over losing your cache every time the process restarts.

from langchain_core.globals import set_llm_cache
from langchain_community.cache import SQLiteCache
from langchain_openai import ChatOpenAI

set_llm_cache(SQLiteCache(database_path=".langchain_cache.db"))

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

response = llm.invoke("Explain what a Bloom filter is, in one paragraph.")
print(response.content)

Run this script, kill the process, and run it again — the second run reads straight from .langchain_cache.db instead of calling the API. This is particularly useful for data pipelines that process the same documents repeatedly during development. If you're iterating on a RAG pipeline and re-running it against the same 200 documents every time you tweak a downstream step, SQLite caching means only the very first run actually costs money; every subsequent run of the same extraction or summarization step is free and near-instant.

One practical tip: put the cache database path in a location that's excluded from version control (add it to .gitignore) and consider using a different cache file per environment so your local dev cache doesn't leak into a shared staging cache by accident.

Distributed Caching With Redis

Once you have multiple application instances — say, a FastAPI service running behind a load balancer with four replicas — an in-process or single-file cache doesn't help much, because each replica has its own isolated cache and a request that hits replica 2 won't benefit from a cache entry written by replica 1. This is where Redis caching becomes the standard choice for production systems.

from langchain_core.globals import set_llm_cache
from langchain_community.cache import RedisCache
from langchain_openai import ChatOpenAI
import redis

redis_client = redis.Redis(host="localhost", port=6379, db=0)
set_llm_cache(RedisCache(redis_client=redis_client, ttl=3600))

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

response = llm.invoke("List three benefits of database indexing.")
print(response.content)

The ttl parameter above is worth calling out specifically, because it's easy to overlook and it matters a lot in practice. A cache with no expiration will happily serve a response from six months ago even if your prompt template, your business logic, or the "current" facts referenced by the answer have all changed since then. Setting a sensible TTL — an hour, a day, a week, depending on how time-sensitive your content is — keeps stale answers from lingering indefinitely. For anything involving current events, pricing, or user-specific state, keep the TTL short or skip caching for that call entirely.

Redis also gives you shared caching across every replica of your service, which is the whole point: replica 1 computes an answer once, writes it to Redis, and replicas 2 through 4 get it for free on their next matching request.

Semantic Caching: Beyond Exact String Matches

Exact-match caching is powerful but limited — "What's the capital of France?" and "what is france's capital city" are semantically identical questions but produce different cache keys under the default behavior, so both would trigger separate API calls. Semantic caching closes this gap by using embeddings to find "close enough" matches instead of requiring byte-for-byte identical prompts.

LangChain supports this pattern through cache implementations that compute an embedding for the incoming prompt and compare it against embeddings of previously cached prompts, returning a cached response if the similarity crosses a configured threshold.

from langchain_community.cache import RedisSemanticCache
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_core.globals import set_llm_cache

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

set_llm_cache(
    RedisSemanticCache(
        redis_url="redis://localhost:6379",
        embedding=embeddings,
        score_threshold=0.2,
    )
)

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

llm.invoke("What is the capital of France?")
# This semantically similar prompt can now hit the same cache entry:
llm.invoke("Tell me France's capital city")

Semantic caching is powerful but it's not free of tradeoffs. Every cache lookup now requires computing an embedding (an extra API call or local model inference) and running a similarity search, which adds latency and cost of its own — just less than a full LLM generation. The similarity threshold also needs tuning: too loose and you'll serve wrong answers to questions that only sound similar; too strict and you barely improve on exact matching. Start with a conservative threshold and loosen it only after you've validated the false-positive rate on real traffic.

Semantic caching makes the most sense for high-volume, high-repetition use cases like customer support bots or FAQ systems where paraphrased-but-equivalent questions are common, and it makes the least sense for creative or open-ended generation where near-duplicate inputs shouldn't necessarily produce identical outputs.

Caching Inside Chains and With Per-Call Overrides

So far every example uses set_llm_cache, which sets a single global cache for the entire process. That's fine for scripts, but in a real application you often want different caching behavior for different chains — maybe your classification chain should cache aggressively since the same categories keep recurring, while your creative-writing chain should never cache because you want fresh variation every time.

You can override caching at the model instantiation level instead of relying on the global setting:

from langchain_openai import ChatOpenAI
from langchain_community.cache import InMemoryCache

shared_cache = InMemoryCache()

# This model caches
classifier_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0, cache=shared_cache)

# This model explicitly never caches, regardless of global settings
creative_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.9, cache=False)

Setting cache=False on a specific model instance is the escape hatch you need whenever a chain step genuinely requires fresh output every time — anything involving randomness as a feature rather than a bug, or any step whose output must reflect the current moment rather than a memoized past moment.

This per-instance control is also how you compose chains with mixed caching behavior in something like LCEL (LangChain Expression Language) pipelines — each model in the chain carries its own cache configuration, so a retrieval-augmented pipeline could cache the query-rewriting step while leaving the final answer-synthesis step uncached if you want maximum freshness in the user-facing output.

What NOT to Cache: The Failure Modes

Caching sounds like a free win, but blindly caching everything creates real bugs. Here are the failure modes worth knowing before you flip caching on in production:

  • Non-deterministic or high-temperature generation. If temperature is high because you want variety — brainstorming, creative writing, generating multiple distinct options — caching defeats the purpose. The user asks for three different taglines and gets the same one three times because the second and third calls hit an identical cached first response... except caching keys typically include temperature, so this specific case is usually safe. The real danger is caching a single call meant to feel spontaneous, where a returning user gets the exact same "random" joke every time.
  • Time-sensitive or user-specific content. Anything referencing "today," "now," account balances, or per-user context should either skip caching or include the relevant context in the prompt itself so it becomes part of the cache key. If your prompt says "Current date: {date}" and you interpolate a real date string, cache keys naturally differ day to day, which is usually what you want.
  • Prompts containing secrets or PII. A cache is a data store like any other. If your prompts include user emails, tokens, or sensitive identifiers, that data now lives in your cache backend too, subject to the same retention and access-control requirements as everywhere else in your system. Audit what ends up in Redis or SQLite just as carefully as you'd audit your logs.
  • Silent staleness after prompt-template changes. If you update a system prompt or few-shot examples but the user-facing message stays the same, exact-match caching won't notice — unless the changed template text is itself part of what gets hashed into the key. In practice, most LangChain cache key implementations do include the full serialized prompt, so template changes usually bust the cache correctly, but it's worth verifying for your specific setup, especially if you're doing anything custom with partial prompts or prompt composition.
  • Testing against a warm cache. If you cache aggressively during development, you might not notice that your actual API calls are failing or misconfigured, because every test just serves cached success responses. Clear your cache — or use a fresh InMemoryCache per test run — when you're specifically testing the live API integration path.

A good habit is to log cache hit/miss rates as a metric in production, the same way you'd monitor any other cache (like a CDN or a database query cache). If your hit rate is near zero, something's off with your key generation or your traffic genuinely has no repetition and caching isn't buying you much. If it's suspiciously high on content that should vary, you may be serving stale or duplicate content where users expect freshness.

Measuring the Impact: Cost and Latency

It's worth being concrete about what caching actually saves, because "reduces cost" is vague until you measure it against your own traffic. The math is straightforward: if X% of your LLM calls are cache hits, your API spend on those calls drops to zero, and your latency for those calls drops from however long generation takes (often one to several seconds for a reasoning-heavy prompt) to single-digit milliseconds for local caches or low tens of milliseconds for a network round-trip to Redis.

A simple way to instrument this yourself is to wrap your LLM calls with timing and a counter:

import time
from langchain_core.globals import set_llm_cache
from langchain_community.cache import InMemoryCache
from langchain_openai import ChatOpenAI

cache = InMemoryCache()
set_llm_cache(cache)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

def timed_invoke(prompt: str):
    start = time.perf_counter()
    result = llm.invoke(prompt)
    elapsed = time.perf_counter() - start
    return result, elapsed

prompts = [
    "Define recursion.",
    "Define recursion.",
    "Define a linked list.",
    "Define recursion.",
]

for p in prompts:
    _, elapsed = timed_invoke(p)
    print(f"{p!r}: {elapsed*1000:.2f}ms")

Running this will show the first "Define recursion." call taking real network time, and every subsequent identical call collapsing to near-zero milliseconds. In a workload where a meaningful fraction of prompts repeat — support bots, documentation Q&A, batch document processing with overlapping content, retries after transient errors — this adds up fast, both in dollars and in perceived application speed.

It's also worth noting that caching interacts well with retry logic. If your application retries a failed call (say, after a rate-limit error) with the exact same prompt, and the first attempt actually did succeed but your code failed to read the response due to a transient network hiccup, a cache can prevent you from paying twice for work that already completed. This is a secondary but genuinely useful side effect of having caching in place.

Building a Production-Ready Caching Strategy

Pulling all of this together, a sensible default strategy for a real application looks like this:

  1. Use SQLiteCache or InMemoryCache in local development and CI so tests run fast and don't burn API quota.
  2. Use RedisCache with an explicit TTL in production, sized to match how quickly your underlying content actually changes.
  3. Set cache=False explicitly on any model instance where freshness or randomness is a functional requirement, don't rely on remembering to skip the global cache.
  4. Include genuinely variable context (dates, user IDs, session state) directly in the prompt text so it naturally becomes part of the cache key, rather than fighting the cache with workarounds.
  5. Consider semantic caching only after you've validated exact-match caching's hit rate and found it insufficient — it adds real complexity and its own cost (embedding calls) that isn't worth paying until you've proven the need.
  6. Monitor hit rate as an ongoing metric, not a one-time check, since traffic patterns and prompt templates evolve.

None of this requires exotic infrastructure. A single Redis instance, one call to set_llm_cache, and a TTL that matches your content's shelf life covers the vast majority of real-world LangChain applications. The mistake most teams make isn't picking the wrong caching backend — it's not having any caching strategy at all until the API bill forces the conversation.

Wrapping Up

Caching is the kind of optimization that's easy to postpone because nothing is visibly broken without it — your application still works, it's just slower and more expensive than it needs to be. But once you've seen a support bot serve a fifty-millisecond cached response instead of a two-second generated one, or watched a monthly API bill drop by double digits because a batch pipeline stopped reprocessing the same documents on every run, it's hard to treat caching as optional again.

Start simple: drop in InMemoryCache for development, graduate to SQLiteCache for anything that needs to survive a restart, and move to RedisCache the moment you have more than one server instance in production. Layer semantic caching on top only once you've measured that exact-match caching isn't catching enough of your real traffic patterns. And always keep an eye on what you're caching — time-sensitive, user-specific, or intentionally random outputs need explicit opt-outs, not blind trust in the default behavior.

If you want to go deeper into building production LangChain applications — covering caching, memory, agents, retrieval pipelines, and deployment patterns in a structured, hands-on format — check out the LangChain Tutorial 2026 course, where we build these systems from scratch and cover exactly the kind of real-world tradeoffs discussed here.