Back to Insights
2026-05-05 14 min read Tanuj Garg

Database Architecture for AI Agent Memory: Storing State, Conversations, and Knowledge at Scale

AI & Automation#AI Agents#Databases#Agent Memory#System Design#Scalability#State Management

Introduction

An AI agent without memory is a stateless function—it forgets everything between calls. But an agent with memory becomes a persistent system that learns, adapts, and maintains context across sessions.

The challenge: agent memory generates intense database workloads. Every interaction reads conversation history, writes new observations, updates state, and retrieves relevant knowledge. Multiply this by thousands of concurrent agents, and you have a database architecture problem.

Agent memory isn't just a key-value store. It's a multi-tiered system with different data types, access patterns, and consistency requirements—each demanding different database choices and architectures.


Section 1: The Three Layers of Agent Memory

Agent memory is typically organized into three distinct layers, each with different database requirements:

Short-term / working memory

What the agent needs to complete its current task:

  • Current conversation messages,
  • Intermediate reasoning steps,
  • Tool call results and observations,
  • Task-specific state and variables.

Database requirements: fast reads/writes, ephemeral (can be deleted after task), often in-memory.

Long-term / episodic memory

What the agent has done and learned over time:

  • Past conversations and their outcomes,
  • Successful and failed task patterns,
  • User preferences and interaction history,
  • Error patterns and recovery strategies.

Database requirements: persistent storage, efficient time-range queries, high read throughput for recall.

Semantic / knowledge memory

What the agent knows about the world:

  • Facts, concepts, and relationships,
  • Domain-specific knowledge bases,
  • Document embeddings for RAG-style retrieval,
  • Structured knowledge graphs.

Database requirements: vector search for similarity, graph traversal for relationships, high read throughput.


Section 2: Database Choices for Each Memory Layer

Short-term memory: Redis or in-memory stores

Short-term memory needs microsecond read/write latency. Redis is the standard choice:

class AgentWorkingMemory {
  private redis: Redis;
  private agentId: string;

  async pushThought(thought: string): Promise<void> {
    await this.redis.lpush(
      `agent:${this.agentId}:working_memory`,
      JSON.stringify({ thought, timestamp: Date.now() })
    );
    // Keep only last 100 thoughts
    await this.redis.ltrim(`agent:${this.agentId}:working_memory`, 0, 99);
  }

  async getWorkingMemory(): Promise<Thought[]> {
    const items = await this.redis.lrange(
      `agent:${this.agentId}:working_memory`,
      0, -1
    );
    return items.map(item => JSON.parse(item)).reverse();
  }

  async clear(): Promise<void> {
    await this.redis.del(`agent:${this.agentId}:working_memory`);
  }
}

Why Redis works: atomic operations, TTL support for auto-expiry, list/set data structures that map naturally to agent state.

Long-term memory: PostgreSQL or document stores

Long-term memory needs persistence, querying, and efficient storage:

// Schema for episodic memory in PostgreSQL
CREATE TABLE agent_episodes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  agent_id VARCHAR(255) NOT NULL,
  episode_type VARCHAR(50) NOT NULL, -- 'conversation', 'task', 'error'
  content JSONB NOT NULL,
  outcome VARCHAR(50), -- 'success', 'failure', 'partial'
  created_at TIMESTAMPTZ DEFAULT NOW(),
  embedding VECTOR(1536) -- for semantic search over past episodes
);

CREATE INDEX idx_agent_episodes_agent_time
  ON agent_episodes(agent_id, created_at DESC);

CREATE INDEX idx_agent_episodes_embedding
  ON agent_episodes USING ivfflat (embedding vector_cosine_ops);

Why PostgreSQL works: JSONB for flexible content, vector support (pgvector) for semantic recall, mature ecosystem, strong consistency.

Semantic memory: Vector databases + graph databases

Knowledge retrieval needs specialized databases:

  • Vector DB (Pinecone, Qdrant, Milvus): for similarity search over facts and documents,
  • Graph DB (Neo4j, Neptune): for relationship traversal and knowledge graphs.
class AgentKnowledgeMemory {
  private vectorDb: VectorDatabase;
  private graphDb: GraphDatabase;

  async recallRelevantKnowledge(query: string): Promise<Fact[]> {
    // 1. Embed the query
    const queryEmbedding = await embed(query);

    // 2. Vector search for relevant facts
    const vectorResults = await this.vectorDb.search(queryEmbedding, 10);

    // 3. Graph traversal for related facts
    const factIds = vectorResults.map(r => r.id);
    const relatedFacts = await this.graphDb.query(`
      MATCH (f:Fact)-[r:RELATED_TO]-(related:Fact)
      WHERE f.id IN $factIds
      RETURN related
      LIMIT 20
    `, { factIds });

    return [...vectorResults, ...relatedFacts];
  }
}

Section 3: Handling High Write Throughput from Agents

AI agents generate a lot of writes—every thought, tool call, observation, and state change can be a write. With thousands of agents, this becomes a massive write stream.

Batching writes

Don't write every agent step individually. Batch writes to reduce database load:

class BatchedMemoryWriter {
  private writeBuffer: Map<string, MemoryWrite[]> = new Map();
  private flushInterval: NodeJS.Timeout;

  constructor(private db: Database, flushMs: number = 1000) {
    this.flushInterval = setInterval(() => this.flush(), flushMs);
  }

  async write(agentId: string, memory: MemoryWrite): Promise<void> {
    if (!this.writeBuffer.has(agentId)) {
      this.writeBuffer.set(agentId, []);
    }
    this.writeBuffer.get(agentId)!.push(memory);
  }

  private async flush(): Promise<void> {
    const promises = [];
    for (const [agentId, writes] of this.writeBuffer.entries()) {
      if (writes.length > 0) {
        promises.push(
          this.db.batchInsert(agentId, writes),
          this.writeBuffer.set(agentId, []) // clear buffer
        );
      }
    }
    await Promise.all(promises);
  }
}

Async write pipelines

Separate write processing from agent execution using a message queue:

// Agent writes to a queue, doesn't wait for DB
async function agentStep(agentId: string, step: AgentStep) {
  // Process step immediately in memory
  const result = await executeStep(step);

  // Async: persist to database via queue
  await writeQueue.publish({
    agentId,
    step,
    result,
    timestamp: Date.now()
  });

  return result;
}

// Worker processes writes in batches
async function writeWorker() {
  const batch: WriteMessage[] = [];
  while (true) {
    const message = await writeQueue.consume();
    batch.push(message);

    if (batch.length >= 100) {
      await db.batchInsert(batch);
      batch.length = 0;
    }
  }
}

Section 4: Efficient Memory Retrieval at Scale

As agents accumulate memory, retrieving relevant information becomes a performance bottleneck.

Time-decayed relevance

Not all memories are equally important. Recent memories are more relevant than old ones:

async function recallMemories(
  agentId: string,
  query: string,
  limit: number = 10
): Promise<Memory[]> {
  // Combine recency and relevance
  const memories = await db.query(`
    SELECT *,
           -- Recency score: exponential decay based on age
           1.0 / (1.0 + EXTRACT(EPOCH FROM (NOW() - created_at)) / 86400.0) AS recency_score,
           -- Relevance score from vector similarity
           (1 - cosine_distance(embedding, $queryEmbedding)) AS relevance_score
    FROM agent_episodes
    WHERE agent_id = $agentId
    ORDER BY (0.3 * recency_score + 0.7 * relevance_score) DESC
    LIMIT $limit
  `, { agentId, queryEmbedding: await embed(query), limit });

  return memories;
}

Memory summarization

Instead of retrieving raw memories, store summarized versions for efficient recall:

async function summarizeAndStoreMemories(agentId: string) {
  // Get recent raw memories
  const recentMemories = await db.query(`
    SELECT * FROM agent_episodes
    WHERE agent_id = $agentId
    AND created_at > NOW() - INTERVAL '7 days'
    ORDER BY created_at DESC
  `, { agentId });

  // Summarize with LLM
  const summary = await llm.complete({
    prompt: `Summarize these agent memories into key learnings:\n${JSON.stringify(recentMemories)}`
  });

  // Store summary as a compact memory
  await db.insert({
    agent_id: agentId,
    episode_type: 'summary',
    content: { summary, period: '7 days', memory_count: recentMemories.length },
    created_at: new Date()
  });
}

Section 5: Multi-Agent Memory Sharing

When multiple agents collaborate, they need shared memory—a common knowledge base they all contribute to and read from.

Shared knowledge base architecture

class SharedAgentMemory {
  private db: Database;

  async writeSharedKnowledge(fact: Fact, contributors: string[]): Promise<void> {
    await this.db.query(`
      INSERT INTO shared_knowledge (fact, embedding, contributors, confidence)
      VALUES ($fact, $embedding, $contributors, $confidence)
    `, {
      fact,
      embedding: await embed(fact),
      contributors,
      confidence: 1.0 / contributors.length
    });
  }

  async querySharedKnowledge(query: string, topK: number = 5): Promise<Fact[]> {
    const queryEmbedding = await embed(query);
    return this.db.query(`
      SELECT fact, contributors, confidence,
             (1 - cosine_distance(embedding, $queryEmbedding)) AS similarity
      FROM shared_knowledge
      ORDER BY similarity DESC
      LIMIT $topK
    `, { queryEmbedding, topK });
  }
}

Conflict resolution for shared memory

When multiple agents write conflicting information, you need a resolution strategy:

  • Last-write-wins: simple but loses information,
  • Confidence-weighted: store confidence scores, prefer higher confidence,
  • Versioned facts: keep multiple versions, let readers decide.
async function resolveMemoryConflict(
  existing: Fact,
  newFact: Fact,
  agentId: string
): Promise<Fact> {
  // Use confidence scores
  if (newFact.confidence > existing.confidence) {
    return { ...newFact, supersedes: existing.id };
  }

  // Or merge information
  return {
    ...existing,
    content: mergeContent(existing.content, newFact.content),
    contributors: [...existing.contributors, agentId]
  };
}

Section 6: Database Scaling Strategies for Agent Memory

As your agent system grows, your memory database needs to scale.

Read replicas for memory retrieval

Agents read memory far more often than they write it. Use read replicas:

class ScalableMemoryStore {
  private primary: Database; // writes
  private replicas: Database[]; // reads

  async writeMemory(agentId: string, memory: Memory): Promise<void> {
    await this.primary.insert('agent_memories', {
      agentId,
      ...memory,
      created_at: new Date()
    });
  }

  async recallMemory(agentId: string, query: string): Promise<Memory[]> {
    // Route to least-loaded replica
    const replica = this.getLeastLoadedReplica();
    return replica.query(`
      SELECT * FROM agent_memories
      WHERE agent_id = $1
      ORDER BY created_at DESC
      LIMIT 50
    `, [agentId]);
  }
}

Sharding by agent ID

For very large deployments, shard memory by agent ID:

function getShardForAgent(agentId: string, shards: Database[]): Database {
  const hash = murmurhash(agentId);
  return shards[hash % shards.length];
}

This ensures all memory for a given agent is on the same shard, enabling efficient cross-memory queries.

Tiered storage for old memories

Move old, rarely-accessed memories to cheaper storage:

async function tierMemoryStorage() {
  // Move memories older than 90 days to cold storage
  const oldMemories = await hotDb.query(`
    SELECT * FROM agent_memories
    WHERE created_at < NOW() - INTERVAL '90 days'
  `);

  await coldStorage.bulkInsert(oldMemories);
  await hotDb.query(`
    DELETE FROM agent_memories
    WHERE created_at < NOW() - INTERVAL '90 days'
  `);
}

Section 7: Consistency and Isolation for Agent Memory

Agent memory systems face consistency challenges: what happens when an agent reads its memory while another process is writing to it?

Snapshot isolation for agent reads

Use database snapshot isolation so agents read a consistent view of their memory:

async function agentTaskWithConsistentMemory(agentId: string, task: Task) {
  // Start a transaction with snapshot isolation
  const result = await db.transaction(async (tx) => {
    // All memory reads within this transaction see the same snapshot
    const memory = await tx.query(
      'SELECT * FROM agent_memories WHERE agent_id = $1',
      [agentId]
    );

    const result = await executeTask(task, memory);

    // Write results
    await tx.insert('agent_memories', {
      agent_id: agentId,
      content: result,
      created_at: new Date()
    });

    return result;
  });

  return result;
}

Eventual consistency for shared memory

Shared knowledge bases can use eventual consistency—writes propagate asynchronously:

async function writeSharedMemoryAsync(fact: Fact) {
  // Write to local cache immediately
  localCache.set(fact.id, fact);

  // Async: propagate to shared store
  await writeQueue.publish({ type: 'shared_memory_write', fact });
}

Conclusion

Database architecture for AI agent memory is about matching data patterns to the right storage systems. Short-term memory needs the speed of Redis. Long-term memory needs the flexibility of PostgreSQL or document stores. Semantic memory needs the search capabilities of vector and graph databases.

The key insight: don't try to use one database for everything. Each memory layer has distinct access patterns, consistency requirements, and scale characteristics. Build a multi-tier memory architecture that plays to each database's strengths.

As your agent system grows, focus on batching writes, scaling reads with replicas, and moving old data to tiered storage. The difference between an agent that scales and one that bottlenecks on memory is usually the database architecture.


Need help building scalable memory systems for your AI agents?