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 client.query.cypher)

Geospatial features

Yes (Point properties, GDS)

Not exposed

Multi-tenancy

user_identifier kwarg

user_identifier (sent as userId) + workspace via API key

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_graph to layer agent memory onto an existing knowledge graph.

  • You need geospatial queries (Point properties, 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.reasoning surface covers your use case.

  • You want server-managed embedding, extraction, and deduplication.

Use both at once

MemoryClient is backend-monomorphic per-instance — one client holds one backend. But you can construct two clients in the same process if you want hybrid setups (e.g. bolt for domain data + NAMS for agent memory).

Feature matrix

Capability Bolt NAMS

client.short_term.add_message / search_messages

Yes

Yes

client.short_term.list_sessions / list_conversations

Yes

Bolt-only / Yes

client.short_term.create_conversation / bulk_add_messages

(synthesized)

Yes (Platinum)

client.short_term.get_observations / get_reflections

No

Yes (Platinum)

client.long_term.add_entity / search_entities

Yes (with dedup)

Yes (server-side dedup)

client.long_term.set_entity_feedback / get_entity_history

No

Yes (Platinum)

client.long_term.get_entity_provenance

Yes

Yes (Gold)

client.long_term.geocode_locations / search_locations_near

Yes

No

client.long_term.find_potential_duplicates / merge_duplicate_entities

Yes

(handled server-side)

client.reasoning.start_trace / add_step / record_tool_call

Yes

Yes

client.reasoning.get_tool_stats

Yes (dict)

No (use client.query.cypher if your NAMS tier exposes query access)

client.users (UserMemory)

Yes

No (use user_identifier= on calls)

client.buffered (BufferedWriter)

Yes

No (NAMS commits synchronously)

client.consolidation (dedupe / archival jobs)

Yes

No (server-managed)

client.eval (evaluation harness)

Yes

Yes (audit dimension is bolt-only)

client.query.cypher (read-only)

Yes

Yes (Platinum)

client.graph (raw Neo4jClient)

Yes (deprecated, removal v0.6.0)

No (use client.query.cypher)

client.schema.adopt_existing_graph

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 + nams_memory_tools() for Platinum extras

Strands / AgentCore (AWS)

Yes

Yes + nams_context_graph_tools() for Platinum extras

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}/messages etc.) — 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

TransportError (subclass of ConnectionError)

Network errors and 5xx after retries exhausted

AuthenticationError

401, 403

NotSupportedError

Method unavailable on the active backend (or HTTP 405/501)

RateLimitError

429 after retries exhausted (carries retry_after)

ValidationError

HTTP 400 (carries details)

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:

  • ShortTermProtocoladd_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.

  • LongTermProtocoladd_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.

  • ReasoningProtocolstart_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.

  • CypherQueryProtocolcypher(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-class User nodes)

  • 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 add_message

~1ms (in-cluster)

~50ms (HTTPS RT)

Bulk add_messages_batch(100)

~10ms

(bolt-only)

bulk_add_messages(100)

(Platinum)

~80ms

search_messages (vector)

~5-50ms (index size)

~50-100ms

client.query.cypher (small read)

~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.