You can port all seven of mem0’s public methods, get a successful response from every one, and still build an agent that remembers different things.
The problem is that add, search, get, update, delete, history and reset are only the visible edge of a memory system. Behaviour also lives in the extraction prompt, text normalisation, deduplication hash, retrieval order, metadata filters, scoring formula and history semantics. Change any of those and the difference propagates into every future conversation, even though the new implementation still looks compatible.
I wanted mem0’s behaviour in Rust because memory sits on the hot path of the agent systems I build. A Python runtime there means an interpreter, a substantial resident footprint, a broad dependency tree and another environment to deploy. A small native binary was appealing, but replacing Python only mattered if it preserved the product’s memory decisions. The Maestro coding agent produced the first implementation and parity harness; I set the compatibility standard: mem0-rs had to remember what Python mem0 2.0.4 would have remembered.
The main extraction prompt ran to 33,653 characters and had to be copied exactly. Some of it invited revision. I would normally consider rewriting an awkward instruction conscientious engineering; in a compatibility port, that would have changed the product.
The extraction prompt largely determines which parts of a conversation become durable facts. A cleaner sentence could make the Rust version retain something Python omitted, or ignore something Python kept, without causing an error anywhere. The verifier exists to stop that kind of edit from slipping through.
Where memory behaviour lives
An inferred add retrieves related memories, combines them with recent scoped messages, asks a model to extract durable facts, then normalises and hashes those facts before storage. A small difference near the beginning changes what reaches every later stage.
Deduplication shows how quiet the drift can be. mem0 hashes normalised fact text with MD5. If the port normalises differently, both implementations can return success while one stores the same fact twice. Every later search then sees two copies.
Retrieval is more entangled still. Semantic similarity is combined with BM25 over lemmatised text, optional entity boosts, metadata filters, score thresholds and truncation. Reordering two operations can change which memory survives the final top_k, even when every individual calculation is reasonable.
I treated all of this as the port: prompts, parsing, hashing, storage events, scoring and ordering alongside the seven public methods.
Five prompts, copied exactly
bench/verify_prompts.py compares the Rust constants directly with the Python source:
Prompt fidelity (Rust constants vs Python mem0 source)
[IDENTICAL] ADDITIVE_EXTRACTION_PROMPT (python 33653 chars, rust 33653 chars)
[IDENTICAL] DEFAULT_UPDATE_MEMORY_PROMPT (python 5310 chars, rust 5310 chars)
[IDENTICAL] AGENT_CONTEXT_SUFFIX (python 563 chars, rust 563 chars)
[IDENTICAL] PROCEDURAL_MEMORY_SYSTEM_PROMPT (python 5100 chars, rust 5100 chars)
[IDENTICAL] MEMORY_ANSWER_PROMPT (python 547 chars, rust 547 chars)
PROMPT_FIDELITY: PASS
The verifier makes no judgement about whether those prompts are good. It stops a language port from becoming an unannounced prompt rewrite. Improvements can happen later, deliberately, with their own evaluation.
The same discipline applies to runtime behaviour. bench/run_parity.sh drives Python and Rust through seventeen shared scenarios. A scripted model returns the same extraction output to both; an FNV-1a embedder produces deterministic vectors. Once provider randomness is removed, disagreement belongs to the implementations.
The scenarios compare raw and inferred storage, MD5 deduplication, ranked search text and order, scores within 0.01, operator filters, updates and history events, deletion, prompt construction and parsing fallbacks. All seventeen pass. The reference is pinned to mem0 2.0.4, so when upstream changes, I can see the difference rather than accidentally absorbing it.
I later applied the same harness to mem0-ts. It also passes seventeen out of seventeen, giving three implementations a shared behavioural contract instead of three independent interpretations of the documentation.
The spaCy exception
I did make one deliberate change. Python mem0 can use spaCy for richer entity extraction when spaCy is installed; without it, entity linking does nothing. I did not want that runtime in the default Rust binary, so mem0-rs always extracts a dependency-free subset of proper names and quoted text.
The port extracts more entities than stock mem0 without spaCy and fewer than an installation using spaCy’s dependency parsing for compound and noun entities. Entity boosts are secondary to semantic and BM25 ranking, but the difference can still affect recall, so the quality documentation records the exception explicitly.
Storage backends introduce another boundary. The embedded store uses native BM25; Postgres substitutes full-text search; Qdrant omits the keyword stage and relies on semantic scores plus entity boosts. Switching providers is a configuration change, but it is not behaviourally neutral. The vector-store guide says so beside the configuration rather than leaving the surprise for production.
Speed, after parity
Only after the two versions agreed did I measure the reason for doing the port. The equal-workload benchmark removes network calls, uses fixed embeddings, disables inference and gives both implementations in-process stores. That isolates the framework work which Rust can actually change; no port makes an LLM provider answer faster.
On a 2 vCPU, 8 GB Linux machine, a run of 2,000 adds and 500 searches measured:
| operation | Rust | Python |
|---|---|---|
| add | 18.7 µs/op | 64.8 µs/op |
| search | 6.1 ms/op | 18.4 ms/op |
| peak RSS | 15.4 MB | 100.9 MB |
That is about 3 to 3.5 times faster per operation, with roughly one-sixth the peak memory for this workload. A smaller run used 9.5 MB in Rust against 96.9 MB in Python. Python received its faster no-spaCy path, so the comparison does not hide a large NLP runtime on one side.
The deployment result is a binary of roughly 12 MB with no Python environment to provision. It can begin with the embedded store and move to Qdrant or Postgres when the deployment needs shared storage. The security guide is intentionally blunt about the server: keep it internal and put an authenticated gateway in front.
Arcwell’s memory provider now runs code derived from mem0-rs, vendored into the monorepo, and the TypeScript port answers to the same parity suite. Taranjeet Singh, Deshraj Yadav and the mem0 contributors designed the behaviour those implementations preserve. The suite lets me update my two ports deliberately as upstream changes and notice when either one drifts.