Backends: Bolt vs NAMS
A conceptual look at the two storage backends MemoryClient supports as of v0.4 — when to pick which, what’s different at the operational layer, and what trade-offs each makes.
The same MemoryClient API runs against two completely different storage backends: direct-bolt-to-Neo4j (the historical default) and the hosted Neo4j Agent Memory Service (NAMS). For task-oriented setup, see Use NAMS. For porting, see Migrate to NAMS.
TL;DR
| Concern | Bolt (direct Neo4j) | NAMS (hosted service) |
|---|---|---|
You provision |
Neo4j cluster (or Aura) |
Just an API key |
Ops burden |
You own indexes, backups, upgrades |
Managed for you |
Schema control |
Full (you can extend models) |
Server-managed |
Custom Cypher |
Read + write |
Read-only (via |
Geospatial features |
Yes (Point properties, GDS) |
Not exposed |
Multi-tenancy |
|
|
Embeddings / extraction |
Client-side (your config) |
Server-side (managed) |
Latency |
In-cluster (microseconds) |
HTTPS round-trip (tens of ms) |
When to choose which
Choose bolt when:
-
You already operate Neo4j (Aura or self-hosted) and want first-class access.
-
You need write-Cypher for custom data ingestion alongside agent memory.
-
You’re using bolt-only features: GDS algorithms, custom indexes,
adopt_existing_graphto layer agent memory onto an existing knowledge graph. -
You need geospatial queries (
Pointproperties, distance search). -
Air-gapped deployments — no outbound HTTPS to a hosted service.
Choose NAMS when:
-
You don’t want to run Neo4j. A managed REST API is enough.
-
You’re building a quick prototype and an API key is faster than spinning up Aura.
-
You don’t need write-Cypher — the high-level
client.short_term/client.long_term/client.reasoningsurface covers your use case. -
You want server-managed embedding, extraction, and deduplication.
Feature matrix
| Capability | Bolt | NAMS |
|---|---|---|
|
Yes |
Yes |
|
Yes |
Bolt-only / Yes |
|
(synthesized) |
Yes (Platinum) |
|
No |
Yes (Platinum) |
|
Yes (with dedup) |
Yes (server-side dedup) |
|
No |
Yes (Platinum) |
|
Yes |
Yes (Gold) |
|
Yes |
No |
|
Yes |
(handled server-side) |
|
Yes |
Yes |
|
Yes ( |
No (use |
|
Yes |
No (use |
|
Yes |
No (NAMS commits synchronously) |
|
Yes |
No (server-managed) |
|
Yes |
Yes (audit dimension is bolt-only) |
|
Yes |
Yes (Platinum) |
|
Yes (deprecated, removal v0.6.0) |
No (use |
|
Yes |
No (server-managed schema) |
MCP server |
Yes (16 tools) |
Yes (16 tools + 4 Platinum) |
LangChain, LlamaIndex, CrewAI, OpenAI Agents, Microsoft Agent, Google ADK |
Yes |
Yes (transparent — no code changes) |
Pydantic AI |
Yes |
Yes + |
Strands / AgentCore (AWS) |
Yes |
Yes + |
Operational model
Bolt: you own the database
The driver opens a persistent Bolt connection pool to a Neo4j instance. Reads and writes are issued as Cypher statements; schema setup (constraints, vector indexes, point indexes) is run on connect(). Memory lives entirely inside Neo4j — your backups, your upgrades, your ops.
NAMS: hosted REST API
MemoryClient opens an httpx.AsyncClient to the NAMS endpoint. Every operation is one HTTP request. NAMS owns the underlying Neo4j cluster; you never touch it directly. Auth is a single long-lived API key (MEMORY_API_KEY), sent as Authorization: Bearer ….
Wire protocols (NAMS only)
NAMS supports two:
-
REST (
POST /v1/conversations/{session_id}/messagesetc.) — the hosted service URL ending in/v1. -
TCK bridge (
POST /{snake_case_method}) — used by the conformance test reference implementation. The transport auto-selects based on whether the endpoint URL contains/v\d+.
You can force one or the other with NamsConfig(transport_mode="rest" | "bridge").
Error semantics
Both backends share the same exception hierarchy under MemoryError. NAMS adds five subclasses for HTTP-specific failures:
| Exception | When raised |
|---|---|
|
Network errors and 5xx after retries exhausted |
|
401, 403 |
|
Method unavailable on the active backend (or HTTP 405/501) |
|
429 after retries exhausted (carries |
|
HTTP 400 (carries |
The retry policy honors Retry-After on 429 and uses exponential backoff for 5xx and network errors.
The unified API contract
Both backends honor four @runtime_checkable Protocols in neo4j_agent_memory.core.protocols:
-
ShortTermProtocol—add_message,get_conversation,search_messages,list_sessions,delete_message,clear_session,get_context,get_conversation_summary,create_conversation,list_conversations,bulk_add_messages,get_observations,get_reflections. -
LongTermProtocol—add_entity,add_preference,add_fact,add_relationship,search_entities,search_preferences,search_facts,get_entity_by_name,get_related_entities,get_preferences_for,supersede_preference,get_facts_about,get_entity_relationships,get_context,set_entity_feedback,get_entity_history,get_entity_provenance. -
ReasoningProtocol—start_trace,add_step,record_tool_call,complete_trace,search_steps,get_similar_traces,get_trace,get_trace_with_steps,get_session_traces,list_traces,get_tool_stats,link_trace_to_message,get_context. -
CypherQueryProtocol—cypher(query, params)(read-only).
Code written against these Protocols (or their concrete bolt classes, which structurally satisfy them) is portable. Bolt-specific extras like add_messages_batch, extract_entities_from_session, find_potential_duplicates, geocode_locations live on the bolt classes only and aren’t part of the protocol contract.
What’s not on either backend
Some v0.2 / v0.3 features are bolt-only by design (the underlying server doesn’t expose them):
-
client.users(first-classUsernodes) -
client.buffered.submit(…)(background write queue) -
client.consolidation.*(dedupe / archival jobs) -
client.schema.adopt_existing_graph()(layering onto a pre-existing graph) -
Geospatial / Point-property features
-
Audit-edge (
:TOUCHED) features
On NAMS these accessors return a _NamsUnsupported sentinel — property access is harmless, but method calls raise NotSupportedError with a workaround hint. See Migrate to NAMS for the full table.
Performance characteristics
| Operation | Bolt | NAMS |
|---|---|---|
Single |
~1ms (in-cluster) |
~50ms (HTTPS RT) |
Bulk |
~10ms |
(bolt-only) |
|
(Platinum) |
~80ms |
|
~5-50ms (index size) |
~50-100ms |
|
~1ms |
~50ms |
NAMS is dominated by HTTPS round-trip cost. For high-throughput workloads, bolt is materially faster. For typical chat-agent workloads (one message per turn), the difference is invisible to users.
Migration is one env var away
# Existing v0.3 bolt code:
async with MemoryClient(MemorySettings(neo4j={"password": "..."})) as client:
await client.short_term.add_message(...)
Becomes:
import os
os.environ["MEMORY_API_KEY"] = "nams_xxxxx"
# Same code body — backend auto-selects NAMS from env.
async with MemoryClient() as client:
await client.short_term.add_message(...)
See Migrate to NAMS for the porting guide and full NotSupportedError reference.