LangGraph Redis

This repository contains Redis implementations for LangGraph, providing both Checkpoint Savers and Stores functionality.
Overview
The project consists of three main components:
- Redis Checkpoint Savers: Implementations for storing and managing checkpoints using Redis
- Redis Stores: Redis-backed key-value stores with optional vector search capabilities
- Redis Middleware: LangChain agent middleware for semantic caching, tool caching, and conversation memory
Dependencies
Python Dependencies
The project requires the following main Python dependencies:
redis>=5.2.1
redisvl>=0.5.1
langgraph-checkpoint>=2.0.24
Redis Modules Requirements
IMPORTANT: This library requires Redis with the following modules:
- RedisJSON - For storing and manipulating JSON data
- RediSearch - For search and indexing capabilities
Redis 8.0+
If you're using Redis 8.0 or higher, both RedisJSON and RediSearch modules are included by default as part of the core
Redis distribution. No additional installation is required.
Redis < 8.0
If you're using a Redis version lower than 8.0, you'll need to ensure these modules are installed:
- Use Redis Stack, which bundles Redis with these modules
- Or install the modules separately in your Redis instance
Failure to have these modules available will result in errors during index creation and checkpoint operations.
Azure Managed Redis / Azure Cache for Redis / Redis Enterprise Configuration
If you're using Azure Managed Redis, Azure Cache for Redis (especially Enterprise tier) or Redis Enterprise, there are important configuration considerations:
Client Configuration
Azure Managed Redis, Azure Cache for Redis and Redis Enterprise use a proxy layer that makes the cluster appear as a single endpoint. This requires using a standard Redis client, not a cluster-aware client:
from redis import Redis
from langgraph.checkpoint.redis import RedisSaver
client = Redis(
host="your-cache.redis.cache.windows.net",
port=6379,
password="your-access-key",
ssl=True,
ssl_cert_reqs="required",
decode_responses=False
)
saver = RedisSaver(redis_client=client)
saver.setup()
Why This Matters
- Proxy Architecture: Azure Managed Redis, Azure Cache for Redis and Redis Enterprise use a proxy layer that handles cluster operations internally
- Automatic Detection: RedisSaver will correctly detect this as non-cluster mode when using the standard client
- No Cross-Slot Errors: The proxy handles key distribution, avoiding cross-slot errors
Azure Specific Settings
For Azure Managed Redis & Azure Cache for Redis Enterprise tier:
- Port: Use port
10000 with TLS, or 6379 for standard
- Modules: RediSearch and RedisJSON need to be selected at creation
- SSL/TLS: Always enabled, minimum TLS 1.2
Example for Azure Managed Redis, Azure Cache for Redis Enterprise:
client = Redis(
host="your-host-endpoint",
port=10000,
password="your-access-key",
ssl=True,
ssl_cert_reqs="required",
decode_responses=False
)
Installation
Install the library using pip:
pip install langgraph-checkpoint-redis
Redis Checkpoint Savers
Important Notes
[!IMPORTANT]
When using Redis checkpointers for the first time, make sure to call .setup() method on them to create required
indices. See examples below.
Standard Implementation
from langgraph.checkpoint.redis import RedisSaver
write_config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
read_config = {"configurable": {"thread_id": "1"}}
with RedisSaver.from_conn_string("redis://localhost:6379") as checkpointer:
checkpointer.setup()
checkpoint = {
"v": 1,
"ts": "2024-07-31T20:14:19.804150+00:00",
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
"channel_values": {
"my_key": "meow",
"node": "node"
},
"channel_versions": {
"__start__": 2,
"my_key": 3,
"start:node": 3,
"node": 3
},
"versions_seen": {
"__input__": {},
"__start__": {
"__start__": 1
},
"node": {
"start:node": 2
}
},
"pending_sends": [],
}
checkpointer.put(write_config, checkpoint, {}, {})
loaded_checkpoint = checkpointer.get(read_config)
checkpoints = list(checkpointer.list(read_config))
Async Implementation
from langgraph.checkpoint.redis.aio import AsyncRedisSaver
async def main():
write_config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
read_config = {"configurable": {"thread_id": "1"}}
async with AsyncRedisSaver.from_conn_string("redis://localhost:6379") as checkpointer:
await checkpointer.asetup()
checkpoint = {
"v": 1,
"ts": "2024-07-31T20:14:19.804150+00:00",
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
"channel_values": {
"my_key": "meow",
"node": "node"
},
"channel_versions": {
"__start__": 2,
"my_key": 3,
"start:node": 3,
"node": 3
},
"versions_seen": {
"__input__": {},
"__start__": {
"__start__": 1
},
"node": {
"start:node": 2
}
},
"pending_sends": [],
}
await checkpointer.aput(write_config, checkpoint, {}, {})
loaded_checkpoint = await checkpointer.aget(read_config)
checkpoints = [c async for c in checkpointer.alist(read_config)]
import asyncio
asyncio.run(main())
Shallow Implementations
Shallow Redis checkpoint savers store only the latest checkpoint in Redis. These implementations are useful when
retaining a complete checkpoint history is unnecessary.
from langgraph.checkpoint.redis.shallow import ShallowRedisSaver
write_config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
read_config = {"configurable": {"thread_id": "1"}}
with ShallowRedisSaver.from_conn_string("redis://localhost:6379") as checkpointer:
checkpointer.setup()
Redis Checkpoint TTL Support
Both Redis checkpoint savers and stores support automatic expiration using Redis TTL:
ttl_config = {
"default_ttl": 60,
"refresh_on_read": True,
}
with RedisSaver.from_conn_string("redis://localhost:6379", ttl=ttl_config) as saver:
saver.setup()
When no TTL is configured, checkpoints are persistent (never expire automatically).
Removing TTL (Pinning Threads)
You can make specific checkpoints persistent by removing their TTL. This is useful for "pinning" important threads that should never expire:
from langgraph.checkpoint.redis import RedisSaver
saver = RedisSaver.from_conn_string("redis://localhost:6379", ttl={"default_ttl": 60})
saver.setup()
config = {"configurable": {"thread_id": "important-thread", "checkpoint_ns": ""}}
saved_config = saver.put(config, checkpoint, metadata, {})
checkpoint_id = saved_config["configurable"]["checkpoint_id"]
checkpoint_key = f"checkpoint:important-thread:__empty__:{checkpoint_id}"
saver._apply_ttl_to_keys(checkpoint_key, ttl_minutes=-1)
When no TTL configuration is provided, checkpoints are persistent by default (no expiration).
This makes it easy to manage storage and ensure ephemeral data is automatically cleaned up while keeping important data persistent.
Redis Stores
Redis Stores provide a persistent key-value store with optional vector search capabilities.
Synchronous Implementation
from langgraph.store.redis import RedisStore
with RedisStore.from_conn_string("redis://localhost:6379") as store:
store.setup()
index_config = {
"dims": 1536,
"distance_type": "cosine",
"fields": ["text"],
}
ttl_config = {
"default_ttl": 60,
"refresh_on_read": True,
}
with RedisStore.from_conn_string(
"redis://localhost:6379",
index=index_config,
ttl=ttl_config
) as store:
store.setup()
Async Implementation
from langgraph.store.redis.aio import AsyncRedisStore
async def main():
ttl_config = {
"default_ttl": 60,
"refresh_on_read": True,
}
async with AsyncRedisStore.from_conn_string(
"redis://localhost:6379",
ttl=ttl_config
) as store:
await store.setup()
asyncio.run(main())
Redis Middleware for LangChain Agents
Redis middleware provides semantic caching, tool result caching, conversation memory, and semantic routing for LangChain agents. These middleware components integrate directly with langchain.agents.create_agent().
Key Features
- SemanticCacheMiddleware: Cache LLM responses by semantic similarity, reducing costs and latency
- ToolResultCacheMiddleware: Cache expensive tool executions (API calls, computations)
- ConversationMemoryMiddleware: Inject semantically relevant past messages into context
- SemanticRouterMiddleware: Route requests based on semantic matching
Quick Start
import ast
import operator as op
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langgraph.middleware.redis import (
SemanticCacheMiddleware,
SemanticCacheConfig,
ToolResultCacheMiddleware,
ToolCacheConfig,
)
SAFE_OPERATORS = {
ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,
ast.Div: op.truediv, ast.Pow: op.pow, ast.USub: op.neg,
}
def _eval_expr(node):
if isinstance(node, ast.Constant):
return node.value
elif isinstance(node, ast.BinOp) and type(node.op) in SAFE_OPERATORS:
return SAFE_OPERATORS[type(node.op)](_eval_expr(node.left), _eval_expr(node.right))
elif isinstance(node, ast.UnaryOp) and type(node.op) in SAFE_OPERATORS:
return SAFE_OPERATORS[type(node.op)](_eval_expr(node.operand))
raise ValueError(f"Unsupported expression")
def safe_eval(expr: str) -> float:
return _eval_expr(ast.parse(expr, mode='eval').body)
@tool
def calculate(expression: str) -> str:
"""Evaluate a math expression."""
return str(safe_eval(expression))
calculate.metadata = {"cacheable": True}
@tool
def get_stock_price(symbol: str) -> str:
"""Get current stock price."""
return fetch_price(symbol)
get_stock_price.metadata = {"cacheable": False}
semantic_cache = SemanticCacheMiddleware(
SemanticCacheConfig(
redis_url="redis://localhost:6379",
name="llm_cache",
distance_threshold=0.15,
ttl_seconds=3600,
deterministic_tools=["calculate"],
)
)
tool_cache = ToolResultCacheMiddleware(
ToolCacheConfig(
redis_url="redis://localhost:6379",
name="tool_cache",
ttl_seconds=1800,
)
)
agent = create_agent(
model="gpt-4o-mini",
tools=[calculate, get_stock_price],
middleware=[semantic_cache, tool_cache],
)
result = await agent.ainvoke({"messages": [HumanMessage(content="Calculate 25 * 4")]})
Tool Cacheability
The tool cache uses a priority chain (inspired by SQL function volatility and MCP ToolAnnotations) to decide whether a tool call should be cached. The checks are evaluated in order — the first match wins:
| 1 | metadata["cacheable"] is set | Use its boolean value (highest priority) |
| 2 | metadata["destructive"] == True | Never cache |
| 3 | metadata["volatile"] == True | Never cache |
| 4 | metadata["read_only"] and metadata["idempotent"] | Cache |
| 5 | Tool name matches a side-effect prefix | Never cache |
| 6 | Call args contain a volatile arg name | Never cache |
| 7 | Config whitelist / blacklist | Existing fallback |
Tool Metadata
Control cacheability per-tool using LangChain's native metadata:
@tool
def search(query: str) -> str:
"""Search the web."""
return web_search(query)
search.metadata = {"cacheable": True}
@tool
def send_email(to: str, body: str) -> str:
"""Send an email."""
return smtp_send(to, body)
send_email.metadata = {"destructive": True}
@tool
def get_exchange_rate(pair: str) -> str:
"""Get live exchange rate."""
return fetch_rate(pair)
get_exchange_rate.metadata = {"volatile": True}
@tool
def lookup_zip(code: str) -> str:
"""Look up a ZIP code."""
return zip_db.get(code)
lookup_zip.metadata = {"read_only": True, "idempotent": True}
from langchain_core.tools import StructuredTool
get_weather = StructuredTool.from_function(
func=fetch_weather,
name="get_weather",
description="Get current weather",
metadata={"cacheable": False},
)
Advanced Cache Key Configuration
ToolCacheConfig supports three opt-in fields for fine-grained cache key control. All default to None (disabled), so existing behavior is unchanged.
from langgraph.middleware.redis import (
ToolResultCacheMiddleware,
ToolCacheConfig,
DEFAULT_VOLATILE_ARG_NAMES,
DEFAULT_SIDE_EFFECT_PREFIXES,
)
tool_cache = ToolResultCacheMiddleware(
ToolCacheConfig(
redis_url="redis://localhost:6379",
name="tool_cache",
ttl_seconds=1800,
volatile_arg_names=DEFAULT_VOLATILE_ARG_NAMES,
ignored_arg_names={"request_id", "trace_id", "correlation_id"},
side_effect_prefixes=DEFAULT_SIDE_EFFECT_PREFIXES,
)
)
Volatile arg names — When a tool call's arguments contain a key matching one of these names (checked recursively at any nesting depth), the call is never cached. This prevents stale results for time-dependent queries like {"query": "weather", "timestamp": 1709827200}.
Ignored arg names — Per-request noise like request_id or trace_id inflates cache misses without affecting the tool's output. Stripping them from the cache key means two otherwise-identical calls will share a cache entry regardless of their tracking IDs.
Side-effect prefixes — Tools whose names start with mutating prefixes (e.g., send_email, delete_record, create_user) are never cached, since their results represent actions that should always execute. This can be overridden per-tool with metadata["cacheable"] = True.
Middleware Composition
Combine multiple middleware using MiddlewareStack or factory functions:
from langgraph.middleware.redis import MiddlewareStack, from_configs
stack = MiddlewareStack([
SemanticCacheMiddleware(SemanticCacheConfig(redis_url="redis://localhost:6379", name="llm_cache")),
ToolResultCacheMiddleware(ToolCacheConfig(redis_url="redis://localhost:6379", name="tool_cache")),
])
stack = from_configs(
configs=[
SemanticCacheConfig(name="llm_cache", ttl_seconds=3600),
ToolCacheConfig(name="tool_cache", ttl_seconds=1800),
],
redis_url="redis://localhost:6379",
)
agent = create_agent(model="gpt-4o-mini", tools=tools, middleware=[stack])
Connection Sharing with Checkpointer
Share Redis connections between middleware and checkpointer:
from langgraph.checkpoint.redis.aio import AsyncRedisSaver
from langgraph.middleware.redis import IntegratedRedisMiddleware
checkpointer = AsyncRedisSaver(redis_url="redis://localhost:6379")
await checkpointer.asetup()
middleware = IntegratedRedisMiddleware.from_saver(
checkpointer,
configs=[
SemanticCacheConfig(name="llm_cache"),
ToolCacheConfig(name="tool_cache"),
],
)
agent = create_agent(
model="gpt-4o-mini",
tools=tools,
checkpointer=checkpointer,
middleware=[middleware],
)
Example Notebooks
See the examples/middleware/ directory for detailed notebooks:
middleware_semantic_cache.ipynb: LLM response caching with semantic matching
middleware_tool_caching.ipynb: Tool result caching with metadata-based control
middleware_conversation_memory.ipynb: Semantic conversation history retrieval
middleware_composition.ipynb: Combining middleware with checkpointers
Examples
The examples directory contains Jupyter notebooks demonstrating the usage of Redis with LangGraph:
Checkpoint and Store Examples
persistence_redis.ipynb: Demonstrates the usage of Redis checkpoint savers with LangGraph
create-react-agent-memory.ipynb: Shows how to create an agent with persistent memory using Redis
cross-thread-persistence.ipynb: Demonstrates cross-thread persistence capabilities
persistence-functional.ipynb: Shows functional persistence patterns with Redis
Middleware Examples (examples/middleware/)
middleware_semantic_cache.ipynb: LLM response caching with semantic similarity matching
middleware_tool_caching.ipynb: Tool result caching with metadata-based cacheability control
middleware_conversation_memory.ipynb: Semantic conversation history and context injection
middleware_composition.ipynb: Combining multiple middleware with shared Redis connections
Running Example Notebooks
To run the example notebooks with Docker:
-
Navigate to the examples directory:
cd examples
-
Start the Docker containers:
docker compose up
-
Open the URL shown in the console (typically http://127.0.0.1:8888/tree) in your browser to access Jupyter.
-
When finished, stop the containers:
docker compose down
Implementation Details
Redis Module Usage
This implementation relies on specific Redis modules:
- RedisJSON: Used for storing structured JSON data as native Redis objects
- RediSearch: Used for creating and querying indices on JSON data
Indexing
The Redis implementation creates these main indices using RediSearch:
- Checkpoints Index: Stores checkpoint metadata and versioning
- Channel Values Index: Stores channel-specific data
- Writes Index: Tracks pending writes and intermediate states
For Redis Stores with vector search:
- Store Index: Main key-value store
- Vector Index: Optional vector embeddings for similarity search
TTL Implementation
Both Redis checkpoint savers and stores leverage Redis's native key expiration:
- Native Redis TTL: Uses Redis's built-in
EXPIRE command for setting TTL
- TTL Removal: Uses Redis's
PERSIST command to remove TTL (with ttl_minutes=-1)
- Automatic Cleanup: Redis automatically removes expired keys
- Configurable Default TTL: Set a default TTL for all keys in minutes
- TTL Refresh on Read: Optionally refresh TTL when keys are accessed
- Applied to All Related Keys: TTL is applied to all related keys (checkpoint, blobs, writes)
- Persistent by Default: When no TTL is configured, keys are persistent (no expiration)
Contributing
We welcome contributions! Here's how you can help:
Development Setup
Available Commands
The project includes several make commands for development:
-
Testing:
make test
make test-all
-
Linting and Formatting:
make format
make lint
make check-types
-
Code Quality:
make test-coverage
make coverage-report
make coverage-html
make find-dead-code
-
Redis for Development/Testing:
make redis-start
make redis-stop
Contribution Guidelines
- Create a new branch for your changes
- Write tests for new functionality
- Ensure all tests pass:
make test
- Format your code:
make format
- Run linting checks:
make lint
- Submit a pull request with a clear description of your changes
- Follow Conventional Commits for commit messages
License
This project is licensed under the MIT License.