07

The Production Data Plane

Choose one durable source of truth, add specialized infrastructure only after measurement, and design every cache around correctness.

Start with explicit production boundaries

Application containers should be disposable. Graph checkpoints, long-term memories, audit records, and idempotency claims must survive restarts outside the pod or process. Expensive background work belongs on a queue. Attachments belong in object storage. Caches accelerate reads but never become the only copy of truth.

This topology scales request workers independently from memory workers. A model-provider slowdown does not erase state, and a failed extraction job can retry without replaying an email action.

                     ┌─────────────────────────────┐
email provider ────▶ │ API + LANGGRAPH WORKERS     │
                     └───┬──────────┬──────────┬───┘
                         │          │          │
                         ▼          ▼          ▼
                    POSTGRES     REDIS      OBJECT STORE
                   + pgvector   / VALKEY    raw content
                         │
                         ▼ events
                    DURABLE QUEUE ─────▶ MEMORY WORKERS

Make PostgreSQL authoritative

For most new systems, PostgreSQL plus pgvector is the best default. Relational tables hold tenants, users, checkpoints, memory metadata, versions, permissions, audit links, and deletion state. The vector extension keeps embeddings close to those transactional records and supports exact or approximate nearest-neighbor search.

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE memories (
  tenant_id uuid NOT NULL,
  user_id uuid NOT NULL,
  memory_id uuid PRIMARY KEY,
  memory_type text NOT NULL,
  content text NOT NULL,
  embedding vector(1536),
  embedding_model text NOT NULL,
  status text NOT NULL DEFAULT 'active',
  confidence real,
  source_ref text NOT NULL,
  valid_from timestamptz NOT NULL DEFAULT now(),
  valid_to timestamptz,
  metadata jsonb NOT NULL DEFAULT '{}'
);

CREATE INDEX memories_hnsw
ON memories USING hnsw (embedding vector_cosine_ops);

Enforce tenant and user predicates in every query and, where practical, with row-level security. Vector similarity is applied only inside the authorized candidate set. Back up the database and prove restores; an untested backup is merely a hope.

Add Qdrant only after a benchmark says why

There is no universal vector-count threshold at which pgvector suddenly fails. Data shape, filter selectivity, write rate, latency target, hardware, index tuning, and operational skill all matter. Stay with pgvector while it meets measured service levels and keeps the system simpler.

Add Qdrant—or another dedicated vector engine—when profiling shows that vector search needs independent sharding, replication, tuning, or scaling. Keep PostgreSQL as metadata authority and treat the vector service as a derived retrieval index. Use an outbox or versioned indexing job rather than fragile ad-hoc dual writes.

OptionBest fitStrengthMain cost
PostgreSQL + pgvectorDefault and metadata-heavy retrievalTransactions, joins, one backup pathANN tuning competes with database workload
QdrantIndependently scaled vector retrievalPurpose-built filtering, sharding, replicationA second distributed data system
Managed vector databaseSmall platform team prioritizing operationsVendor-managed scalingCost, portability, governance trade-offs
OpenSearchLexical + vector document discoveryBM25, filters, aggregations, vectorsOperational weight; weak canonical store

The migration trigger should be a failed benchmark tied to a service-level objective, not an impressive round number.

Use Redis or Valkey for ephemeral acceleration

Redis or Valkey is useful for rate limits, distributed locks, idempotency claims, hot profiles, OAuth-token caches, embedding caches, and short-lived retrieval results. It is not the authoritative memory store: eviction, flushes, or regional failover must not erase durable user knowledge.

Cache-aside is easier to reason about than write-behind. Read the cache, fall back to the database, and populate a short-lived entry. On memory supersession or procedure activation, advance a namespace epoch or invalidate affected keys.

cache_key = stable_hash({
    "tenant": tenant_id,
    "user": user_id,
    "query": normalized_query,
    "memory_epoch": memory_epoch,
    "embedding_model": embedding_model,
    "policy_version": policy_version,
})

if cached := redis.get(cache_key):
    return deserialize(cached)

results = search_authoritative_memory()
redis.setex(cache_key, 120, serialize(results))
return results

Never cache a side-effect decision such as “send,” “schedule,” or “delete” and replay it without fresh authorization. Cache latency, not authority.

Choose queue and object storage by workload

A managed cloud queue is a strong first choice for extraction, embedding, consolidation, and re-indexing. It provides retries, visibility timeouts, and dead-letter queues without operating a streaming platform. Choose Kafka when sustained throughput, ordered partitions, long replay, and multiple independent consumers justify its complexity.

Store raw MIME messages, attachments, large evaluation artifacts, and redacted source snapshots in S3, GCS, or an equivalent object store. Databases hold references and metadata. Use scoped service identities, encryption, retention policy, and lifecycle deletion.

Version embeddings and rebuild safely

Record the embedding provider, model, dimension, normalization, and content hash on every vector. A migration writes new embeddings into a parallel column or collection, measures retrieval quality against the labelled set, switches reads by version, and retires the old index only after rollback is no longer needed.

Do not re-embed in place without an audit trail. Mixing incompatible embeddings in one search silently corrupts ranking even when every individual write succeeds.

Design for degradation

If semantic retrieval times out, the agent may continue with current-thread context for a low-risk draft, clearly marking that durable preferences were unavailable. It should not perform a high-impact action whose safety depends on memory. If Redis is down, fall back to PostgreSQL with rate protection. If the queue is down, the visible response may succeed while an outbox retains the learning event for later publication.

Define these modes before incidents. “Memory unavailable” is an observable state, not an exception that prompts the model to invent missing context.

Key takeaways

  • Keep graph state and long-term memory outside disposable application containers.
  • Start with PostgreSQL plus pgvector as the authoritative store and benchmark before splitting vectors out.
  • Use Qdrant when measured vector pressure justifies independent infrastructure.
  • Redis or Valkey accelerates idempotency, profiles, and retrieval; it is not durable memory authority.
  • Queues decouple background work, object storage holds large artifacts, and embedding versions make migration safe.

Checklist

  • [ ] I can assign checkpoints, memories, caches, jobs, and attachments to the correct stores.
  • [ ] I can explain why vector ranking happens after tenant filtering.
  • [ ] I can define a measured trigger for adding Qdrant.
  • [ ] I can construct a cache key that changes with memory and policy versions.
  • [ ] I can describe a parallel-index embedding migration and rollback.