Shared Memory for Multi-Agent Systems: Blackboards to Message Queues
Multi agent shared memory is the coordination layer that decides whether a team of AI agents behaves like a team or like five interns who never speak to each other. The moment you split work across a researcher, a verifier, and a writer, they need somewhere to read and write partial results, and that somewhere comes in two broad families: shared state that every agent can inspect (the blackboard tradition, going back to the 1970s) and streams of messages that agents subscribe to (the queue tradition). This article walks through both ends of that spectrum with runnable Python: a classic blackboard with a control loop, LangGraph typed state with reducers, Redis Streams with consumer groups, and the compare-and-set code that stops concurrent agents from silently destroying each other's work.
The short answer for the impatient: blackboards and graph state give every agent a consistent view of the task and are the right default for a single orchestrated team. Message queues scale further, decouple failures, and hand you an audit log for free. Nearly every production system ends up hybrid: a queue for transport, a structured store for the task's working set, and a database or vector store for knowledge that outlives the task. The hard engineering is not picking a topology. It is controlling writes, because an LLM agent's read-modify-write cycle spans seconds to minutes of model calls, which makes conflict windows enormous compared to anything you deal with in normal backend work.
What Multi Agent Shared Memory Actually Means
Before the patterns, pin down the term, because "memory" gets used for at least three different things in agent systems.
- Working memory: the scratchpad for the current task. Partial results, the plan, open questions, intermediate artifacts. This is what blackboards and graph state hold, and it usually dies with the task.
- Episodic memory: a record of what happened. Which agent did what, in what order, with what result. Append-only logs and message streams give you this almost for free.
- Semantic memory: durable facts and learned knowledge that should survive across tasks. Vector stores, relational tables, and knowledge graphs live here.
Multi agent shared memory means any of these layers becomes readable and writable by more than one agent. That single property creates three problems that a solo agent never has.
First, visibility: which agent sees which updates, and when. If the verifier reads the board before the researcher finishes writing, it verifies a half-formed claim. Second, contention: two agents writing the same key at the same time, where the second write erases the first and nobody notices. Third, context budget: shared memory is not part of any model's context window. Every read is a deliberate act of serializing store contents into a prompt, so a shared store that grows without compaction is a token-burning machine that slowly crowds out the actual task.
Keep those three problems in mind. Every architecture below is a different trade among them.
The Blackboard Pattern: The Original Shared Memory Architecture
The blackboard pattern predates LLMs by five decades. It came out of the Hearsay-II speech understanding project at Carnegie Mellon in the 1970s, and the metaphor still holds: a group of specialists stands around a physical blackboard, each watching for the moment their expertise applies, walking up, and adding to the solution.
The architecture has exactly three parts.
- The blackboard: a shared, structured data store holding the problem and every partial solution.
- Knowledge sources: independent specialists that read the board, decide whether they can contribute right now, and write their contribution back. They never talk to each other directly. All communication goes through the board.
- Control: a loop that watches the board, picks which eligible knowledge source runs next, and detects quiescence (nobody has anything left to add).
Here is a minimal, runnable version in pure Python. The lock matters even in a single process, because you will eventually run knowledge sources in threads.
import threading
from dataclasses import dataclass, field
from typing import Any, Callable
@dataclass
class Blackboard:
_data: dict = field(default_factory=dict)
_lock: threading.RLock = field(default_factory=threading.RLock)
version: int = 0
def read(self, key: str) -> Any:
with self._lock:
entry = self._data.get(key)
return entry["value"] if entry else None
def write(self, key: str, value: Any, agent: str) -> int:
with self._lock:
self._data[key] = {"value": value, "by": agent}
self.version += 1
return self.version
def snapshot(self) -> dict:
with self._lock:
return {k: v["value"] for k, v in self._data.items()}
@dataclass
class KnowledgeSource:
name: str
can_contribute: Callable[[dict], bool]
contribute: Callable[[dict], tuple[str, Any]]
def run_controller(board: Blackboard, sources: list[KnowledgeSource],
max_cycles: int = 25) -> dict:
for _ in range(max_cycles):
snap = board.snapshot()
eligible = [s for s in sources if s.can_contribute(snap)]
if not eligible:
return snap # quiescence: the team is done
source = eligible[0] # swap in smarter scheduling here
key, value = source.contribute(snap)
board.write(key, value, agent=source.name)
return board.snapshot()Wiring an LLM agent in is just a knowledge source whose contribute function calls a model:
def planner_ready(snap: dict) -> bool:
return "question" in snap and "plan" not in snap
def planner_run(snap: dict) -> tuple[str, Any]:
# call your model of choice here; stubbed for brevity
steps = ["gather sources", "verify claims", "draft answer"]
return "plan", steps
sources = [KnowledgeSource("planner", planner_ready, planner_run)]The pattern maps unreasonably well to LLM agent teams. Contribution is opportunistic, so there is no fixed pipeline to maintain: a critic agent fires whenever a draft appears, regardless of who wrote it. Partial solutions are visible to everyone, so a stuck agent can benefit from another agent's half-finished work. And adding a specialist is one list append, not a rewiring of the graph.
The weaknesses are just as real. The control loop is where the actual intelligence lives, and "pick the first eligible source" degrades badly as the team grows: you end up building priority scoring, which is a scheduler, which is hard. The board itself is a single point of contention. And the naive version above is single-process, which is fine for one orchestrated task and useless for a distributed fleet.
One modern note: coding agents rediscovered this pattern by accident. When Claude Code or similar tools spawn subagents that all read and write the same repository working tree, the filesystem is the blackboard, files are the entries, and git is the version counter. If you have used that workflow, you have used a blackboard.
Multi Agent Shared Memory in LangGraph: Typed State and Reducers
LangGraph is the most widely deployed way to get multi agent shared memory today, and it is worth seeing precisely because it packages the blackboard idea into something with types and merge rules. The state schema is the board, nodes are knowledge sources, and edges plus conditional routing are the control component.
The important design decision hides in one annotation. By default, when a node returns a value for a state key, that value replaces whatever was there: last write wins. When you fan out to parallel agents, last write wins is exactly the lost update bug. Reducers fix it by declaring how concurrent writes combine.
import operator
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
class ResearchState(TypedDict):
question: str
findings: Annotated[list[str], operator.add] # concurrent appends merge
draft: str # last write wins, intentionally
def search_agent(state: ResearchState) -> dict:
return {"findings": [f"web result about {state['question']}"]}
def papers_agent(state: ResearchState) -> dict:
return {"findings": [f"paper excerpt about {state['question']}"]}
def writer_agent(state: ResearchState) -> dict:
notes = "\n".join(state["findings"])
return {"draft": f"Report from {len(state['findings'])} findings:\n{notes}"}
g = StateGraph(ResearchState)
g.add_node("search", search_agent)
g.add_node("papers", papers_agent)
g.add_node("writer", writer_agent)
g.add_edge(START, "search")
g.add_edge(START, "papers") # search and papers run in parallel
g.add_edge("search", "writer")
g.add_edge("papers", "writer")
g.add_edge("writer", END)
app = g.compile()
result = app.invoke({"question": "agent memory", "findings": [], "draft": ""})
print(result["draft"])Both parallel agents write to findings in the same superstep, and the operator.add reducer merges the writes deterministically instead of dropping one. That is the whole trick, and it is worth stating plainly: a reducer is declarative concurrency control. You stop hoping writes do not collide and instead declare what a collision means. Append for lists, dict merge for maps, max for scores, custom functions for anything else.
Two more LangGraph facilities round out the memory story. Checkpointers (SQLite for dev, Postgres for prod) persist the state after every superstep keyed by thread id, which turns working memory into something that survives crashes and doubles as episodic memory you can rewind. And the store interface gives you a key-value layer shared across threads for long-lived facts, so you do not smuggle durable knowledge through task state.
The limits mirror the blackboard's: this is shared memory for one orchestrated graph. The state lives with the graph run. Two independent services cannot casually share it, and that is the point where teams reach for a queue.
Message Queues: Shared Memory You Subscribe To
The queue family flips the model. Instead of agents reading a board, agents receive messages. The old Go proverb applies directly: do not communicate by sharing memory, share memory by communicating. Every handoff becomes an explicit event, and the shared memory is the log of those events.
Redis Streams is the lightest way to get this with real semantics: append-only log, consumer groups for work distribution, acknowledgements, and replay.
import json
import redis
r = redis.Redis(decode_responses=True)
# A researcher agent publishes a finding as an event
r.xadd("agent:events", {
"type": "finding",
"agent": "researcher-2",
"payload": json.dumps({"claim": "X reduces Y", "confidence": 0.8}),
})
# Writers form a consumer group: each event is delivered to exactly
# one member of the group, so you can scale writers horizontally.
try:
r.xgroup_create("agent:events", "writers", id="0", mkstream=True)
except redis.ResponseError:
pass # group already exists
messages = r.xreadgroup("writers", "writer-1",
{"agent:events": ">"}, count=10, block=5000)
for stream, entries in messages:
for msg_id, fields in entries:
payload = json.loads(fields["payload"])
# ... do the work, write output somewhere durable ...
r.xack("agent:events", "writers", msg_id)Three properties make this compelling for agent fleets. Decoupling: producers do not know or care which agents consume, so you add a new specialist by adding a consumer group, without touching any existing agent. Failure isolation: an agent that crashes mid-task leaves its message unacknowledged, and the pending-entries mechanism lets another worker claim and retry it. A crashed agent cannot corrupt anyone else's view of the world. Replay: the stream is an append-only log, so XRANGE over it reconstructs exactly what the team knew at any point in time. That is your episodic memory and your debugging story in one structure, and when an agent produces a bizarre output, replaying its inbox is how you find out why.
The same shape scales up through the usual suspects: Kafka when you need retention and throughput across many services, NATS JetStream when you want lightweight subjects with persistence, RabbitMQ or SQS for classic work queues. And note that most agent framework "handoffs", including the OpenAI Agents SDK style of passing control between agents, are this same philosophy running in-process: a message moves, state does not.
The costs are the mirror image of the benefits. There is no board to glance at: no single place holds "the current state of the task". Each consumer builds its own projection, and in practice you reintroduce a materialized view, typically a Redis hash or a Postgres row per task that consumers update as events flow. At that point you are running a queue for transport and a small blackboard for the working set, which is the hybrid almost everyone converges on. Queues also deliver at-least-once, not exactly-once, so duplicate messages will happen. Every consumer must be idempotent: derive a dedupe key from the message id, check it before acting, and make writes safe to repeat.
Concurrency Control: Where Agent Teams Quietly Lose Data
Here is the failure mode that costs real money. Agent A reads the task state, spends 45 seconds across three model calls deciding how to update the summary, and writes it back. Agent B did the same thing in parallel, starting 5 seconds later, and writes back 10 seconds after A. B's write is based on a snapshot that predates A's work, so A's contribution is gone. No error was raised. The final output is just mysteriously worse.
In a normal web service the read-modify-write window is microseconds. With LLM agents it is the length of your model calls, which means conflicts are not rare edge cases: at any real concurrency they are routine. You have four defenses, in order of preference.
- Partition ownership. Each agent owns specific keys and nobody else writes them. The researcher owns findings, the writer owns draft, the critic owns review. Most conflicts disappear at design time. Do this first.
- Append, do not overwrite. Model shared keys as grow-only collections with a merge rule, which is exactly what LangGraph reducers and event streams give you. Two appends commute; two overwrites do not.
- Optimistic concurrency. For keys that genuinely must be read-modify-written by multiple agents, version every value and make writes conditional: the write succeeds only if the version has not moved since the read. On failure, re-read, re-merge (often with a cheap model call), and retry.
- Locks, reluctantly. A lock held across a 45-second think is a lock that stalls the team, and a crashed agent holding it stalls the team forever. If you truly need one, make it a lease with a TTL and keep model calls outside the critical section.
Optimistic compare-and-set in Redis fits in one function:
import redis
def cas_write(r: redis.Redis, key: str, expected_version: int,
value: str) -> bool:
vkey = f"{key}:version"
with r.pipeline() as pipe:
try:
pipe.watch(vkey)
current = int(pipe.get(vkey) or 0)
if current != expected_version:
pipe.unwatch()
return False # stale read: re-read, merge, retry
pipe.multi()
pipe.set(key, value)
pipe.incr(vkey)
pipe.execute()
return True
except redis.WatchError:
return False # raced with another writerThe agent-side protocol: read the value and its version together, think, then call cas_write with the version you read. A False return is not an error, it is the system telling the agent its knowledge is stale, and the correct response is to fold the newer value into its own update before retrying. If you are on Postgres instead of Redis, the same idea is an UPDATE with a WHERE version = clause, checking the affected row count.
Vector Stores as Shared Semantic Memory
Everything so far is task-scoped. The third layer of multi agent shared memory is knowledge that outlives tasks: facts the team has established, preferences it has learned, summaries of past episodes. This is where vector stores (pgvector, Qdrant, Chroma, Weaviate, Redis with vector search) earn their place, though a plain Postgres table with jsonb is underrated when the facts are structured and you can query by key instead of by similarity.
Three rules keep a shared semantic store useful instead of toxic.
- Write distilled facts, not transcripts. A 40-turn agent conversation is not memory, it is sediment. Store the three-sentence conclusion with enough context to stand alone. Retrieval quality tracks the quality of what you wrote far more than the embedding model.
- Stamp provenance on every entry. Author agent, timestamp, source, confidence. When an entry turns out wrong, provenance is the difference between deleting one row and distrusting the whole store. It also lets readers weight entries: a fact written by the verifier outranks one written by a first-pass researcher.
- Gate durable writes. This is the poisoning problem: one agent hallucinates a "fact", writes it to shared memory, and every future agent retrieves it as established truth. The fix is a validation gate in front of the store: a critic agent or a rule-based check that must pass before anything is committed to long-term memory. Working memory can be sloppy; semantic memory cannot.
On namespacing: a single team-wide collection with metadata filters usually beats per-agent silos, because the entire point is cross-agent reuse. But give each agent a private scratch namespace too, so half-formed hunches do not leak into the shared pool before they pass the gate.
Choosing a Shared Memory Architecture
The decision usually falls out of three questions: how many processes, how long do tasks run, and what must survive.
- One process, one orchestrated task, a handful of agents: in-memory blackboard or LangGraph state with reducers. Add a checkpointer for crash recovery. Do not deploy infrastructure you do not need.
- Pipeline or DAG shape with parallel fan-out: graph state with reducers is purpose-built for this. The reducer handles merge, the checkpointer handles durability, and you keep a single consistent view.
- Agents on different machines, autoscaling workers, long-running jobs: message queue as transport (Redis Streams to start, Kafka or NATS when scale demands), plus a materialized working-set store per task. Consumers must be idempotent.
- Knowledge that must outlive tasks: vector store or relational table behind a validation gate, with provenance metadata on every row.
- Hard audit or replay requirements: make the append-only stream the source of truth and derive all state from it. Regulated environments end up here.
And one budget note that architecture diagrams always omit: shared memory is read into prompts. If your board serializes to 30,000 tokens and eight agents each read it five times per task, the memory layer is a first-class driver of your inference bill and your latency. Keep entries distilled, expose filtered views per agent role rather than the whole board, and compact aggressively.
A Production Checklist
Before you trust an agent team's shared memory in production, check that you have:
- Versioned writes or reducers on every key that more than one agent can touch.
- Ownership documented: for each key, which agent writes it.
- Provenance (agent, timestamp, source) on every shared entry, working and semantic.
- Idempotent consumers and dedupe keys anywhere a queue is involved.
- TTLs and compaction so working memory does not grow monotonically.
- A validation gate in front of all durable semantic writes.
- Logging on every read and write, because "why did the agent believe that" is a query you will run.
- A token budget for memory reads, measured, not guessed.
Start Smaller Than You Think
The pattern progression that works: start with typed shared state and reducers in a single process, because it is debuggable and consistent. Add a stream when you actually have two machines or need failure isolation, not before. Add a gated semantic store when knowledge genuinely must outlive tasks. At every step, the discipline that pays is the boring one: own your keys, version your writes, distill what you store, and log everything. Multi agent shared memory is not a framework feature you enable. It is a small distributed system, and it rewards being treated like one.
FAQ
What is multi agent shared memory?
It is any memory layer (working state, event history, or long-term knowledge) that multiple AI agents can read and write. It covers blackboard-style shared state, message streams that agents subscribe to, and shared vector or relational stores, along with the concurrency control that keeps simultaneous agents from corrupting each other's updates.
Is a blackboard better than a message queue for AI agents?
Neither dominates. A blackboard gives every agent one consistent view and is simpler for a single orchestrated team. A queue scales across machines, isolates failures, and provides replay, at the cost of no single current-state view. Production systems usually combine them: queue for transport, materialized state for the working set.
How do I stop agents from overwriting each other's updates?
In order of effectiveness: give each agent exclusive ownership of its keys, model shared data as append-only with a merge rule (reducers), use optimistic compare-and-set with version numbers for true read-modify-write keys, and avoid locks across model calls because agent think time makes lock holds pathological.
Should all agents share one vector store or have separate ones?
Share one collection for team knowledge, filtered by metadata, because cross-agent reuse is the point. Keep a private scratch namespace per agent for unvalidated hunches, and require entries to pass a validation gate before promotion to the shared pool. That gate is your main defense against one agent's hallucination poisoning the whole team.
Do frameworks handle this for me?
Partially. LangGraph gives you typed shared state, reducers, and checkpointing. CrewAI ships built-in short-term and long-term crew memory. The OpenAI Agents SDK and Microsoft Agent Framework (the successor to AutoGen) center on handoffs and message passing. None of them decide ownership, write gating, compaction, or token budgets for you. Those remain design work.
How big should a shared memory entry be?
Small enough that an agent can read many of them without blowing its context budget. A useful discipline is to store conclusions in a few sentences with provenance, and keep raw material (transcripts, documents) elsewhere, referenced by id, so agents pull detail only when they need it.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.
Related reading