AI Agents

Agent Memory Persistence: Vector, SQL, or Graph?

Choose the right agent memory systems for your AI agents — vector, SQL, and graph each win in different scenarios. Learn when structured memory beats embeddings.

By Laxaar Engineering Team Aug 22, 2026 11 min read
Agent Memory Persistence: Vector, SQL, or Graph?

Most teams building agents reach for a vector store first. It's the default in almost every tutorial, the first integration listed in LangChain and LlamaIndex docs, and it feels right: you embed memories, retrieve the semantically closest ones, inject them into context. The agent "remembers." The demo looks impressive.

Then you hit production. Your agent needs to know whether a user has already completed an onboarding step. It needs to recall every decision made about a specific contract, in order. It needs to traverse a relationship: "who reported the issue that caused this incident?" None of these questions are similarity searches. They're lookups, aggregations, and graph traversals. A vector store will either fail them outright or return stale, approximate answers that look correct until they're silently wrong.

Agent memory persistence is a backend selection problem, not a retrieval problem. The store you pick should match the structure of your memory, not the marketing material of the tool. At Laxaar we've built agent memory layers across all three paradigms, and we have a clear point of view on when each one earns its place.

What you'll learn

The three memory types agents actually need

Before picking a backend, it's worth naming what agent memory actually contains. Three categories come up repeatedly across production systems:

Episodic memory. The record of past events: what the agent did, what the user said, what happened in prior sessions. "We discussed budget constraints in our last conversation." This is narrative, time-ordered, and often fuzzy to retrieve. You want the relevant past, not the complete past.

Semantic memory. Factual knowledge: product specs, user preferences, domain rules, reference data. "This customer is on the Enterprise plan." Lookups here are often exact. You're not asking "what is approximately true about this customer?" You're asking "what IS true."

Relational memory. The network of connections between entities: who owns what, which events caused which outcomes, how entities relate to each other. "Show me all decisions that affected this project, made by people in this team." Traversals over a graph.

The mistake is defaulting all three to the same backend. Vector stores handle fuzzy episodic retrieval reasonably well. They handle exact semantic lookups poorly and relational traversals not at all.

How vector memory works and where it wins

Vector memory is a storage pattern where memories are encoded as high-dimensional embeddings and retrieved by approximate nearest-neighbor search against a query embedding. The premise: semantically similar things will cluster in the embedding space, so "remind me of conversations about pricing" will surface sessions that discussed cost, budget, rates, and fees, even if none used the word "pricing."

import openai
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
import uuid

client = QdrantClient(":memory:")

# Create a collection for episodic agent memory
client.create_collection(
    collection_name="agent_episodes",
    vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)

def embed(text: str) -> list[float]:
    resp = openai.embeddings.create(model="text-embedding-3-small", input=text)
    return resp.data[0].embedding

def store_episode(session_id: str, content: str, metadata: dict):
    vector = embed(content)
    client.upsert(
        collection_name="agent_episodes",
        points=[PointStruct(
            id=str(uuid.uuid4()),
            vector=vector,
            payload={"session_id": session_id, "content": content, **metadata},
        )],
    )

def recall(query: str, top_k: int = 5) -> list[dict]:
    results = client.search(
        collection_name="agent_episodes",
        query_vector=embed(query),
        limit=top_k,
    )
    return [r.payload for r in results]

Vector memory earns its place in episodic retrieval with open-ended queries, long-term conversational context, and document-grounded knowledge bases. It's the right default when you can't enumerate what the agent will need to recall. The fuzzy match is the point.

The real trade-off: approximate retrieval means you'll occasionally surface the wrong memory and miss a relevant one. In a customer support agent, that's a minor quality degradation. In a financial compliance agent that needs to recall every documented decision, it's a reliability failure.

When SQL is the right agent memory backend

SQL memory is a pattern where agent memories are stored as structured rows in a relational database, retrieved with deterministic queries. No embeddings. No approximate search. Exact answers.

The obvious objection is that SQL "can't handle unstructured memory." That's true if your memory is genuinely unstructured. But most production agent memory is far more structured than teams admit at the start.

User state is structured. Session history has a schema. Preferences, settings, and decisions can all be modeled as rows. The resistance to SQL usually comes from conflating "memory has prose in it" with "memory has no structure." A user preference stored as { user_id, preference_key, value, updated_at } is still relational data, even if value is a text field.

import sqlite3
from datetime import datetime

conn = sqlite3.connect("agent_memory.db")
conn.execute("""
    CREATE TABLE IF NOT EXISTS memories (
        id          INTEGER PRIMARY KEY AUTOINCREMENT,
        agent_id    TEXT NOT NULL,
        user_id     TEXT NOT NULL,
        memory_type TEXT NOT NULL,  -- 'preference', 'fact', 'decision'
        key         TEXT NOT NULL,
        value       TEXT NOT NULL,
        confidence  REAL DEFAULT 1.0,
        created_at  TEXT NOT NULL,
        updated_at  TEXT NOT NULL
    )
""")
conn.execute("CREATE INDEX IF NOT EXISTS idx_user_key ON memories(user_id, key)")
conn.commit()

def remember(agent_id: str, user_id: str, memory_type: str, key: str, value: str):
    now = datetime.utcnow().isoformat()
    conn.execute("""
        INSERT INTO memories (agent_id, user_id, memory_type, key, value, created_at, updated_at)
        VALUES (?, ?, ?, ?, ?, ?, ?)
        ON CONFLICT(user_id, key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at
    """, (agent_id, user_id, memory_type, key, value, now, now))
    conn.commit()

def recall_exact(user_id: str, key: str) -> str | None:
    row = conn.execute(
        "SELECT value FROM memories WHERE user_id = ? AND key = ? ORDER BY updated_at DESC LIMIT 1",
        (user_id, key)
    ).fetchone()
    return row[0] if row else None

SQL memory is the right choice when the agent needs exact answers, when memory has a defined schema, when you need aggregations ("how many times has this user asked about refunds?"), or when audit trails matter. It's also dramatically cheaper to operate than a vector store at moderate scale: no embedding inference cost, no ANN index, just a query.

The trade-off: SQL can't answer "what did we discuss that was related to the user's frustration last week?" That's a semantic question and SQL won't help you. SQL wins on determinism; it loses on fuzziness.

When graph databases earn their complexity

Graph memory stores entities as nodes and relationships as edges, queried with path traversal rather than similarity search or row lookup. It earns its complexity when memory has inherent network structure that flat tables can't represent cleanly.

Consider an AI support agent managing a complex software product. The agent needs to know: "Has a similar issue been reported before? If so, what was the resolution path? Were any dependent services affected? Who was involved?" This is a traversal question. A vector store will surface semantically similar issues but can't follow the resolution chain. A SQL join across five tables works but becomes increasingly brittle as the relationship depth grows.

from neo4j import GraphDatabase

driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))

def store_incident(issue_id: str, description: str, reporter_id: str, affected_services: list[str]):
    with driver.session() as session:
        session.run("""
            MERGE (i:Issue {id: $issue_id})
            SET i.description = $description
            MERGE (u:User {id: $reporter_id})
            MERGE (u)-[:REPORTED]->(i)
        """, issue_id=issue_id, description=description, reporter_id=reporter_id)

        for svc in affected_services:
            session.run("""
                MERGE (s:Service {name: $svc})
                MERGE (i:Issue {id: $issue_id})
                MERGE (i)-[:AFFECTS]->(s)
            """, svc=svc, issue_id=issue_id)

def find_related_issues(service_name: str) -> list[dict]:
    with driver.session() as session:
        result = session.run("""
            MATCH (i:Issue)-[:AFFECTS]->(s:Service {name: $service_name})
            OPTIONAL MATCH (u:User)-[:REPORTED]->(i)
            RETURN i.id AS issue_id, i.description AS description, u.id AS reporter
            ORDER BY i.id DESC LIMIT 10
        """, service_name=service_name)
        return [dict(r) for r in result]

Graph memory's real strength is relationship-dense domains: knowledge management, organizational memory, dependency tracking, causal chains. If you find yourself writing multi-hop SQL joins to answer questions the agent needs to ask frequently, that's the signal to reach for a graph backend.

The trade-off: graph databases are operationally heavier than Postgres, the query language (Cypher for Neo4j, GQL broadly) adds a learning curve, and cloud-managed options have historically been pricier than managed Postgres. Don't introduce a graph store until you've confirmed the query patterns actually need it.

Head-to-head comparison across key dimensions

DimensionVector StoreSQL (Relational)Graph Database
Query typeSemantic similarityExact lookup, aggregationPath traversal, relationship depth
Memory structure fitUnstructured / narrativeStructured / tabularNetworked entities and relationships
Retrieval determinismApproximateExactExact (for defined paths)
Operational complexityMediumLowHigh
Cost at scaleHigher (embedding inference + ANN)LowerMedium to high
Audit / complianceWeakStrongStrong
Schema flexibilityHighLow to mediumMedium
Best memory typeEpisodic, document-groundedSemantic facts, user stateRelational, causal, organizational

The right read here isn't "pick the one with the most green cells." It's "which row matches your actual memory access patterns?"

Combining backends in a layered memory architecture

Production agents with non-trivial memory needs rarely survive on a single backend. The pattern that works is a tiered memory layer where each backend handles the queries it's actually good at.

A practical layered design:

  • Working memory: in-context (the current conversation window, managed by the agent loop itself — no external store needed).
  • Episodic memory: vector store for fuzzy recall of past sessions, long-tail user history, and document-grounded facts.
  • Semantic / state memory: SQL for user preferences, profile facts, onboarding status, account state. Anything that needs an exact, up-to-date answer.
  • Relational memory: graph database for entity relationships, dependency maps, and organizational knowledge. Only bring it in when the domain actually requires it.

The agent's retrieval layer then routes queries to the appropriate backend. This is less glamorous than a single "memory module," but it's how you avoid the failure mode where a vector store returns an approximate answer when the agent needed the exact one.

At Laxaar, when we build AI agents for production workloads, we start with SQL for state and add a vector store only after we've confirmed that the episodic recall problem is real and that semantic similarity is actually a better retrieval signal than a keyed lookup.

Common mistakes teams make with agent memory

Storing everything in a vector store and calling it done. Embeddings are a retrieval mechanism, not a storage format. You're trading query accuracy for query flexibility. That trade-off is only worth it for the queries that genuinely need semantic search.

No memory expiry or relevance decay. An agent that remembers every session equally has increasingly noisy context as history grows. Old memories crowd out recent ones. Production systems need a recency signal: timestamps used in ranking, a confidence score that decays over time, or explicit TTL-based expiry for low-value episodic memories.

Skipping a write schema for the vector store. Because vector stores accept arbitrary text, teams write raw conversation turns, summaries, tool outputs, and user messages all into the same collection with inconsistent structure. At retrieval time, the results are a random mix of memory types that the agent can't usefully differentiate. Standardize a memory record format (even a simple { type, content, metadata }) before you start writing.

Treating graph as a default when SQL would do. Graph databases are a compelling idea and genuinely powerful for relationship-heavy domains. But most "I need to track relationships" problems at the start of a project are two or three join tables in Postgres. Reach for a graph store after Postgres starts fighting you, not before.

For teams exploring AI automation services and custom agent architectures, the memory layer is usually where the production gap lives. Not in the model or the agent loop, but in the backend that stores and retrieves context.

Frequently Asked Questions

Can I start with a vector store and migrate to SQL or graph later?

Yes, but plan for it. If your agent writes memories in a structured format from day one (even if you embed and store them in a vector collection), you can re-ingest the structured data into SQL or a graph store later without re-building the memory writing logic. The migration cost is in re-indexing and updating the retrieval layer, not in the data itself. Don't delay starting because you're not sure which backend to pick; start with the most likely fit and make the write schema portable.

How many memories can a vector store realistically hold before retrieval quality degrades?

This depends on your query patterns and collection hygiene more than raw record count. A well-maintained collection with metadata filtering (scoping retrieval to a specific user or session) can stay accurate at millions of records. The quality degrades when the collection becomes a homogeneous blob: everything embedded, no metadata filtering, top-k returning memories from unrelated contexts. Collection design (namespacing, metadata schemas, filtering at query time) matters more than the total record count.

Does an agent actually need persistent memory, or is in-context enough?

For single-session tasks, in-context often suffices. The case for persistent memory is cross-session continuity: the agent should know the user's stated preferences from three weeks ago without being told again. It should know that a previous run failed at step four and why. If your agent doesn't need to remember anything across sessions, skip persistent memory entirely and save the complexity.

What's the operational cost difference between these options?

SQL (Postgres) is the cheapest to operate. You probably already have it, managed instances are inexpensive, and there's no additional inference cost per write. Vector stores (Pinecone, Qdrant, Weaviate) add embedding inference cost per write and ANN query cost per read; managed options run roughly $70-200/month at moderate scale depending on collection size. Graph databases (Neo4j Aura, Amazon Neptune) are the most expensive to run managed, typically starting at $200+/month for meaningful workloads. The cost differential justifies starting with SQL and vector, and only adding graph when the query patterns demand it.

Can I use a relational database for both exact memory and vector memory?

Yes. Postgres with the pgvector extension supports both row-level exact lookups and ANN vector search in the same database. For early-stage agents or teams that want to minimize infrastructure, pgvector is a pragmatic starting point: SQL for structured memory and vector similarity search in a single database, no separate vector store required. The trade-off is that Postgres with pgvector won't match a purpose-built vector database on throughput at large scale, but for most agent workloads it's more than sufficient.


Designing agent memory for a production system and not sure which backend fits your access patterns? The Laxaar team works on agent architectures across industries. Reach out and we'll help you map your memory requirements to the right backend before you've built yourself into a corner.

Working on something like this?

Get a fixed scope, timeline, and price within one business day — no obligation.

AI AgentsAgent MemoryAgent Persistence
Grow your business with us

Take your business to the next level.

Tell us what you're building. We'll come back inside one business day with a fixed scope, timeline, and team — or an honest “this isn't a fit”.

ENGINEERING PHILOSOPHY

Code is useless if it's not comprehensible to those who maintain it. We write code the next person can actually understand.