Home / Articles

Move LangGraph vector state from memory to Redis persistence

Swap LangGraph vector state from memory to Redis with a factory for get_vector_store, session metadata on ingest, filtered retrieval and delete-by-session.

Publisher: REACTAPP.TOP13 min read3102 words

This article is published in English.

Sources: From In-Memory to Redis: Vector Persistence

In-memory vector stores are the simplest correct design only while one process owns the whole conversation. Real backends break that assumption: new CLI runs, new HTTP workers, restarts, and multi-instance deploys. Persistence means moving the same LangGraph nodes onto Redis (or another durable vector index) without rewriting the product logic.

1. get_vector_store() — the one function that had to change

Abstract construction behind a factory. In-memory for tests; Redis URL + index name for deployed environments.

_SESSION_STORES: Dict[str, InMemoryVectorStore] = {}

def get_vector_store(session_id: str) -> InMemoryVectorStore:
    if session_id not in _SESSION_STORES:
        _SESSION_STORES[session_id] = InMemoryVectorStore(embeddings)
    return _SESSION_STORES[session_id]
def get_vector_store(session_id: str) -> RedisVectorStore:
    redis_config = RedisConfig(
        index_name=f"session:{session_id}",
        redis_url=config.REDIS_URL,
        embedding_dimensions=config.EMBEDDING_DIMENSIONS,
        metadata_schema=[
            {"name": "session_id", "type": "tag"},
            {"name": "file_name", "type": "tag"},
        ],
    )
    return RedisVectorStore(embeddings, config=redis_config, ttl=config.SESSION_TTL_SECONDS)
docs = [
    Document(
        page_content=c["text"],
        metadata={
            "session_id": state["session_id"],  # <-- added back
            "file_name": c["file_name"],
            "chunk_index": c["chunk_index"],
        },
    )
    for c in chunks
]
state["retrieved"] = vs.similarity_search(
    state["query"],
    k=config.TOP_K,
    filter=Tag("session_id") == state["session_id"],
)
def clear_session(session_id: str) -> None:
    get_vector_store(session_id).index.delete(drop=True)
uv add langgraph langchain-core langchain-openai langchain-text-splitters \
       azure-ai-documentintelligence azure-core \
       langchain-redis redisvl \
       python-dotenv
REDIS_PASSWORD=changeme docker compose up -d
[+] Running 3/3
 ✔ Network redismanualvectorization_default      Created                                                                                       0.0s
 ✔ Volume "redismanualvectorization_redis-data"  Created                                                                                       0.0s
 ✔ Container usb-redis-vector-store              Started
AZURE_OPENAI_ENDPOINT=https://aif-cus-aisearchegs-dev-01.cognitiveservices.azure.com/
AZURE_OPENAI_API_KEY=<Redacted>
# AZURE_OPENAI_API_VERSION = os.environ.get("AZURE_OPENAI_API_VERSION", "2024-10-21")
AZURE_OPENAI_EMBEDDING_DEPLOYMENT=text-embedding-3-small
AZURE_OPENAI_CHAT_DEPLOYMENT="gpt-5-mini"
# --- Azure Document Intelligence (used for all file text extraction) ---
DOCUMENTINTELLIGENCE_ENDPOINT=https://adi-cus-aisearchegs-dev-01.cognitiveservices.azure.com/
DOCUMENTINTELLIGENCE_KEY=<Redacted>
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=changeme
SESSION_TTL_SECONDS=14400
uv run python -m src.redismanualvectorization.main \
  --session demo1 \
  --files ./source/sample_bank_statement.pdf ./source/sample_loan_agreement.pdf
Ingested 2 file(s) into session 'demo1' (persisted in Redis).
docker exec -it usb-redis-vector-store redis-cli -a changeme
FT._LIST
# Execution results
1) session:demo2
2) session:demo1
FT.INFO session:demo1

# Execution results
1) index_name
 2) session:demo1
 3) index_options
 4) (empty array)
 5) index_definition
 6) 1) key_type
    2) HASH
    3) prefixes
    4) 1) session:demo1:
    5) default_score
    6) "1"
 7) attributes
 8) 1) 1) identifier
       2) text
       3) attribute
       4) text
       5) type
       6) TEXT
       7) WEIGHT
       8) "1"
    2)  1) identifier
        2) embedding
        3) attribute
        4) embedding
        5) type
        6) VECTOR
        7) algorithm
        8) FLAT
        9) data_type
       10) FLOAT32
       11) dim
       12) (integer) 1536
       13) distance_metric
       14) COSINE
    3) 1) identifier
       2) _index_name
       3) attribute
       4) _index_name
       5) type
       6) TEXT
       7) WEIGHT
       8) "1"
    4) 1) identifier
       2) _metadata_json
       3) attribute
       4) _metadata_json
       5) type
       6) TEXT
       7) WEIGHT
       8) "1"
    5) 1) identifier
       2) session_id
       3) attribute
       4) session_id
       5) type
       6) TAG
       7) SEPARATOR
       8) |
    6) 1) identifier
       2) file_name
       3) attribute
       4) file_name
       5) type
       6) TAG
       7) SEPARATOR
       8) |
 9) num_docs
10) (integer) 24
11) max_doc_id
12) (integer) 24
13) num_terms
14) (integer) 850
15) num_records
16) (integer) 1921
17) inverted_sz_mb
18) "0.09056949615478516"
19) vector_index_sz_mb
20) "6.0091552734375"
21) total_inverted_index_blocks
22) (integer) 853
23) offset_vectors_sz_mb
24) "0.0028848648071289063"
25) doc_table_size_mb
26) "0.017791748046875"
27) sortable_values_size_mb
28) "0"
29) key_table_size_mb
30) "7.74383544921875e-4"
31) tag_overhead_sz_mb
32) "1.10626220703125e-4"
33) text_overhead_sz_mb
34) "0.028371810913085938"
35) total_index_memory_sz_mb
36) "0.14574527740478516"
37) geoshapes_sz_mb
38) "0"
39) records_per_doc_avg
40) "80.04166412353516"
41) bytes_per_record_avg
42) "49.43727111816406"
43) offsets_per_term_avg
44) "1.5065070390701294"
45) offset_bits_per_record_avg
46) "8.362128257751465"
47) hash_indexing_failures
48) (integer) 0
49) total_indexing_time
50) "11.055999755859375"
51) indexing
52) (integer) 0
53) percent_indexed
54) "1"
55) number_of_uses
56) (integer) 3
57) cleaning
58) (integer) 0
59) gc_stats
60)  1) bytes_collected
     2) "0"
     3) total_ms_run
     4) "0"
     5) total_cycles
     6) "0"
     7) average_cycle_time_ms
     8) "nan"
     9) last_run_time_ms
    10) "0"
    11) gc_numeric_trees_missed
    12) "0"
    13) gc_blocks_denied
    14) "0"
61) cursor_stats
62) 1) global_idle
    2) (integer) 0
    3) global_total
    4) (integer) 0
    5) index_capacity
    6) (integer) 128
    7) index_total
    8) (integer) 0
63) dialect_stats
64) 1) dialect_1
    2) (integer) 0
    3) dialect_2
    4) (integer) 1
    5) dialect_3
    6) (integer) 0
    7) dialect_4
    8) (integer) 0
65) Index Errors
66) 1) indexing failures
    2) (integer) 0
    3) last indexing error
    4) N/A
    5) last indexing error key
    6) "N/A"
    7) background indexing status
    8) OK
67) field statistics
68) 1) 1) identifier
       2) text
       3) attribute
       4) text
       5) Index Errors
       6) 1) indexing failures
          2) (integer) 0
          3) last indexing error
          4) N/A
          5) last indexing error key
          6) "N/A"
    2) 1) identifier
       2) embedding
       3) attribute
       4) embedding
       5) Index Errors
       6) 1) indexing failures
          2) (integer) 0
          3) last indexing error
          4) N/A
          5) last indexing error key
          6) "N/A"
    3) 1) identifier
       2) _index_name
       3) attribute
       4) _index_name
       5) Index Errors
       6) 1) indexing failures
          2) (integer) 0
          3) last indexing error
          4) N/A
          5) last indexing error key
          6) "N/A"
    4) 1) identifier
       2) _metadata_json
       3) attribute
       4) _metadata_json
       5) Index Errors
       6) 1) indexing failures
          2) (integer) 0
          3) last indexing error
          4) N/A
          5) last indexing error key
          6) "N/A"
    5) 1) identifier
       2) session_id
       3) attribute
       4) session_id
       5) Index Errors
       6) 1) indexing failures
          2) (integer) 0
          3) last indexing error
          4) N/A
          5) last indexing error key
          6) "N/A"
    6) 1) identifier
       2) file_name
       3) attribute
       4) file_name
       5) Index Errors
       6) 1) indexing failures
          2) (integer) 0
          3) last indexing error
          4) N/A
          5) last indexing error key
          6) "N/A"
num_docs            24
vector_index_sz_mb   6.0091552734375
24 × 1536 × 4 bytes = 147,456 bytes ≈ 0.14 MB
FT.SEARCH session:demo1 "*" LIMIT 0 5 RETURN 3 file_name session_id text
# Execution Results
1) (integer) 24
 2) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G2V"
 3) 1) "file_name"
    2) "sample_bank_statement.pdf"
    3) "session_id"
    4) "demo1"
    5) "text"
    6) "##### Monthly Maintenance Fee\n\nA $12.00 monthly maintenance fee applies unless one of the following waiver conditions is met during the statement period: (a) a minimum daily balance of\n$1,500 is maintained, (b) total qualifying direct deposits of $500 or more are received, or (c) the account holder is enrolled in a linked student or senior\naccount program. Fee waiver eligibility is evaluated at the end of each statement cycle based on the criteria above.\n\n\n##### Error Resolution & Your Rights"
 4) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G31"
 5) 1) "file_name"
    2) "sample_loan_agreement.pdf"
    3) "session_id"
    4) "demo1"
    5) "text"
    6) "\"Scheduled Payment Date\" means the 15th calendar day of each month during the Loan Term, or the next Business Day\nif the 15th falls on a non-Business Day.\n\n<!-- PageBreak -->\n\n\n# SAMPLE AGREEMENT - SYNTHETIC TEST DATA - NOT A REAL LOAN DOCUMENT\n\n\n## 4. Amortization Schedule (First 12 Payments)"
 6) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G34"
 7) 1) "file_name"
    2) "sample_loan_agreement.pdf"
    3) "session_id"
    4) "demo1"
    5) "text"
    6) "A complete 48-month amortization schedule is available from the Lender upon request. Figures above are rounded to the\nnearest cent and may differ slightly from actual billed amounts due to daily interest accrual conventions.\n\n\n## 5. Prepayment\n\nBorrower may prepay all or part of the outstanding Principal Amount at any time without penalty. Partial prepayments will be\napplied first to accrued interest, then to outstanding fees, then to Principal, and will not change the Scheduled Payment\nDate or Monthly Payment amount unless the Borrower requests and the Lender approves a loan re-amortization in writing.\n\n\n### 6. Late Payments and Default"
 8) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G2T"
 9) 1) "file_name"
    2) "sample_bank_statement.pdf"
    3) "session_id"
    4) "demo1"
    5) "text"
    6) "##### Interest Rate & Annual Percentage Yield\n\nThis account earns interest at a variable rate, currently 0.15% with an Annual Percentage Yield (APY) of 0.15%, applied to the average daily balance.\nInterest is compounded monthly and credited to your account on the last business day of the statement period, shown in the Transaction History as 'Interest\nPayment'. Rates are subject to change without notice at the bank's discretion and are current as of the statement date shown on page one.\n\n\n##### Monthly Maintenance Fee"
10) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G2W"
11) 1) "file_name"
    2) "sample_bank_statement.pdf"
    3) "session_id"
    4) "demo1"
    5) "text"
    6) "##### Error Resolution & Your Rights\n\nIn case of errors or questions about electronic transfers, telephone the bank at the number on your card or write to the address on file as soon as possible.\nThe bank must hear from you no later than 60 days after the first statement on which the problem appears. You will need to provide your name and account\nnumber, describe the error, and state the dollar amount of the suspected error. The bank will investigate and report results within 10 business days, or\nwithin 45 days for transactions initiated outside a 30-day period after the first deposit to the account.\n\n\n##### Funds Availability"
127.0.0.1:6379> SCAN 0 MATCH session:demo1:*
1) "22"
2)  1) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G2S"
    2) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G2X"
    3) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G2R"
    4) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G33"
    5) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G2V"
    6) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G30"
    7) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G2Z"
    8) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G36"
    9) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G2P"
   10) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G2Y"
1) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G2V"
   file_name    sample_bank_statement.pdf
   session_id   demo1
   text         "##### Monthly Maintenance Fee
A $12.00 monthly maintenance fee applies unless one of the
                 following waiver conditions is met during the statement
                 period: (a) a minimum daily balance of $1,500 is
                 maintained..."
2) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G31"
   file_name    sample_loan_agreement.pdf
   session_id   demo1
   text         "\"Scheduled Payment Date\" means the 15th calendar day of
                 each month during the Loan Term...
<!-- PageBreak -->
                 # SAMPLE AGREEMENT - SYNTHETIC TEST DATA - NOT A REAL LOAN
                 DOCUMENT"
SCAN 0 MATCH session:demo1:*
# Execution Results
1) "22"
2) 1) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G2S"
   2) "session:demo1:01M1MN5JG8DDCXP7K2XHP54G2X"
   ... (8 more)
import redis
from src.redismanualvectorization.graph import get_vector_store

session_id = "demo1"
vs = get_vector_store(session_id)
r = redis.Redis(host="localhost", port=6379, password="changeme")
keys = r.keys(f"session:{session_id}:*")
print(f"{len(keys)} chunk(s) in this session")
raw = r.hgetall(keys[0])
vector = vs.convert_vector({"embedding": raw[b"embedding"]})
print("Vector length:", len(vector))          # expect 1536
print("First 8 dims:", [round(v, 5) for v in vector[:8]])
print("Chunk text:", raw[b"text"][:120])

# Results for execution
uv run python -m src.redismanualvectorization.raw_vector_check
24 chunk(s) in this session
Vector length: 1536
First 8 dims: [-0.01327, -0.06799, 0.00346, 0.01323, -0.01469, 0.0217, -0.01146, 0.05652]
Chunk text: b'<td>Lumen Electric Utility</td>\n<td>Bill Payment</td>\n<td>-$24.03</td>\n<td>$7,265.31</td>\n</tr>\n<tr>\n<td>02/14/2026</td>'
krishnansriram@mac RedisManualVectorization % uv run python -m src.redismanualvectorization.main \
  --session demo1 \
  --query "What's the overdraft protection fee?"

Q: What's the overdraft protection fee?

A: The overdraft protection transfer fee is $10.00 per occurrence.

Sources:
  - sample_bank_statement.pdf (chunk 7): #### Important Account Disclosures   ##### Overdraft Protection  If a transaction is presented that ...
  - sample_bank_statement.pdf (chunk 9): ##### Monthly Maintenance Fee  A $12.00 monthly maintenance fee applies unless one of the following ...
  - sample_bank_statement.pdf (chunk 8): ##### Interest Rate & Annual Percentage Yield  This account earns interest at a variable rate, curre...
  - sample_bank_statement.pdf (chunk 3): <td>-$10.44</td> <td>$8,096.52</td> </tr> <tr> <td>02/07/2026</td> <td>Greenfield Grocers</td> <td>D...
krishnansriram@mac RedisManualVectorization % uv run python -m src.redismanualvectorization.main \
  --session demo2 \
  --query "What's the overdraft protection fee?"

Q: What's the overdraft protection fee?

A: I don't know — no relevant content about an overdraft protection fee was found in the uploaded files.

2. ingest_node — session_id rejoins metadata

Every chunk must carry the session (or tenant) key used later for filters. Losing metadata at ingest is how leaks happen.

3. retrieve_node — explicit filter plus index discipline

Filter by session even if the index name is per-session. Belt and suspenders beat optimistic isolation.

5. Dependencies — direct imports get direct entries

Pin Redis client and vector integration versions. Transitive upgrades have broken index field types before.

Inspecting what you stored

1. What indexes exist

List indexes after deploy; failed creates are silent until the first empty retrieve.

2. Schema

Confirm vector dimension, text fields, and tag fields match the code.

3. List records without dumping raw vectors

4. Full metadata for a record

Actually see the vector — decode floats when debugging dimension mismatches

Closing notes

Limitations even with managed Redis

You still need backups, auth, memory caps, and a delete-by-session story. Persistence fixes restarts; it does not invent governance.

Treat the in-memory path as a teaching wheel and Redis as the default for any shared or multi-request deployment of the same graph.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Add a smoke test that ingests two sessions, retrieves from each, and asserts zero cross-talk. Run it against Redis in CI with a disposable container so filter bugs never reach shared environments.

Persistence also changes failure modes: a partial ingest can leave orphans. Make upserts idempotent on chunk ids and provide a reindex job that rebuilds a session from source documents.

For local development, keep the in-memory factory behind an env flag so engineers are not forced to run Redis to edit a prompt. For shared staging, forbid the in-memory path entirely.

Measure p95 retrieve latency before and after the Redis move. Persistence that is correct but three times slower will push people back to unsafe shortcuts.

Session teardown must delete-by-filter, not flush the database. Operational runbooks should include the exact CLI/FT.DROP or delete-query commands with examples.

When multiple app instances write concurrently, choose an index design that tolerates concurrent upserts and document Redis Cluster hash-tag constraints if you shard.

Log the index name and session filter with every retrieve for support. Empty results are often wrong-index or wrong-filter, not “the model failed.”

Persistence also changes failure modes: a partial ingest can leave orphans. Make upserts idempotent on chunk ids and provide a reindex job that rebuilds a session from source documents.

For local development, keep the in-memory factory behind an env flag so engineers are not forced to run Redis to edit a prompt. For shared staging, forbid the in-memory path entirely.

Measure p95 retrieve latency before and after the Redis move. Persistence that is correct but three times slower will push people back to unsafe shortcuts.

Session teardown must delete-by-filter, not flush the database. Operational runbooks should include the exact CLI/FT.DROP or delete-query commands with examples.

When multiple app instances write concurrently, choose an index design that tolerates concurrent upserts and document Redis Cluster hash-tag constraints if you shard.

Log the index name and session filter with every retrieve for support. Empty results are often wrong-index or wrong-filter, not “the model failed.”

Persistence also changes failure modes: a partial ingest can leave orphans. Make upserts idempotent on chunk ids and provide a reindex job that rebuilds a session from source documents.

For local development, keep the in-memory factory behind an env flag so engineers are not forced to run Redis to edit a prompt. For shared staging, forbid the in-memory path entirely.

Measure p95 retrieve latency before and after the Redis move. Persistence that is correct but three times slower will push people back to unsafe shortcuts.

Session teardown must delete-by-filter, not flush the database. Operational runbooks should include the exact CLI/FT.DROP or delete-query commands with examples.

When multiple app instances write concurrently, choose an index design that tolerates concurrent upserts and document Redis Cluster hash-tag constraints if you shard.

Log the index name and session filter with every retrieve for support. Empty results are often wrong-index or wrong-filter, not “the model failed.”

Persistence also changes failure modes: a partial ingest can leave orphans. Make upserts idempotent on chunk ids and provide a reindex job that rebuilds a session from source documents.