Back to Blog

Why Rust for AI Memory: Performance, Safety, and a Self-Hosted Memory Server That Fits in 44 MB

An AI memory server sits on the critical path of every agent invocation. Before your model generates a single token, the memory layer must embed the query, search across potentially millions of stored memories, score and rank results, and return relevant context — all within a latency budget that users will not notice. This is not a batch job or an analytics pipeline. It is a synchronous, real-time service that runs 24/7, handling concurrent requests from dozens of agents simultaneously.

The language you choose for this workload determines your floor: the minimum latency, the minimum memory footprint, the minimum operational overhead. After evaluating Go, Python, and C++ for the Dakera engine, we chose Rust — and the decision has proven itself at every level of the stack.

~44 MB
Single binary size
87.6%
LoCoMo benchmark (1540Q)
0
External API calls for embeddings

Performance: Zero-Cost Abstractions Where It Matters

Agent memory retrieval is latency-sensitive in a way that most backend services are not. Every millisecond added to the memory lookup directly increases your agent's time-to-first-token. When an agent makes 3-5 memory recalls per conversation turn, even small per-query overhead compounds fast.

Rust's zero-cost abstractions mean that the high-level code we write — iterators, trait objects, generic data structures — compiles down to the same machine code you would write by hand in C. There is no interpreter overhead, no JIT warmup, no boxing of primitives, no hidden allocations behind convenient APIs.

No Garbage Collector Pauses

Garbage-collected languages (Python, Go, Java, C#) periodically pause all threads to reclaim unused memory. In Go, GC pauses are typically 1-3ms but can spike to 10ms+ under memory pressure. In Python, the GC can pause for 50ms+ on large heaps. For a memory server handling hundreds of concurrent queries, these pauses create unpredictable latency spikes that violate SLOs.

Rust has no garbage collector. Memory is freed deterministically when values go out of scope. The Dakera server maintains a stable p99 latency regardless of heap size or allocation rate — because there is no background process competing for CPU time to reclaim memory.

SIMD-Accelerated Distance Computations

Vector similarity search is fundamentally a distance computation problem: given a query vector, find the nearest neighbors across millions of stored vectors. Dakera's HNSW and IVF indexes use SIMD (Single Instruction, Multiple Data) intrinsics to compute cosine similarity and L2 distance across batches of vectors in parallel at the CPU register level.

// Simplified: SIMD-accelerated cosine similarity in Dakera's core
use std::arch::x86_64::*;

pub unsafe fn cosine_similarity_avx2(a: &[f32], b: &[f32]) -> f32 {
    let mut dot = _mm256_setzero_ps();
    let mut norm_a = _mm256_setzero_ps();
    let mut norm_b = _mm256_setzero_ps();

    for i in (0..a.len()).step_by(8) {
        let va = _mm256_loadu_ps(a.as_ptr().add(i));
        let vb = _mm256_loadu_ps(b.as_ptr().add(i));
        dot = _mm256_fmadd_ps(va, vb, dot);
        norm_a = _mm256_fmadd_ps(va, va, norm_a);
        norm_b = _mm256_fmadd_ps(vb, vb, norm_b);
    }
    // Horizontal sum and final division...
    hsum256(dot) / (hsum256(norm_a).sqrt() * hsum256(norm_b).sqrt())
}

On a 384-dimensional embedding (MiniLM), a single AVX2-accelerated cosine comparison takes approximately 15 nanoseconds. Rust makes it straightforward to write these intrinsics with proper safety boundaries — the unsafe block is minimal and auditable, while the surrounding code benefits from full borrow-checking and type safety.

Memory Layout Control

Rust gives us precise control over how data is laid out in memory. The HNSW graph nodes, the BM25 inverted index postings, and the vector storage are all structured for cache-line alignment and sequential access patterns. This is not an optimization you can retrofit into a language with opaque object headers and pointer-chasing GC metadata — it has to be designed in from the start.

The result: Dakera's HNSW traversal achieves 95%+ cache hit rates on hot indexes, because the graph adjacency lists are stored contiguously in memory rather than scattered across GC-managed heap allocations.

Safety: The Ownership Model Prevents Data Races

A memory server is inherently concurrent. Multiple agents query simultaneously. Background tasks consolidate memories, decay importance scores, and update knowledge graphs. The BM25 index is rebuilt while queries are being served. Write-ahead logs flush while new memories arrive.

In C or C++, this concurrency requires manual synchronization — mutexes, atomics, careful lock ordering — and a single mistake creates a data race that corrupts memory silently. In Go, race conditions are detected at runtime but not prevented at compile time. In Python, the GIL limits true parallelism entirely.

Rust's ownership and borrowing model prevents data races at compile time. The type system enforces that either multiple readers OR a single writer can access shared state — never both simultaneously. This is checked by the compiler, not by a runtime detector that might miss edge cases:

// This pattern is guaranteed race-free by the Rust compiler
use std::sync::Arc;
use tokio::sync::RwLock;

pub struct MemoryIndex {
    hnsw: Arc<RwLock<HnswGraph>>,
    bm25: Arc<RwLock<InvertedIndex>>,
    wal: Arc<Mutex<WriteAheadLog>>,
}

impl MemoryIndex {
    pub async fn recall(&self, query: &EmbeddedQuery) -> Vec<Memory> {
        // Multiple concurrent readers — no lock contention
        let hnsw = self.hnsw.read().await;
        let bm25 = self.bm25.read().await;

        let vector_results = hnsw.search(query.embedding(), 20);
        let text_results = bm25.search(query.terms(), 20);

        reciprocal_rank_fusion(vector_results, text_results)
    }

    pub async fn store(&self, memory: Memory) -> Result<()> {
        // Exclusive writer — compiler prevents concurrent reads
        let mut wal = self.wal.lock().await;
        wal.append(&memory)?;

        let mut hnsw = self.hnsw.write().await;
        hnsw.insert(memory.embedding(), memory.id());
        Ok(())
    }
}

This is not a style preference — it is a correctness guarantee. Rust's ownership model makes data races a compile-time error. The compiler physically will not produce a binary that contains a data race — not because you are careful, but because the type system encodes thread-safety rules that are checked before any code runs.

No Null Pointer Exceptions

Rust has no null. Every value that might be absent is explicitly wrapped in Option<T>, and every fallible operation returns Result<T, E>. The compiler forces you to handle both cases. This eliminates an entire class of runtime panics that plague production services in other languages — the "NoneType has no attribute" in Python, the nil pointer dereference in Go, the NullPointerException in Java.

For a memory server that must stay up continuously, this compile-time exhaustiveness checking is not a convenience — it is an operational necessity.

Binary Distribution: One File, No Runtime

Deploying a Python memory service means shipping a Python interpreter, pip-installed dependencies (often 200+ packages for ML workloads), system libraries for NumPy/SciPy, and CUDA drivers if you want GPU inference. A typical Python-based memory service Docker image is 1.5-3 GB.

Deploying Dakera means copying one file.

44 MB
Dakera binary
~80 MB
Docker image (distroless)
0
Runtime dependencies

The single binary includes everything: the HTTP/gRPC server, the ONNX inference runtime, the HNSW index, the BM25 engine, the RocksDB storage layer, the WAL, the encryption module, and the gossip-based clustering protocol. There is no separate embedding service to deploy, no Redis for caching, no Postgres for metadata, no external vector database.

# Deploy Dakera — that's it
docker pull ghcr.io/dakera-ai/dakera:latest
docker run -d \
  --name dakera \
  -p 3300:3300 \
  -p 50051:50051 \
  -v /var/lib/dakera:/data \
  -e DAKERA_API_KEY=your-key \
  ghcr.io/dakera-ai/dakera:latest

# Health check — server ready in under 1 second
curl http://localhost:3300/health
# {"status":"healthy","version":"0.11.55"}

This simplicity has operational consequences. There is one process to monitor, one log stream to ingest, one binary to update, one container to restart. When your memory layer is four microservices (embedding API + vector DB + metadata store + orchestrator), every failure mode multiplies. When it is one binary, failure modes are linear and debuggable.

Docker Image Size Comparison

Service Image size Cold start Runtime deps
Dakera (Rust) ~80 MB < 1 second None
Mem0 (Python) ~2.1 GB 8-15 seconds Redis, Qdrant, OpenAI API
Zep (Go + Python) ~900 MB 3-5 seconds Postgres, OpenAI API
Custom pgvector stack ~1.8 GB 10-20 seconds Postgres, embedding service

The ONNX Inference Story: Embeddings Without External APIs

Most memory systems outsource embedding generation to an external API — OpenAI, Cohere, or a self-hosted inference server. This creates three problems for production deployments:

  • Latency — Every store and recall operation requires a network round-trip to the embedding service (typically 50-200ms)
  • Availability — Your memory server's uptime is now bounded by the embedding API's uptime
  • Cost — At scale, embedding API costs dominate the memory system's operating budget

Dakera embeds ONNX Runtime directly into the binary. The embedding models (MiniLM-L6, BGE-small, E5-small) are loaded at startup and run inference on CPU — no GPU required, no external service, no network call. A 384-dimensional embedding is generated in 2-4ms on a single CPU core.

// Dakera's embedded inference pipeline
pub struct EmbeddingEngine {
    session: ort::Session,        // ONNX Runtime session
    tokenizer: Tokenizer,        // HuggingFace tokenizer
    dim: usize,                   // Output dimensionality (384)
}

impl EmbeddingEngine {
    pub fn embed(&self, text: &str) -> Result<Vec<f32>> {
        let tokens = self.tokenizer.encode(text)?;
        let input_ids = Array2::from_shape_vec(
            (1, tokens.len()),
            tokens.get_ids().to_vec()
        )?;

        let outputs = self.session.run(
            ort::inputs![input_ids]?
        )?;

        // Mean pooling over token embeddings
        let embeddings = outputs[0].extract_tensor::<f32>()?;
        Ok(mean_pool(embeddings, tokens.len()))
    }
}

Zero external API calls. Dakera generates embeddings, indexes vectors, runs BM25 scoring, and serves results — all within a single process. Your memory server's availability depends on exactly one thing: whether the binary is running.

This architecture means Dakera works identically in air-gapped environments, on edge devices, and behind strict firewalls. There is no phone-home, no license server, no API key for embedding generation. The models ship inside the binary (or are loaded from a local path at startup for custom models).

Durable Storage: RocksDB with Write-Ahead Logging

Memory is only useful if it persists. Dakera uses RocksDB as its storage engine — a proven LSM-tree database that handles write-heavy workloads efficiently. Every memory store operation first writes to a write-ahead log (WAL), ensuring that acknowledged writes survive process crashes and power failures.

RocksDB was chosen over alternatives for specific reasons:

  • Embedded — No separate database process to manage; the storage engine lives inside the Dakera binary
  • Write-optimized — LSM-tree architecture handles high write throughput without degrading read performance
  • Compression — LZ4/Zstd compression reduces disk usage by 60-70% for text-heavy memory workloads
  • Battle-tested — Powers Facebook's MySQL storage, CockroachDB, and TiKV in production at massive scale

On top of RocksDB, Dakera adds AES-256-GCM encryption at rest. Every memory is encrypted before it reaches the storage engine, and decrypted on read. The encryption key never touches disk — it is held in memory and provided via environment variable or secret manager at startup.

Comparison: Rust vs Python vs Go for Memory Servers

The choice of implementation language has cascading effects across every dimension of a memory server. Here is how the three most common choices compare for this specific workload:

Dimension Rust Go Python
GC pauses None (ownership model) 1-10ms typical 50ms+ on large heaps
Embedding inference Native ONNX, 2-4ms CGo bindings, 5-8ms Native Python, 3-6ms (GIL-limited)
SIMD vectorization First-class intrinsics Limited compiler auto-vectorization Via NumPy/C extensions only
Binary size 44 MB (all-inclusive) 60-80 MB typical 1.5-3 GB with dependencies
Concurrency safety Compile-time data race prevention Runtime race detector (opt-in) GIL prevents true parallelism
Memory control Byte-level layout, zero-copy GC-managed, some control via unsafe Object headers, reference counting
Cold start time < 1 second 1-2 seconds 8-20 seconds (import overhead)
Crash safety No null, exhaustive error handling Nil panics possible Runtime exceptions, untyped errors
Deployment complexity Single static binary Single binary (dynamic libc) Virtualenv + system libs + CUDA

Go is a reasonable choice for network services, and its goroutine model makes concurrency ergonomic. But for a memory server — where the hot path involves SIMD distance computations, cache-aligned data traversal, and zero-copy serialization — Go's GC and limited memory control leave performance on the table. Python excels for prototyping and ML research but is fundamentally unsuited for a latency-sensitive, always-on server process.

Multi-Node Clustering: Gossip Without Overhead

For production deployments that require high availability, Dakera supports multi-node clustering using a gossip-based protocol. Nodes discover each other, replicate memory state, and handle failover — all implemented in Rust with the same safety guarantees as the single-node path.

# Three-node HA cluster configuration
# Node 1
dakera serve \
  --cluster-name production \
  --node-id node-1 \
  --gossip-port 7946 \
  --gossip-seeds node-2:7946,node-3:7946

# Cluster status
curl http://localhost:3300/v1/cluster/status
# {"nodes":3,"healthy":3,"replication_factor":2}

The gossip protocol is lightweight: nodes exchange state digests every few seconds, and only sync deltas when divergence is detected. Under normal operation, cluster overhead is less than 1% of CPU and negligible network bandwidth. When a node fails, the remaining nodes detect the absence within seconds and redistribute query load automatically.

What This Means for Self-Hosted Deployments

The combination of Rust's performance, safety, and distribution characteristics creates a memory server that is uniquely suited to self-hosted deployment:

  • Minimal infrastructure — One binary, one port (REST on 3300, gRPC on 50051). No Redis, no Postgres, no embedding API, no queue. Your operational surface area is one process.
  • Predictable resource usage — No GC spikes, no memory fragmentation over time, no slow memory leaks from reference cycles. The process uses what it needs and nothing more.
  • Air-gapped capable — With embedded ONNX models, Dakera runs identically on an internet-connected cloud VM and on an isolated on-premise server with no egress. Zero telemetry, zero phone-home.
  • Low hardware requirements — A production Dakera instance serving 100K memories fits comfortably on a 2-core, 2 GB RAM VPS. The entire system — embeddings, indexes, storage, API — runs within that envelope.
  • Fast iteration — Sub-second cold starts mean deployments and rollbacks are instant. A docker pull and restart takes less time than most Python services spend importing their dependency tree.

Production-ready today. Dakera ships with SDKs for Python, TypeScript, Go, and Rust, a CLI, and an MCP server with 83 tools for direct integration with Claude Desktop, Cursor, and Windsurf. Pull the Docker image and have agent memory running in under 60 seconds.

# Install and verify in under a minute
docker run -d --name dakera \
  -p 3300:3300 -p 50051:50051 \
  -e DAKERA_API_KEY=your-key \
  -v dakera-data:/data \
  ghcr.io/dakera-ai/dakera:latest

# Verify: store and recall a memory
curl -s -X POST http://localhost:3300/v1/memories \
  -H "Authorization: Bearer your-key" \
  -H "Content-Type: application/json" \
  -d '{"agent_id":"test","content":"Rust is the right choice for memory servers","importance":0.95}'

curl -s -X POST http://localhost:3300/v1/memories/recall \
  -H "Authorization: Bearer your-key" \
  -H "Content-Type: application/json" \
  -d '{"agent_id":"test","query":"language choice for infrastructure","top_k":5}'

Rust is not the easiest language to write a memory server in. The borrow checker is demanding, the compile times are longer, and the ecosystem has fewer pre-built ML components than Python. But for a service that must be fast, correct, and operationally simple — that runs 24/7, handles concurrent access to shared mutable state, and ships as a single artifact with no dependencies — Rust is not just a good choice. It is the correct one.

The proof is in the numbers: 87.6% on the LoCoMo benchmark across 1540 questions, a 44 MB binary that embeds its own ONNX inference runtime, and compile-time guarantees against data races and memory corruption. That is what the right language choice buys you.

Ready to get started?

Add long-term memory to your AI agents in minutes.

Get Started Free