Halfway through porting mem0’s memory layer to Rust, I caught myself editing the main extraction prompt instead of copying it. The prompt runs 33,653 characters, and there are instructions in it I would have worded differently. Rewording them is the one thing I couldn’t let myself do: in mem0, that prompt largely decides what gets remembered at all.
Change a few sentences and both implementations still return success from add, but the port begins remembering different facts from the original. A wrong deduplication hash produces the same outward success while one implementation stores a fact twice and feeds both copies into every future turn. There is no error or obvious failing test. An agent reads memory on every turn, so drift may remain invisible. I made one deliberate exception, described below.
mem0-rs is my Rust port of mem0 v2.0.4, Taranjeet Singh and Deshraj Yadav’s long-term memory layer for agents. It extracts durable facts from conversations, deduplicates them, stores them with embeddings, and retrieves them through semantic and BM25 search. I ported it because memory sits on the hot path of a dense agent system. A Python runtime there brings interpreter overhead, high resident memory, a large dependency tree, and a deployment that includes a Python environment. I wanted one fast binary with small dependencies that remembered exactly what the Python version remembered. The repository therefore ships bench/verify_prompts.py to catch my edits.
Seven methods, and everything underneath
mem0 exposes seven methods: add, search, get, update, delete, history, and reset. Matching those signatures is easy. The duplicate fact occurs one layer lower. On an inferred add, facts returned by the LLM are deduplicated by MD5 before embedding and persistence. Different text normalisation before hashing can store the same fact twice behind the same success code.
MD5 is only the simplest source of drift. The extraction prompt combines recent scoped messages with related memories retrieved before extraction. A change to either input produces different facts. search combines semantic retrieval, BM25 over a lemmatised text field, optional entity boosts, metadata filters, thresholds, and truncation. A different operation order or scoring formula changes the ranking returned on future turns.
Seventeen scenarios, no model
In early June, with the port finished, I ran the verifier. It checks that the five prompt constants governing extraction, updates, procedural memory, and answers are byte-identical to Python mem0’s:
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
If I had let one rewording through, the first line would read [DIFFERS] instead and the run would end PROMPT_FIDELITY: FAIL. It has never printed that word at me; it caught me only in intention.
mem0-rs also has a parity harness that drives both implementations through 17 scenarios. It uses an FNV-1a hash embedder for deterministic embeddings and scripted LLM output. With provider randomness removed, any difference between the Python and Rust runs is a porting bug. Checks cover storage results, deduplication, ranked search order with scores within 0.01, metadata filters, update and history events, deletion, prompt fidelity, scoring, and text-parsing fallbacks. All 17 pass on both implementations today. Parity is pinned to v2.0.4 while upstream mem0 continues to change. Rerunning the harness against a new release shows what moved. Because prompt fidelity is one scenario, my attempted rewording would have failed in two places.
The same harness now drives a second, independent port, mem0-ts in TypeScript, to the same 17 out of 17.
So I have to tell you about the one place I changed the behavior on purpose.
The one deliberate divergence
When spaCy is installed, Python mem0 can use it for richer entity extraction. I did not want a large NLP runtime in the Rust binary’s default path, so mem0-rs always extracts a dependency-free subset consisting of proper names and quoted text. The quality document defines the difference. Stock mem0 without spaCy performs no entity linking, so the port extracts more than a default install and less than an install with spaCy, which also finds compound and noun entities through dependency parses. I accepted this documented, narrow difference because entity boosting is secondary to semantic and BM25 ranking. If your recall depends on spaCy’s extraction, mem0-rs will behave differently, regardless of the speed results below.
What the port bought
I looked at speed only after the harness passed. In the equal-workload benchmark, with the network removed on both sides, mem0-rs is roughly 3 to 3.5 times faster per operation than Python mem0. The causes are checkable: no interpreter, no GIL, no Pydantic validation and deepcopy on every call, and scoring math specialised by the compiler instead of dispatched at runtime. On the 2,000-add and 500-search run, add latency was 18.7 us/op in Rust against 64.8 in Python. Search took 6.1 ms/op against 18.4, and peak RSS was 15.4 MB against 100.9 MB. On the smaller run, peak RSS was 9.5 MB against 96.9 MB. LLM and embedding calls cost the same in both implementations; the savings come from the work around them. The benchmark also gave Python its fast path by running its lemmatiser without spaCy. Installing spaCy widens the gap.
The deployment story is short: the whole thing ships as a single binary of about 12 MB, with no Python environment on the deployment surface. The rest is in the docs, from the memory-augmented chat proxy mode to the security doc’s blunt advice: treat the server as an internal service and put a gateway in front.
Storage begins in-process with an embedded vector store and can move to Qdrant or Postgres with pgvector through one configuration change. The vector-store documentation records an important caveat. The embedded store keeps its native BM25 keyword stage, pgvector substitutes Postgres full-text search, and Qdrant omits BM25 and ranks with semantic scores plus entity boosts. Changing stores changes retrieval behaviour without producing an error.
Everything else matched, and the port is in production. Arcwell runs its memory provider on code derived from mem0-rs and vendored into the monorepo. Taranjeet Singh, Deshraj Yadav, and the mem0 contributors designed the behaviour this port preserves. The repository is small. If you read one part, make it the parity harness in bench/. I would use the same approach on the next port.