<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: mote</title>
    <description>The latest articles on DEV Community by mote (@motedb).</description>
    <link>https://dev.to/motedb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3796371%2Fa075a83c-f1f4-41e4-ab40-7b42a4fe6565.png</url>
      <title>DEV Community: mote</title>
      <link>https://dev.to/motedb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/motedb"/>
    <language>en</language>
    <item>
      <title>I Gave My Roombot a Memory Using Gemini Embeddings and a 900KB Rust DB</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Sun, 12 Jul 2026 07:29:49 +0000</pubDate>
      <link>https://dev.to/motedb/i-gave-my-roombot-a-memory-using-gemini-embeddings-and-a-900kb-rust-db-5dci</link>
      <guid>https://dev.to/motedb/i-gave-my-roombot-a-memory-using-gemini-embeddings-and-a-900kb-rust-db-5dci</guid>
      <description>&lt;p&gt;I have a problem. I lose things. Soldering irons, the third M3 screw, the receipt I swore I left on the desk. So this weekend I did what any reasonable person would do: I taught my roombot to remember where my stuff is.&lt;/p&gt;

&lt;p&gt;Not "store a row." Remember. The difference matters, and it's the whole reason &lt;a href="https://github.com/motedb/motedb" rel="noopener noreferrer"&gt;moteDB&lt;/a&gt; exists.&lt;/p&gt;

&lt;p&gt;Let me back up. The passion here isn't "I built a find-my-irons app." It's that I've been low-key obsessed for years with one question: why can't machines remember like we do? We walk into a room and &lt;em&gt;know&lt;/em&gt; the red box is the one we grabbed three minutes ago. Robots forget that the instant the frame buffer clears. That gap bugged me enough to spend 18 months writing a database in Rust. This weekend I finally pointed it at something stupid and fun.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build
&lt;/h2&gt;

&lt;p&gt;Hardware: a Raspberry Pi 5, a webcam, and a robot arm I assembled from a kit that Definitely Had All The Parts (it did not).&lt;/p&gt;

&lt;p&gt;The memory pipeline has two ends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google AI (Gemini)&lt;/strong&gt; turns what the camera sees into a multimodal embedding — image plus a short text caption. One vector that means "the soldering iron on the blue mat, tip facing left."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;moteDB&lt;/strong&gt; stores that embedding on-device, next to the timestamp, the pose, and a text note. No server. No cloud round-trip at query time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why this split? Gemini is genuinely good at multimodal embeddings — better than anything I can run locally on a Pi. But I don't want my robot's memory living in someone else's API. So Gemini does the &lt;em&gt;thinking&lt;/em&gt;, moteDB does the &lt;em&gt;keeping&lt;/em&gt;. The embedding gets computed once and then lives in a 900KB binary on the device.&lt;/p&gt;

&lt;p&gt;Here's the actual insert path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;motedb&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Episode&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Gemini gives us a multimodal embedding (image + caption)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;gemini&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;embed_multimodal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;GEMINI_EMBEDDING_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// gemini-embedding-001&lt;/span&gt;
    &lt;span class="n"&gt;image_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"soldering iron on blue mat, tip left"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 2. moteDB keeps it on-device, as one atomic episode&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Db&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"room_memory.motedb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="nf"&gt;.episode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"find_soldering_iron_001"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="nf"&gt;.log_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="nf"&gt;.log_scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pose_x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="nf"&gt;.log_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"last seen on the workbench"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="nf"&gt;.close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// seals + builds the local index&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, when I ask "where's my iron," the roombot snaps a frame, Gemini embeds it, and moteDB answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;gemini&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;embed_multimodal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GEMINI_EMBEDDING_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"soldering iron"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="nf"&gt;.search&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"frame"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.top_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// → "blue mat, tip left, seen 4 min ago"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The numbers, because I don't trust vibes
&lt;/h2&gt;

&lt;p&gt;Raspberry Pi 5, 8GB:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gemini embedding (image + caption) call: ~120ms over the network, once per new object&lt;/li&gt;
&lt;li&gt;moteDB store: ~0.3ms&lt;/li&gt;
&lt;li&gt;moteDB similarity search over 1,000 remembered objects: ~8ms&lt;/li&gt;
&lt;li&gt;RAM for the on-device index: ~22MB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part I actually care about: the robot recalls where something is in under 10ms &lt;em&gt;after&lt;/em&gt; the one-time embedding. No cloud dependency at query time. If my wifi dies, the roombot still knows where the iron is. That's the entire point of an embedded memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;I expected the hard part to be the embeddings. It wasn't. The hard part was &lt;em&gt;forgetting&lt;/em&gt;. My first version remembered every frame and the Pi's disk filled in an afternoon. moteDB's Episode API saved me here — I model "one search task" as one bounded, replayable episode, and I age them out by time instead of by frame count. "Remember where I put X" is naturally an episode. The robot's working memory became a stack of episodes, not an ever-growing pile of vectors.&lt;/p&gt;

&lt;p&gt;The other surprise: atomic cross-modal writes matter more than I admitted. Early on I'd insert the embedding, then crash before the text note landed, and end up with a "ghost" memory — an object the robot "remembered" with no description. moteDB's transaction boundary (&lt;code&gt;ep.close()&lt;/code&gt; commits frame + scalar + text together or rolls back) killed that class of bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Gemini embeddings cost a network call per new object. Fine for "where's my iron." Not fine for 200Hz sensor streams — there I use moteDB's local vector path without Gemini.&lt;/li&gt;
&lt;li&gt;moteDB will lose to Postgres on analytical queries. Don't use it for that. It wins on "small device, must remember, no server."&lt;/li&gt;
&lt;li&gt;The roombot still can't put the iron back. One problem at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I'm posting this
&lt;/h2&gt;

&lt;p&gt;This is my entry for DEV's Weekend Challenge: Passion Edition. The theme is "build something inspired by passion" — and honestly, giving machines a memory has been my hobby-horse for longer than I'd like to admit. Pointing Gemini's multimodal embeddings at an on-device Rust DB was the first time the idea felt real in my own apartment.&lt;/p&gt;

&lt;p&gt;If you're building anything that needs to &lt;em&gt;remember&lt;/em&gt; in the physical world — robots, drones, edge agents — what's the one thing you wish your memory layer could do that it can't today?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;moteDB is open-source, 100% Rust, and runs in ~900KB on embedded hardware. &lt;code&gt;cargo add motedb&lt;/code&gt;. GitHub: &lt;a href="https://github.com/motedb/motedb" rel="noopener noreferrer"&gt;motedb/motedb&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>database</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your AI Agent Has Amnesia: Why We Built a Database to Fix It</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Sun, 12 Jul 2026 05:31:19 +0000</pubDate>
      <link>https://dev.to/motedb/your-ai-agent-has-amnesia-why-we-built-a-database-to-fix-it-1d4n</link>
      <guid>https://dev.to/motedb/your-ai-agent-has-amnesia-why-we-built-a-database-to-fix-it-1d4n</guid>
      <description>&lt;h1&gt;
  
  
  Your AI Agent Has Amnesia: Why We Built a Database to Fix It
&lt;/h1&gt;

&lt;p&gt;If you've worked with AI coding agents for more than a weekend, you've hit this wall: the agent writes beautiful code, then forgets why it made certain design decisions the moment the session ends.&lt;/p&gt;

&lt;p&gt;The next time you open the project, the agent is starting from zero. It doesn't remember that you rejected the &lt;code&gt;async&lt;/code&gt; approach because of the runtime constraints. It doesn't know that the &lt;code&gt;User&lt;/code&gt; model was deliberately kept thin because you're migrating auth providers next sprint. It's like working with a brilliant junior developer who has a perfect memory â€” but only for the last 15 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context Window â‰&amp;nbsp; Memory
&lt;/h2&gt;

&lt;p&gt;The AI industry's answer to this has been: "just make the context window bigger." Gemini has 1M tokens. Claude has 200K. But a bigger context window isn't memory â€” it's a bigger scratchpad.&lt;/p&gt;

&lt;p&gt;Here's what gets lost between sessions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning artifacts&lt;/strong&gt;: The chain of thought that led to a decision&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rejected paths&lt;/strong&gt;: What you tried, why it failed, what to avoid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implicit constraints&lt;/strong&gt;: "Don't use &lt;code&gt;unwrap()&lt;/code&gt; here because of the FFI boundary"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-modal context&lt;/strong&gt;: The diagram the agent generated, the screenshot it analyzed, the audio note you recorded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can't cram all of that into a prompt. And even if you could, the agent doesn't &lt;em&gt;query&lt;/em&gt; it â€” it just sits there, inert, hoping the right context is somewhere in the pile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Existing Databases Don't Solve This
&lt;/h2&gt;

&lt;p&gt;We looked at the existing stack and realized: none of it was built for this problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vector databases (Pinecone, Qdrant, Weaviate)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
They're great at semantic search. But they're designed for server-side RAG pipelines, not embedded at the edge. You don't want a network call every time your robot's onboard agent needs to recall "what did I see in the previous room?" Also, they store embeddings â€” not the rich, structured reasoning artifacts an agent produces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQLite / Postgres&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Mature, reliable, works everywhere. But they're not &lt;em&gt;AI-native&lt;/em&gt;. There's no first-class concept of an embedding, a multimodal blob, or a reasoning trace. You can bolt those on with extensions (pgvector, etc.), but you're still mapping AI concepts onto a relational schema that was designed for a different era.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In-memory KV stores (Redis, etc.)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Fast, but they don't persist. And they definitely don't handle multimodal data (images, audio, sensor streams) as first-class citizens.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Agent Memory Actually Needs
&lt;/h2&gt;

&lt;p&gt;We spent months talking to teams building embodied AI (robots, drones, edge inference) and identified what agent memory actually requires:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It has to be embedded.&lt;/strong&gt; If your agent is running on-device (robot, edge server, drone), you can't round-trip to a cloud DB for every memory lookup. It needs to be in-process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It has to be multimodal.&lt;/strong&gt; Agents don't just produce text. They generate images, analyze audio, process sensor data. The memory layer needs to store and query across all of it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It needs to store &lt;em&gt;reasoning&lt;/em&gt;, not just results.&lt;/strong&gt; The most useful thing an agent can remember is not "the answer was X" â€” it's "I tried X, it failed because of Y, here's what I learned."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It needs to be fast.&lt;/strong&gt; Memory lookups should be indistinguishable from a local cache. If the agent has to wait 200ms for a memory lookup, the interaction feels broken.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It needs to be queryable by the agent.&lt;/strong&gt; Not just a dump of chat logs that a human has to read. The agent needs to be able to &lt;em&gt;search&lt;/em&gt; its own memory.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Enter moteDB
&lt;/h2&gt;

&lt;p&gt;We built &lt;strong&gt;moteDB&lt;/strong&gt; to meet these requirements. It's the first (that we know of) AI-native embedded multimodal database, written entirely in Rust.&lt;/p&gt;
&lt;h3&gt;
  
  
  What "AI-Native" Means Here
&lt;/h3&gt;

&lt;p&gt;Most databases store rows, documents, or key-value pairs. moteDB stores &lt;strong&gt;AI artifacts&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings&lt;/strong&gt; as first-class data types (not a bolt-on extension)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multimodal blobs&lt;/strong&gt; (images, audio, video frames) with automatic embedding generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning traces&lt;/strong&gt; â€” structured records of what the agent tried, what worked, what didn't&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Episodic memory&lt;/strong&gt; â€” timestamped sequences of agent actions that can be queried semantically&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Why Embedded?
&lt;/h3&gt;

&lt;p&gt;Because the use case that drove this design was &lt;strong&gt;embodied AI&lt;/strong&gt;: robots, drones, edge-deployed agents that need to remember while operating &lt;em&gt;without&lt;/em&gt; a stable network connection.&lt;/p&gt;

&lt;p&gt;If your warehouse robot is navigating between shelves and needs to remember "the north corridor is blocked by a pallet," that memory lookup has to be local, fast, and resilient to network drops. An embedded DB (like SQLite) gives you that â€” but SQLite wasn't built to handle multimodal embeddings as first-class data.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why Rust?
&lt;/h3&gt;

&lt;p&gt;Three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Memory safety without GC pauses.&lt;/strong&gt; When your agent is doing real-time inference on an edge device, a 200ms GC pause is unacceptable. Rust gives us memory safety with deterministic performance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Embedded deployment.&lt;/strong&gt; Rust compiles to a single binary with no runtime. That matters when your target is a Raspberry Pi on a drone, not a 64-core server in us-east-1.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FFI is trivial.&lt;/strong&gt; We needed moteDB to be callable from Python, C++, JavaScript, and anything else teams are using. Rust's FFI story is the best in the business.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  What Using moteDB Looks Like
&lt;/h2&gt;

&lt;p&gt;Here's the simplest possible example. You're building an agent that navigates a robot through a building:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;motedb&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AgentMemory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;AgentMemory&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"robot_mem.db"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Store a multimodal memory: image + reasoning&lt;/span&gt;
&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="nf"&gt;.store_episode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"north_corridor_blocked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Episode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;modality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;Modality&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ImageWithText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The north corridor is blocked by a pallet"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;reasoning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Tried to navigate north, hit obstacle. Label: static obstruction."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;camera_frame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Later: semantic recall&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;relevant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="nf"&gt;.recall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"how do I get to the loading dock?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;episode&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;relevant&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Remembered: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;episode&lt;/span&gt;&lt;span class="py"&gt;.reasoning&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design choice: the agent doesn't just store "answers." It stores &lt;em&gt;what it tried, what the outcome was, and why&lt;/em&gt;. That's what makes the memory reusable â€” not just a chat log dump.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Numbers (Early Benchmarks)
&lt;/h2&gt;

&lt;p&gt;We're still tuning, but early benchmarks on a Raspberry Pi 4:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embedding storage + retrieval&lt;/strong&gt;: ~3ms for top-10 similarity search across 10K embeddings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multimodal blob storage&lt;/strong&gt;: ~12MB/s write, ~45MB/s read (compressed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning trace query&lt;/strong&gt;: ~8ms for structured query across 5K episodes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory footprint&lt;/strong&gt;: ~18MB resident for a 10K-episode database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For comparison: a network round-trip to Pinecone from an edge device is typically 80-200ms. The math is pretty clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;The agent memory problem is not going away. If anything, it's about to get worse â€” because agents are moving from "helpful assistant in the browser" to "autonomous system running on-device."&lt;/p&gt;

&lt;p&gt;When that happens, the memory layer stops being a "nice to have" and becomes the difference between an agent that learns and an agent that's just expensive autocomplete.&lt;/p&gt;

&lt;p&gt;We're open-source (MIT). &lt;code&gt;cargo add motedb&lt;/code&gt; if you want to try it. Feedback â€” especially from teams building embodied AI â€” is very welcome.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;moteDB is open-source, MIT-licensed. GitHub: &lt;a href="https://github.com/motedb/motedb" rel="noopener noreferrer"&gt;https://github.com/motedb/motedb&lt;/a&gt; â€” stars, issues, and PRs all welcome.*&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>database</category>
      <category>llm</category>
    </item>
    <item>
      <title>moteDB 0.5.1 Is Out: What 18 Months of Building an Embedded Database for Robots Taught Me</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Sat, 11 Jul 2026 05:06:06 +0000</pubDate>
      <link>https://dev.to/motedb/motedb-051-is-out-what-18-months-of-building-an-embedded-database-for-robots-taught-me-hkj</link>
      <guid>https://dev.to/motedb/motedb-051-is-out-what-18-months-of-building-an-embedded-database-for-robots-taught-me-hkj</guid>
      <description>&lt;p&gt;Last Tuesday our warehouse robot forgot what a red box looked like.&lt;/p&gt;

&lt;p&gt;Not metaphorically. It had the force curve, the timestamp, the GPS pose — everything except the one thing that mattered: the visual embedding that let it recognize the same box three minutes later. The data was there. It just couldn't be &lt;em&gt;queried&lt;/em&gt; the way the task needed.&lt;/p&gt;

&lt;p&gt;That's the bug class moteDB exists to kill. And 0.5.1, released this week, is the version where I finally feel like we got the architecture right.&lt;/p&gt;

&lt;p&gt;If you've been following since the v0.1 / v0.2 days, this release is the one where a lot of the awkward scaffolding comes down. Here's what actually changed — and what I'd tell past-me if I could.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;motedb 0.5.1&lt;/code&gt; is on &lt;a href="https://crates.io/crates/motedb" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt; right now. &lt;code&gt;cargo add motedb&lt;/code&gt; or pin &lt;code&gt;motedb = "0.5.1"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's the same 100% Rust, serverless, multimodal embedded database it always was — vectors, time-series, scalar, and text in one file, one process, no daemon. What's different is &lt;em&gt;how&lt;/em&gt; the write path and the index path behave under real robot loads.&lt;/p&gt;

&lt;p&gt;The headline changes since v0.2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Streaming write pipeline&lt;/strong&gt; — writes no longer stall when the memtable flushes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental DiskANN&lt;/strong&gt; — vector indexes update online instead of requiring an offline rebuild&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic cross-modal transactions&lt;/strong&gt; — vector + scalar + time-series commit together, or roll back together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Episode Memory API&lt;/strong&gt; — a first-class primitive for "one task, bounded in time"&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Binary dropped from ~2MB to ~900KB&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let me unpack the ones that actually changed how I think.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The write path used to lie to you
&lt;/h2&gt;

&lt;p&gt;In v0.2, the two-layer design (in-memory buffer → on-disk B-tree with a &lt;code&gt;drain_lock&lt;/code&gt;) fixed the 3-hour benchmark hang. But it had a quiet flaw: when the active buffer flipped to immutable and the drain thread picked it up, &lt;strong&gt;new writes could still observe a half-drained state&lt;/strong&gt; if they raced the drain. For sensor data at 200Hz, that meant occasional "the reading exists but the column index can't see it yet" gaps.&lt;/p&gt;

&lt;p&gt;0.5.1 makes the buffer flip atomic at the WAL level. The trick is boring but effective: every write is stamped with an epoch counter, and the drain thread publishes its completed epoch &lt;em&gt;before&lt;/em&gt; marking the buffer immutable. Reads filter by &lt;code&gt;(buffer_epoch &amp;lt;= read_epoch)&lt;/code&gt;, so a query never sees a buffer that's mid-migration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// v0.5.1 — reads are epoch-bounded, never mid-drain&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;visible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="nf"&gt;.query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.scalar_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"force"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.with_epoch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read_epoch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// new in 0.5&lt;/span&gt;
    &lt;span class="nf"&gt;.execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The robot that forgot the red box? That was a read crossing a drain boundary. It doesn't happen anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. DiskANN used to be a build-time tax
&lt;/h2&gt;

&lt;p&gt;This was the thing I was most annoyed about for a year. DiskANN is the right call for edge hardware — memory-mapped, bounded query memory, no 1.5GB HNSW graph in RAM. But v0.2 only supported &lt;em&gt;offline&lt;/em&gt; index builds. Add 50k new vectors from a day of operation and you rebuilt the whole graph. On a Pi, that's hours.&lt;/p&gt;

&lt;p&gt;0.5.1 ships &lt;strong&gt;incremental DiskANN&lt;/strong&gt;. New vectors get inserted into the existing on-disk graph with local neighbor pruning; the global graph quality slowly converges as the robot operates. You pay a small recall tax during the warm-up window, then it settles.&lt;/p&gt;

&lt;p&gt;I was skeptical this would hold up under deletes (graph splits were the panic source in early testing). 0.5.1's delete path marks nodes tombstoned and re-prunes neighbors lazily during background compaction instead of mutating the live graph. That fixed the crash and kept query latency flat.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Cross-modal queries are now atomic
&lt;/h2&gt;

&lt;p&gt;The whole point of moteDB is "find the camera frames where force exceeded 2N in the last 3 seconds, ranked by similarity to this query image." That's three indexes — vector, scalar, time-series — touched in one query.&lt;/p&gt;

&lt;p&gt;In v0.2 they shared a WAL, so a crash rolled them back consistently. But a &lt;em&gt;normal&lt;/em&gt; multi-write sequence — insert vector, insert scalar, insert text — had no transaction boundary. If your process died between step 2 and 3, you had an orphaned vector with no description.&lt;/p&gt;

&lt;p&gt;0.5.1 adds &lt;code&gt;db.transaction()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="nf"&gt;.begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;b"frame_0042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.insert_scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;b"frame_0042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"force_curve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;curve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.insert_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;b"frame_0042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"desc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"red box, slightly torn corner"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// all three visible, or none&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a robot that logs 200 things per second, atomicity isn't a nice-to-have. It's the difference between "the memory is trustworthy" and "the memory is a liability."&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Episode Memory: the API I wish I'd written first
&lt;/h2&gt;

&lt;p&gt;This is the one I'm most excited about, and the most overdue.&lt;/p&gt;

&lt;p&gt;A robot's natural unit of memory isn't "a row" — it's &lt;strong&gt;an episode&lt;/strong&gt;: everything that happened between "start picking up the red box" and "placed it on the shelf." Sensors, frames, force curves, the plan, the outcome. One bounded, replayable unit.&lt;/p&gt;

&lt;p&gt;0.5.1 adds &lt;code&gt;Episode&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="nf"&gt;.episode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pick_red_box_001"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="nf"&gt;.log_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="nf"&gt;.log_scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wrist_torque"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;torque&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ... the whole task ...&lt;/span&gt;
&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="nf"&gt;.close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// seals the episode, builds its local index&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later you replay it, diff two episodes, or query "show me every episode where wrist torque spiked." It's just structured sugar over the indexes we already had — but sugar that matches how embodied AI actually thinks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;p&gt;Three things, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The WAL is the database.&lt;/strong&gt; Everything else is a cache you're allowed to lose. Spend your cleverness there first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't rebuild indexes offline if you can pay for it incrementally.&lt;/strong&gt; The "build once, ship the artifact" model felt clean. It was wrong for hardware that never stops learning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model the task, not the row.&lt;/strong&gt; Episode Memory should have been v0.1. It took me 18 months to admit that "one embedding + metadata" is the wrong grain for a robot's memory.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The numbers, honestly
&lt;/h2&gt;

&lt;p&gt;Not everything is a win, and I won't pretend it is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Binary: ~2MB → ~900KB (LTO + dead-code elimination)&lt;/li&gt;
&lt;li&gt;Mixed query (vector + range + text) p99 latency: down ~35% from v0.2 on a Pi 4B&lt;/li&gt;
&lt;li&gt;Incremental DiskANN warm-up: ~3% recall dip for the first ~10k inserts, then recovers&lt;/li&gt;
&lt;li&gt;Recall on a fully-warmed graph: still ~95%, same as offline build&lt;/li&gt;
&lt;li&gt;It will still lose to Postgres on analytical queries. Don't use it for that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo add motedb@0.5.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docs and examples are on &lt;a href="https://github.com/motedb/motedb" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. The test suite now runs 1,200+ cases under concurrency in about 4 minutes with zero hangs.&lt;/p&gt;

&lt;p&gt;If you're building anything that needs to &lt;em&gt;remember&lt;/em&gt; in the physical world — robots, drones, edge agents — I'd genuinely like to hear what your memory layer looks like today. What's the one query you wish your stack could answer that it can't?&lt;/p&gt;

</description>
      <category>rust</category>
      <category>database</category>
      <category>embedded</category>
      <category>opensource</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Wed, 08 Jul 2026 13:24:21 +0000</pubDate>
      <link>https://dev.to/motedb/-8ef</link>
      <guid>https://dev.to/motedb/-8ef</guid>
      <description></description>
    </item>
    <item>
      <title>Building a Multimodal Memory Store in Rust: What We Actually Learned</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Sun, 05 Jul 2026 05:22:12 +0000</pubDate>
      <link>https://dev.to/motedb/building-a-multimodal-memory-store-in-rust-what-we-actually-learned-4ef7</link>
      <guid>https://dev.to/motedb/building-a-multimodal-memory-store-in-rust-what-we-actually-learned-4ef7</guid>
      <description>&lt;p&gt;Why would anyone build a database in Rust?&lt;/p&gt;

&lt;p&gt;I've been asked this at least thirty times. Usually by people who think SQLite is fine, or who assume the answer involves blockchain. Neither is correct.&lt;/p&gt;

&lt;p&gt;moteDB started as a robot's memory — a place to store sensor readings, camera frames, IMU data, and navigation state in a way that could survive a crash, answer cross-modal queries, and run on a Raspberry Pi without a server. The robot's Python stack was already fighting SQLite over file locks, and the 23ms write stalls were causing visible servo jitter at 200Hz.&lt;/p&gt;

&lt;p&gt;This is the story of the technical decisions behind moteDB 0.1.6. Not the marketing version. The actual choices, the tradeoffs we made, and the things that bit us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Rust
&lt;/h2&gt;

&lt;p&gt;The short answer: because we needed a database with no runtime, no garbage collector, and a binary small enough to ship on an embedded device alongside the rest of the robot's software.&lt;/p&gt;

&lt;p&gt;The long answer is more interesting.&lt;/p&gt;

&lt;p&gt;Most databases are built in C or C++. That's a reasonable choice — you get manual memory control, mature tooling, and decades of optimization work. But C gives you exactly one thing in return for that control: the ability to get it catastrophically wrong. use-after-free, double-free, buffer overflow — these aren't edge cases in C database code. They're recurring occupational hazards.&lt;/p&gt;

&lt;p&gt;Rust eliminates that class of bugs at compile time. More importantly for an embedded context, Rust does this without a garbage collector. There's no runtime pausing your real-time control loop to clean up heap memory. The cost of safety is paid at compile time, not at runtime.&lt;/p&gt;

&lt;p&gt;There's a subtler benefit that's harder to quantify: Rust makes it safe to be aggressive with optimization. When the borrow checker is watching you, you can use &lt;code&gt;unsafe&lt;/code&gt; blocks where needed for performance, wrap them in safe abstractions, and know the compiler will catch violations. In C, "I'll be careful here" is a hope. In Rust, it's enforced.&lt;/p&gt;

&lt;p&gt;The other practical win is deployment. &lt;code&gt;cargo build --release&lt;/code&gt; gives you a single static binary. No shared libraries to manage, no Python version conflicts, no JVM startup time. On the robot, the database is a 2MB binary that starts in 3ms. That matters more than it sounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LSM-tree vs B-tree Decision
&lt;/h2&gt;

&lt;p&gt;Once we decided on Rust, the next question was the storage engine. This is where most embedded database discussions get muddled.&lt;/p&gt;

&lt;p&gt;SQLite uses a B-tree. B-trees are well-understood, read-optimized, and handle point queries efficiently. They're the right choice for most applications. They're also not ideal for write-heavy workloads with append patterns — which is exactly what sensor data looks like.&lt;/p&gt;

&lt;p&gt;Sensor data isn't random updates to existing records. It's a stream of new entries, timestamped, appended to the end of a log. B-trees need to find the right leaf page, split it if necessary, update parent pointers, and propagate changes up the tree. That's multiple disk seeks per write, even with page caching.&lt;/p&gt;

&lt;p&gt;LSM-trees (Log-Structured Merge-trees) take a different approach. Writes go to an in-memory buffer first. When the buffer fills, it gets flushed to disk as an immutable sorted run. Reads traverse the in-memory buffer and the most recent on-disk run, then older runs if needed. The key insight is that sequential writes are orders of magnitude faster than random writes on spinning disks and SSDs alike.&lt;/p&gt;

&lt;p&gt;The tradeoff: LSM-trees are read-heavy compared to B-trees. A point query might need to check the in-memory buffer, the newest run, and potentially several older runs before finding the answer. B-trees guarantee O(log n) for both reads and writes; LSM-trees give you faster writes at the cost of slower reads that span multiple data levels.&lt;/p&gt;

&lt;p&gt;For a robot, that tradeoff makes sense. Writes happen at 200Hz and need to be non-blocking. Reads are typically range queries over recent data (the last 10 seconds are hot) or point queries into specific indices. The two-tier architecture — hot in-memory buffer + cold on-disk storage — handles both patterns.&lt;/p&gt;

&lt;p&gt;Here's what the write path looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DataEntry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Write to WAL first — durability, not performance&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.wal&lt;/span&gt;&lt;span class="nf"&gt;.append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Acquire write lock on memtable (RwLock, nanosecond contention)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;memtable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.memtable&lt;/span&gt;&lt;span class="nf"&gt;.write&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Insert into in-memory BTreeMap (sorted, fast)&lt;/span&gt;
    &lt;span class="n"&gt;memtable&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. If memtable exceeds threshold, mark as immutable and spawn flush&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;memtable&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.config.memtable_size&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memtable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.trigger_flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The WAL is there because power loss mid-write is a real scenario on a robot. A sensor reading at t=5.2s is worthless if the database thinks it covers t=0 to t=5.1s. The WAL gets fsynced before we acknowledge the write; the memtable doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three-Hour Hang
&lt;/h2&gt;

&lt;p&gt;v0.1.7 had a design that looked clean on paper: one B-tree, one RwLock protecting all reads and writes. Simple. Wrong.&lt;/p&gt;

&lt;p&gt;Twelve concurrent DB instances, each running column index queries while a background thread built indexes. The background thread grabbed the write lock. Every query blocked. With twelve threads competing for one lock, the queue grew faster than it drained. The system was alive — threads running, memory allocated — but nothing was making progress. It looked like a hang. It wasn't even a deadlock.&lt;/p&gt;

&lt;p&gt;The fix took six seconds of code change and about six hours of profiling to understand.&lt;/p&gt;

&lt;p&gt;The new architecture has four components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;StorageEngine&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// In-memory buffer — writes hit here first&lt;/span&gt;
    &lt;span class="n"&gt;index_mem_buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RwLock&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BTreeMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DataEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// On-disk B-tree — reads only&lt;/span&gt;
    &lt;span class="n"&gt;generic_btree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RwLock&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GenericBTree&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// Serializes buffer-to-BTree migration&lt;/span&gt;
    &lt;span class="n"&gt;drain_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mutex&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// Tracks deleted keys to prevent resurrected data&lt;/span&gt;
    &lt;span class="n"&gt;tombstones&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RwLock&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HashSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the memory buffer exceeds its threshold, it atomically flips to an immutable snapshot. The drain thread picks it up and builds the B-tree without blocking readers. New writes hit the new active buffer. The drain lock uses &lt;code&gt;try_lock&lt;/code&gt; so writers never block — if the drain is in progress, writes just keep accumulating in the active buffer until it fills and triggers its own drain.&lt;/p&gt;

&lt;p&gt;The result: column index benchmark runtime dropped from 3+ hours to 6.6 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vector Index: The Memory Problem No One Talks About
&lt;/h2&gt;

&lt;p&gt;moteDB stores vectors alongside structured data. The naive approach — load all vectors into memory, build HNSW, serve queries — doesn't work on embedded hardware. A 1M vectors x 384 dimensions x 4 bytes = 1.5GB. Most edge devices don't have that headroom.&lt;/p&gt;

&lt;p&gt;Most vector databases use HNSW (Hierarchical Navigable Small World). HNSW is excellent — high recall, fast queries. It also builds an in-memory graph structure that scales with the dataset. For a robot running on a Raspberry Pi with 4GB RAM total, that's not viable.&lt;/p&gt;

&lt;p&gt;moteDB uses DiskANN. The core idea: build an index that can be memory-mapped and served from disk with minimal I/O per query. The index structure is designed so that a query only touches the pages it needs, in the order it needs them, with a bounded number of disk seeks.&lt;/p&gt;

&lt;p&gt;The tradeoff is indexing time. HNSW builds in minutes; DiskANN builds in hours on large datasets. But the build happens offline, on a dev machine with proper resources. The query-time memory footprint is bounded regardless of dataset size.&lt;/p&gt;

&lt;p&gt;There's a more practical tradeoff that doesn't get discussed enough: approximate vs. exact nearest neighbor. Vector indices return approximate nearest neighbors, not exact ones. For semantic search over a knowledge base, 95% recall is fine. For robot navigation or precision manufacturing, it might not be. We made the recall threshold configurable because use cases differ, and we're honest about the tradeoff in the documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Multimodal Coexistence Problem
&lt;/h2&gt;

&lt;p&gt;The hardest design problem wasn't any single index type. It was making them coexist without dragging each other down.&lt;/p&gt;

&lt;p&gt;A naive approach: separate storage engines for each modality, one for vectors, one for structured data, one for time-series. This works until you need to query across them. "Find all camera frames where the force sensor exceeded 2N within the last 3 seconds, then rank by semantic similarity to this query image" — that's a cross-modal query that needs to touch three different indexes atomically.&lt;/p&gt;

&lt;p&gt;moteDB stores all indexes independently but shares a common WAL. Each index type has its own flush and compaction logic, but they're coordinated through the same write-ahead log. If a write completes, all indexes reflect it. If power cuts mid-write, the WAL ensures all indexes roll back to a consistent state.&lt;/p&gt;

&lt;p&gt;The practical consequence: adding a new index type doesn't require rearchitecting the write path. You implement the &lt;code&gt;Index&lt;/code&gt; trait, plug it into the engine, and it participates in the WAL and flush cycle. This is how we added vector indexes, full-text search, and spatial indexes without breaking the existing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What v0.1.6 Taught Us About Allocators
&lt;/h2&gt;

&lt;p&gt;The most surprising bug in v0.1.6 had nothing to do with our code. It was in glibc.&lt;/p&gt;

&lt;p&gt;Under heavy concurrent &lt;code&gt;close()&lt;/code&gt; calls, the arena allocator would crash because malloc wasn't thread-safe in the way the code was using it. The root cause: our concurrent test suite was spawning 12 threads, each closing its own DB instance simultaneously. The glibc arena allocator partitions heap memory by thread to reduce lock contention. When many threads all hit the allocator at once, they create competing arenas that can deadlock under specific allocation patterns.&lt;/p&gt;

&lt;p&gt;The fix was simple: don't call &lt;code&gt;close()&lt;/code&gt; from multiple threads simultaneously. We added a test that specifically exercises concurrent close and verified the behavior. The fix took five minutes. Finding it took a day.&lt;/p&gt;

&lt;p&gt;This is the kind of thing that doesn't show up in single-threaded testing, doesn't appear in benchmarks, and only surfaces under the specific concurrent access pattern of a real workload. The only solution is adversarial testing: try to break your own system before your users do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Real and What Isn't
&lt;/h2&gt;

&lt;p&gt;moteDB is not a silver bullet. It's not going to outperform a tuned Postgres deployment for analytical queries. The vector index isn't going to beat Pinecone on recall. The LSM-tree isn't the right choice for a write-light, read-heavy workload.&lt;/p&gt;

&lt;p&gt;What it does is run in 2MB on an embedded device, start in 3ms, handle sensor-rate writes without blocking, and answer cross-modal queries without a server. Those constraints shaped every decision, and the tradeoffs we made reflect them.&lt;/p&gt;

&lt;p&gt;If your constraints are the same — embedded hardware, real-time write requirements, multimodal data, no server infrastructure — the architectural choices here are probably right for you too. If they're not, SQLite is probably fine.&lt;/p&gt;

&lt;p&gt;The code is on GitHub and crates.io. The v0.2.0 release (which includes the two-layer index architecture and the columnar predicate pushdown) builds cleanly with zero clippy warnings and passes 749 concurrent test cases in about 3 minutes.&lt;/p&gt;

&lt;p&gt;We'd rather you use the right tool for the job. If that's moteDB, great. If it isn't, at least now you know what the tradeoffs actually are.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>database</category>
      <category>iot</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Spent 3 Hours Watching My Benchmark Hang, Then 6 Seconds to Fix It</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Thu, 14 May 2026 13:30:36 +0000</pubDate>
      <link>https://dev.to/motedb/i-spent-3-hours-watching-my-benchmark-hang-then-6-seconds-to-fix-it-2fl9</link>
      <guid>https://dev.to/motedb/i-spent-3-hours-watching-my-benchmark-hang-then-6-seconds-to-fix-it-2fl9</guid>
      <description>

&lt;p&gt;Three hours. That's how long &lt;code&gt;bench_column_index&lt;/code&gt; ran before I realized it wasn't going anywhere.&lt;/p&gt;

&lt;p&gt;I was preparing for moteDB v0.2.0 and running the usual performance suite. Twelve DB instances in parallel, each doing &lt;code&gt;SELECT WHERE col = ?&lt;/code&gt; queries while a background thread built indexes. Queries that should take milliseconds started taking minutes. Then hours. Then nothing.&lt;/p&gt;

&lt;p&gt;The culprit was a single &lt;code&gt;RwLock&amp;lt;GenericBTree&amp;gt;&lt;/code&gt; protecting every read and write to the column index. When the background thread grabbed the write lock to bulk-insert, every query blocked. Simple as that. Twelve threads fighting over one lock.&lt;/p&gt;

&lt;p&gt;Here's what I did about it — and how I got that 3-hour hang down to &lt;strong&gt;6.6 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture That Was Killing Us
&lt;/h2&gt;

&lt;p&gt;v0.1.7 had a straightforward design: one B-Tree, one RwLock. Clean. Wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT WHERE col = ?  →  acquire read lock  →  traverse B-Tree  →  return
Background index build  →  acquire write lock  →  bulk insert  →  release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When those two paths hit the same lock simultaneously, queries queued behind the writer. With twelve instances, the queue grew faster than it drained. The system looked alive — threads were running, memory was allocated — but nothing was making progress.&lt;/p&gt;

&lt;p&gt;I needed a different model. Here's what I landed on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two-layer architecture, RocksDB style:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;IndexMemBuffer&lt;/code&gt; — an in-memory BTreeMap with parking_lot::RwLock (nanosecond-level contention). Writes go here first.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GenericBTree&lt;/code&gt; — the on-disk B-Tree. Reads through here. Writes only happen during background drain.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;drain_lock&lt;/code&gt; (Mutex) — serializes the buffer-to-B-Tree migration using &lt;code&gt;try_lock&lt;/code&gt; so writers never block.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tombscones&lt;/code&gt; (HashSet) — tracks deleted keys so drained buffers don't resurrect data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When the memory buffer exceeds a threshold, it atomically flips to an immutable snapshot. The drain thread picks it up and builds the B-Tree without blocking readers. New writes hit the new active buffer.&lt;/p&gt;

&lt;p&gt;There's also a TOCTOU fix: &lt;code&gt;get()&lt;/code&gt; holds the same lock through both the tombstone filter and the LRU cache write, eliminating the race window where a key could be deleted between the two operations.&lt;/p&gt;

&lt;p&gt;The result speaks for itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bench_column_index runtime: 3+ hours → 6.6 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three Phases of Performance Work
&lt;/h2&gt;

&lt;p&gt;Beyond the core lock contention, I spent the release cycle on three performance phases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Memory Layout
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Arc&amp;lt;DataEntry&amp;gt;&lt;/code&gt; eliminated full-row &lt;code&gt;memcpy&lt;/code&gt; on every &lt;code&gt;get()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Non-vector tables got their own BTreeMap instead of the generic wrapper, saving 24 bytes per row&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At 100K rows, that's roughly &lt;strong&gt;10MB of memory saved&lt;/strong&gt; without touching any query logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Syscall Reduction
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;DiskANN insertion path optimized&lt;/li&gt;
&lt;li&gt;SQ8Vectors persistent file handle reuse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every cache miss was triggering 2 syscalls. This phase eliminated that overhead at the I/O layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Space Index and FTS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;i-Octree now uses Morton codes for batch loading, with leaf nodes filled in order to minimize tree splitting&lt;/li&gt;
&lt;li&gt;LSM &lt;code&gt;scan_range()&lt;/code&gt; switched to streaming scan instead of materializing everything&lt;/li&gt;
&lt;li&gt;FTS switched to append-only sharded writes, with delayed merge triggering when a shard hits 5 segments&lt;/li&gt;
&lt;li&gt;Columnar predicate pushdown: decode the timestamp column first to locate rows, then decode target columns on demand — avoids decoding columns that were already filtered out&lt;/li&gt;
&lt;li&gt;Spatial query row cache + removing per-row HashMap allocation: &lt;strong&gt;8000x speedup on spatial range queries&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Audit That Found 28 Problems
&lt;/h2&gt;

&lt;p&gt;I ran three rounds of adversarial auditing before this release. I'm glad I did.&lt;/p&gt;

&lt;p&gt;The findings were... extensive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;B-Tree&lt;/strong&gt;: split leaf index out-of-bounds panic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async index pipeline&lt;/strong&gt;: double-insert causing text index panic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WAL compression + DiskANN&lt;/strong&gt;: 3 separate deadlocks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;close()&lt;/code&gt;&lt;/strong&gt;: not notifying background threads before checkpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Column/text index&lt;/strong&gt;: querying before async pipeline finished building — no fallback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SUM precision loss&lt;/strong&gt;: switched from floating-point accumulation to a two-pass compensation algorithm&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BTreeMap scan&lt;/strong&gt;: materializing all results at once causing memory spikes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primary key query&lt;/strong&gt;: index missing after restart, no fallback to scan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;glibc arena&lt;/strong&gt;: concurrent crash on explicit &lt;code&gt;db.close()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The glibc arena one was particularly fun. Under heavy concurrent &lt;code&gt;close()&lt;/code&gt; calls, the arena allocator would crash because malloc wasn't thread-safe in the way the code was using it. Fixed by not calling &lt;code&gt;close()&lt;/code&gt; from multiple threads simultaneously. Obvious in hindsight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge Devices Finally Get Love
&lt;/h2&gt;

&lt;p&gt;moteDB targets embedded and edge hardware. v0.2.0 has dedicated optimizations for that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EdgeIndexConfig&lt;/code&gt;&lt;/strong&gt;: DiskANN now has bounded memory index configuration, limiting graph memory footprint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FTS bounded shard counter + VersionStore eviction&lt;/strong&gt;: prevents memory growth during long-running operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead code cleanup&lt;/strong&gt;: removed ~2200 lines, reducing binary size&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero clippy warnings&lt;/strong&gt;: everything compiles clean&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing at Scale
&lt;/h2&gt;

&lt;p&gt;The new test infrastructure handles the concurrency edge cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;wait_for_indexes_ready()&lt;/code&gt; — polls &lt;code&gt;pending_index_batches&lt;/code&gt; atomic counter for deterministic index readiness&lt;/li&gt;
&lt;li&gt;CI adaptive data scaling — detects CI environment and automatically reduces test data volume&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;749 new test cases&lt;/strong&gt;, running under 4-thread concurrency, completing in ~3 minutes with zero hangs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;Here's the full picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;35 commits&lt;/strong&gt;, 89 source files changed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;28,118 lines added, 14,815 deleted&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;11 performance optimizations&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;21 bug fixes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3 new features&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The release is on &lt;a href="https://crates.io/crates/motedb" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt; — &lt;code&gt;cargo add motedb&lt;/code&gt; or add &lt;code&gt;motedb = "0.2.0"&lt;/code&gt; to your Cargo.toml.&lt;/p&gt;

&lt;p&gt;If you're running moteDB on edge hardware or need a database that won't stall your queries while building indexes in the background, this one's worth upgrading to.&lt;/p&gt;

&lt;p&gt;The benchmark suite no longer hangs.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Burned 3 Weeks Tuning Vector Search Before Realizing the Problem Was the Index, Not the Algorithm</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Thu, 07 May 2026 23:52:07 +0000</pubDate>
      <link>https://dev.to/motedb/i-burned-3-weeks-tuning-vector-search-before-realizing-the-problem-was-the-index-not-the-algorithm-1bpb</link>
      <guid>https://dev.to/motedb/i-burned-3-weeks-tuning-vector-search-before-realizing-the-problem-was-the-index-not-the-algorithm-1bpb</guid>
      <description>&lt;p&gt;I was getting 200ms latency on vector search with only 50,000 embeddings. For a drone that needs to recognize objects in &amp;lt;50ms, that's not a database — that's a liability.&lt;/p&gt;

&lt;p&gt;So I did what any reasonable developer would do. I spent 3 weeks tuning HNSW parameters. &lt;code&gt;ef_search&lt;/code&gt;, &lt;code&gt;M&lt;/code&gt;, &lt;code&gt;ef_construction&lt;/code&gt; — I tried every combination. I switched to IVF. I tried PQ (product quantization). I even implemented a custom filtering layer to skip low-score candidates early.&lt;/p&gt;

&lt;p&gt;Nothing moved the needle. 180ms. 190ms. 210ms if the CPU was busy with sensor fusion.&lt;/p&gt;

&lt;p&gt;Then I realized the problem wasn't the search algorithm. It was the index structure itself — and the fact that I was treating an embedded database like a server database.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup: Vector Search on a Drone
&lt;/h2&gt;

&lt;p&gt;I'm building moteDB, an embedded multi-modal database for edge AI. The use case: a drone needs to store and query:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vector embeddings&lt;/strong&gt; (image patches, for object re-identification)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-series data&lt;/strong&gt; (telemetry: altitude, GPS, battery)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State&lt;/strong&gt; (mission waypoints, current task)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All on a Raspberry Pi 4 with 8GB RAM and a heatsink that's doing its best.&lt;/p&gt;

&lt;p&gt;The vector search workload: given a query image, find the top-5 most similar patches from the last 10 minutes of flight. This is for visual odometry — if the drone loses GPS, it needs to recognize where it's been.&lt;/p&gt;

&lt;p&gt;With 50,000 embeddings (128-dimensional, float32), a brute-force search takes ~8ms on the Pi 4. That's actually fine. But I wanted to support 500,000+ embeddings (for longer missions), so I needed an index.&lt;/p&gt;

&lt;h2&gt;
  
  
  Week 1: HNSW Tuning Hell
&lt;/h2&gt;

&lt;p&gt;I started with HNSW (Hierarchical Navigable Small World), the go-to algorithm for vector search. Libraries like &lt;code&gt;hnswrs&lt;/code&gt; and &lt;code&gt;qdrant&lt;/code&gt; use it. Seemed like the right choice.&lt;/p&gt;

&lt;p&gt;My first benchmark: 200ms for a single query. That's unacceptable for a drone that needs to make control decisions at 50Hz.&lt;/p&gt;

&lt;p&gt;So I did what the internet told me to do — I tuned parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;M=16, ef_construction=200&lt;/code&gt;: 200ms&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;M=32, ef_construction=400&lt;/code&gt;: 180ms, but 3x larger index&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;M=8, ef_construction=100&lt;/code&gt;: 220ms, smaller index but slower queries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ef_search=50&lt;/code&gt;: faster (150ms) but recall dropped to 85%&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ef_search=200&lt;/code&gt;: slower (250ms) but 98% recall&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No matter what I did, I couldn't get below 150ms with &amp;gt;95% recall. And that's for a single query — in production, the drone needs to run multiple queries concurrently (object detection + visual odometry + geofence checking).&lt;/p&gt;

&lt;h2&gt;
  
  
  Week 2: Trying Other Algorithms
&lt;/h2&gt;

&lt;p&gt;At this point, I was committed to making HNSW work. But I also started questioning the choice. So I benchmarked other algorithms:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IVF (Inverted File Index)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pro: Fast query if you get the &lt;code&gt;nprobe&lt;/code&gt; right&lt;/li&gt;
&lt;li&gt;Con: Needs to be trained, and the clustering falls apart when embeddings are dynamically added (which happens on a drone in realtime)&lt;/li&gt;
&lt;li&gt;Result: 120ms with &lt;code&gt;nprobe=32&lt;/code&gt;, but recall was inconsistent (80-95% depending on data distribution)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;PQ (Product Quantization)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pro: Compresses embeddings, less memory bandwidth&lt;/li&gt;
&lt;li&gt;Con: Lossy compression, and the quantization error is unpredictable&lt;/li&gt;
&lt;li&gt;Result: 90ms with 8-bit PQ, but recall dropped to 75% — unacceptable for visual odometry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Brute-force with SIMD&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pro: Perfect recall, and &lt;code&gt;f32x4&lt;/code&gt; SIMD helps&lt;/li&gt;
&lt;li&gt;Con: O(n) scan, doesn't scale&lt;/li&gt;
&lt;li&gt;Result: 8ms for 50K vectors, but 80ms for 500K — and that's just the vector search, not including the time-series or state queries&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Week 3: The Realization
&lt;/h2&gt;

&lt;p&gt;I was staring at &lt;code&gt;perf top&lt;/code&gt; output for the 100th time when I noticed something. The CPU wasn't spending time in the HNSW graph traversal (which is what I was optimizing). It was spending time in &lt;strong&gt;page cache miss handling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every time I queried the HNSW index, the Pi had to pull graph nodes from RAM (or worse, swap to microSD). The HNSW graph was ~200MB for 500K vectors, and it was randomly accessed — terrible for cache locality.&lt;/p&gt;

&lt;p&gt;The problem wasn't the algorithm. It was the &lt;strong&gt;index access pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Did Wrong
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I treated the index like a server-side structure&lt;/strong&gt;. On a server with 64GB RAM and NVMe SSD, a 200MB randomly-accessed index is fine. The page cache handles it. On a Pi with 8GB RAM (and other processes using most of it), that same index causes page faults on every query.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I didn't account for concurrent queries&lt;/strong&gt;. HNSW is fast for a single query, but when you run 3-4 queries concurrently, they compete for memory bandwidth. The Pi 4's memory controller is not designed for this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I was storing vectors alongside the graph&lt;/strong&gt;. Every graph node stored the full 128-dimensional vector (512 bytes). That's 256MB of vectors for 500K entries, plus the graph structure. Too much for the Pi's memory.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Fix: Embedded-Aware Index Design
&lt;/h2&gt;

&lt;p&gt;I realized I needed to redesign the index for embedded constraints:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Partitioned Storage
&lt;/h3&gt;

&lt;p&gt;Instead of one global HNSW graph, I partitioned vectors by time window (10-minute buckets). Each bucket has its own small HNSW graph (~5MB for 5K vectors). Queries search the most recent N buckets (usually 3-5 for visual odometry).&lt;/p&gt;

&lt;p&gt;This fixed the cache locality problem — the active bucket's graph fits in L2 cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Vector Separation
&lt;/h3&gt;

&lt;p&gt;I separated the graph structure (which needs random access) from the vector data (which is only accessed when a candidate is promising). The graph stores only vector IDs and distances; the actual vectors are stored sequentially and accessed only for final re-ranking.&lt;/p&gt;

&lt;p&gt;This cut memory usage by 3x.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Preallocated Memory Pool
&lt;/h3&gt;

&lt;p&gt;Instead of allocating graph nodes dynamically (which causes fragmentation and unpredictable page faults), I preallocate a memory pool at database initialization. The Pi's kernel can't swap out preallocated memory as easily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before&lt;/strong&gt;: 200ms/query, 95% recall&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After&lt;/strong&gt;: 12ms/query, 97% recall&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt;: 60MB steady-state (instead of 200MB+ spiking)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;If you're building vector search for embedded/edge scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Benchmark on target hardware&lt;/strong&gt;. My initial benchmarks were on my MacBook Pro (M2, 32GB RAM). Everything looked great. On the Pi 4, it was a different story.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache locality &amp;gt; Algorithm complexity&lt;/strong&gt;. An O(n) scan with good locality can outperform O(log n) with random access if your memory is constrained.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't copy server designs to embedded&lt;/strong&gt;. HNSW is great for server-side vector search (Qdrant, Weaviate). But for embedded, you need to think about memory access patterns first, algorithm second.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Profile before optimizing&lt;/strong&gt;. I wasted 2 weeks tuning HNSW parameters when the real bottleneck was page cache misses. &lt;code&gt;perf&lt;/code&gt;, &lt;code&gt;htop&lt;/code&gt;, and &lt;code&gt;/proc/meminfo&lt;/code&gt; are your friends.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The moteDB Approach
&lt;/h2&gt;

&lt;p&gt;This experience shaped how I'm building moteDB. It's not just "a vector database" — it's a vector database designed for the constraints of embedded hardware:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LSM-tree storage&lt;/strong&gt; (not B-tree) for non-blocking writes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partitioned indexes&lt;/strong&gt; for cache locality&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preallocated memory pools&lt;/strong&gt; to avoid unpredictable allocations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-modal storage&lt;/strong&gt; (vectors + time-series + state in one engine) to avoid cross-process communication overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're working on edge AI and hitting performance walls with existing databases, I'd love to hear about your use case. The constraints are different from server-side AI, and the solutions need to be different too.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building moteDB, an open-source embedded multi-modal database for edge AI. It's 100% Rust, Apache 2.0 licensed. Check it out at &lt;a href="https://github.com/motedb" rel="noopener noreferrer"&gt;github.com/motedb&lt;/a&gt; — and if you're working on embodied AI or edge inference, I'd love to collaborate.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>database</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Announcement Google Cloud NEXT Made That Will Actually Change How Robots Work</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Sat, 25 Apr 2026 12:40:54 +0000</pubDate>
      <link>https://dev.to/motedb/the-announcement-google-cloud-next-made-that-will-actually-change-how-robots-work-4p0i</link>
      <guid>https://dev.to/motedb/the-announcement-google-cloud-next-made-that-will-actually-change-how-robots-work-4p0i</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-cloud-next-2026-04-22"&gt;Google Cloud NEXT Writing Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Everyone's Fixated on the Wrong Thing
&lt;/h2&gt;

&lt;p&gt;Google Cloud NEXT '26 dropped, and the tech press spent 48 hours writing up the Gemini Enterprise Agent Platform, the Apple partnership, and TPU v8. All deserved coverage. But the announcement that will actually change how robots work in the real world barely made the headlines.&lt;/p&gt;

&lt;p&gt;It's called &lt;strong&gt;Agent Space&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Agent Space Actually Is
&lt;/h2&gt;

&lt;p&gt;Agent Space is Google's platform for deploying AI agents that interact with the physical world — not chatbots that answer questions, but agents that maintain persistent state in dynamic environments, process sensor data, and execute feedback-driven task loops. It's Google's answer to a simple question: what if AI agents didn't just live in data centers, but were embedded in the physical world?&lt;/p&gt;

&lt;p&gt;This is the embodied AI problem. And it's fundamentally different from the chatbot problem.&lt;/p&gt;

&lt;p&gt;Most AI coverage conflates "agent" with "LLM-powered chatbot." They're not the same thing. A chatbot takes text in, produces text out. A robot takes sensor data in, produces action out — and then the world changes based on that action, which feeds back as new sensor input. That's a feedback loop. Chatbots don't have feedback loops. Robots do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Feedback Loop Changes Everything
&lt;/h2&gt;

&lt;p&gt;Here's what I've learned running AI on physical hardware: the hardest part isn't getting the model to reason. It's keeping a consistent model of the world as the world changes underneath you.&lt;/p&gt;

&lt;p&gt;Your robot moved. The map is stale. The arm reached but the object slipped. The gripper force reading is noisy. The last decision was right but the outcome was wrong because the world didn't cooperate.&lt;/p&gt;

&lt;p&gt;This is where cloud AI hits a wall. A robot running on cloud inference has latency you can't engineer around. A sensor reading arrives at time T. The query goes to the cloud. Inference runs. The command comes back at T + 150ms. Meanwhile the world moved. The faster the robot, the more useless cloud inference becomes.&lt;/p&gt;

&lt;p&gt;You need local state. You need the agent to reason about persistent, structured world models — not raw sensor dumps, but spatial facts, temporal sequences, causal relationships between actions and outcomes. And you need it at the speed of physics.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Nobody Is Writing About
&lt;/h2&gt;

&lt;p&gt;The Agent Space announcement is getting covered as "Google enters the AI agent platform race." That framing misses the interesting part. Google isn't just building another agent workflow platform — they're building infrastructure for agents that live in the real world.&lt;/p&gt;

&lt;p&gt;And if you're building agents that live in the real world, you're going to hit a wall that no amount of model improvement will solve: the data layer.&lt;/p&gt;

&lt;p&gt;The models can reason. What they can't do is efficiently store, query, and update structured representations of a changing world at the speed a robot needs. That's not a model problem. That's a database problem.&lt;/p&gt;

&lt;p&gt;I've spent two years building in this space. My drone ran cloud inference plus a flat file memory layer for the first six months. Every session felt like the robot was starting from scratch. The moment I moved to a local embedded database with structured schemas — spatial indices, temporal event logs, causal chains between actions and outcomes — the robot stopped repeating failures. Not because it got smarter. Because it finally had memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Takeaway
&lt;/h2&gt;

&lt;p&gt;Cloud AI is extraordinary at reasoning about information. Agent Space is Google's acknowledgment that the next frontier is reasoning about the physical world. These are different problems, and they require different infrastructure.&lt;/p&gt;

&lt;p&gt;The models will keep getting better. The agents will keep getting more capable. But underneath it all, the robots that actually work in production won't be the ones with the biggest models. They'll be the ones with the best data infrastructure — structured, local, real-time, and built for the speed of the physical world.&lt;/p&gt;

&lt;p&gt;Agent Space is Google betting that this matters. I think they're right.&lt;/p&gt;

&lt;p&gt;(moteDB is building the storage layer for exactly this — Rust-native, embedded, multimodal. I'm obviously biased, but I also know the problem space. If you're building anything that touches the physical world with AI, I'd want to talk.)&lt;/p&gt;

</description>
    </item>
    <item>
      <title>OpenClaw Gets Almost Everything Right — Except How It Remembers Things</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Sat, 25 Apr 2026 12:24:54 +0000</pubDate>
      <link>https://dev.to/motedb/openclaw-gets-almost-everything-right-except-how-it-remembers-things-4ofn</link>
      <guid>https://dev.to/motedb/openclaw-gets-almost-everything-right-except-how-it-remembers-things-4ofn</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/openclaw-2026-04-16"&gt;OpenClaw Writing Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;OpenClaw is genuinely impressive. Skill orchestration, MCP tool calling, autonomous agent loops — it handles all of that with less friction than anything I’ve tried. After running it on a drone project for a few weeks, I’ve come away convinced: this is what personal AI should feel like.&lt;/p&gt;

&lt;p&gt;And then the agent forgets why it woke up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Memory Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Here’s what happens in practice. Your OpenClaw agent starts a session, does useful work, stores some context. You come back the next day. The agent either has no memory of yesterday, or it has a raw transcript dump that it searches through like grep.&lt;/p&gt;

&lt;p&gt;It works — sort of. But for embodied AI, this is where things fall apart.&lt;/p&gt;

&lt;p&gt;On a robot, memory isn’t a nice-to-have. It’s physics. The agent needs to know: where was the last goal location, what obstacles appeared in the last 30 seconds, which action succeeded vs failed last time. Keyword search on a flat text dump doesn’t cut it. You need temporal queries (“did this happen in the last 5 minutes?”), spatial context (“was this object near the charger?”), and structured retrieval (“what was the last completed task?”).&lt;/p&gt;

&lt;p&gt;Most people handle this by building a RAG pipeline on top of OpenClaw. Vector embeddings, chunking strategies, similarity search. It works until you need actual structured data — and then you’re fighting your own architecture.&lt;/p&gt;

&lt;p&gt;I tried the RAG approach. Spent two days tuning chunk sizes and embedding models. The agent could find “that error from before” — most of the time. But it couldn’t answer “which task failed most recently before the restart?” That’s a one-line SQL query. Except there was no SQL database. Just a folder of markdown files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works
&lt;/h2&gt;

&lt;p&gt;The moment that changed things for me: I gave my robot a proper embedded database. Not as a separate service — as a library it links against at startup.&lt;/p&gt;

&lt;p&gt;Suddenly the agent could write structured logs: task ID, timestamp, location, outcome. It could query “last 10 successful navigation events within 3 meters of current position.” It could do this in under 2ms because the database lives on the same device, no network hop.&lt;/p&gt;

&lt;p&gt;The robot stopped repeating the same failed navigation attempt. Not because it got smarter. Because it could finally remember.&lt;/p&gt;

&lt;p&gt;This isn’t a knock on OpenClaw. The memory problem isn’t unique to OpenClaw — every AI agent framework has it. What OpenClaw gets right is the agent loop. What’s missing is the data layer underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hot Take
&lt;/h2&gt;

&lt;p&gt;OpenClaw ships with a file-based memory because files are universally accessible. That’s a reasonable default. But “universally accessible” and “actually useful for structured reasoning” are different things.&lt;/p&gt;

&lt;p&gt;If you’re running OpenClaw on anything that has to reason about the real world — a robot, a sensor rig, a drone — you’re going to hit the file memory ceiling. The ceiling is low and it comes fast.&lt;/p&gt;

&lt;p&gt;The agents that will actually work in production aren’t the ones with better prompting. They’re the ones with better data infrastructure underneath. OpenClaw is building the brain. Someone has to build the hippocampus.&lt;/p&gt;

&lt;p&gt;(moteDB is trying to be that — a Rust-native embedded multimodal DB purpose-built for exactly this kind of agent memory. Full disclosure: I work on it. But the problem is real, and I don’t think files are the answer.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’d Like to See
&lt;/h2&gt;

&lt;p&gt;OpenClaw already supports custom storage backends through MCP. That’s the right abstraction. What I’d love to see: a first-party (or blessed third-party) skill that lets agents use a structured embedded DB as memory instead of flat files.&lt;/p&gt;

&lt;p&gt;Until then: if your OpenClaw agent is running on hardware and acting confused about context, the problem probably isn’t the agent. It’s what it’s storing memories in.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Built a Database Engine in Rust for My Robot and Learned That SQLite Was the Wrong Battle</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Thu, 23 Apr 2026 00:14:30 +0000</pubDate>
      <link>https://dev.to/motedb/i-built-a-database-engine-in-rust-for-my-robot-and-learned-that-sqlite-was-the-wrong-battle-1mfh</link>
      <guid>https://dev.to/motedb/i-built-a-database-engine-in-rust-for-my-robot-and-learned-that-sqlite-was-the-wrong-battle-1mfh</guid>
      <description>&lt;p&gt;The robot started ignoring me on a Tuesday afternoon.&lt;/p&gt;

&lt;p&gt;Not dramatically — no sparks, no screaming servos. It just... stopped responding to voice commands. The fix required rebooting the onboard computer, which meant walking across the shop floor, finding the reset button, and losing twenty minutes of calibration data I'd spent all morning collecting.&lt;/p&gt;

&lt;p&gt;When I finally dug into the logs, I found the culprit: a corrupted SQLite database. The database was fine — SQLite doesn't corrupt easily. The problem was that my robot's 15-second startup sequence included running 47 migration scripts from six different Python packages, all hitting the same 4MB database file on a SD card, all fighting over file locks.&lt;/p&gt;

&lt;p&gt;I didn't need a better database. I needed a database that wasn't SQLite.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Got Wrong About "Embedded" Databases
&lt;/h2&gt;

&lt;p&gt;My first instinct was to replace SQLite with something designed for embedded systems. I spent a week evaluating LMDB, RocksDB, and LevelDB. All of them are genuinely impressive pieces of engineering. None of them solved my problem.&lt;/p&gt;

&lt;p&gt;Here's what I got wrong: I was thinking about storage as a durability problem — make writes survive crashes, make reads fast, make the whole thing resilient. That's the right framing for a server. It's the wrong framing for a robot.&lt;/p&gt;

&lt;p&gt;A robot doesn't need a database that survives crashes. It needs a database that &lt;strong&gt;doesn't crash in the first place&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What a robot actually needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write latency that doesn't spike&lt;/strong&gt; — even 10ms write stalls cause visible servo jitter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-modal queries&lt;/strong&gt; — "find all camera frames where the left obstacle sensor triggered within the last 3 seconds, and give me the IMU readings at those timestamps"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero configuration&lt;/strong&gt; — there's no startup script on a robot. The database just has to work when power comes on&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power-loss safe writes&lt;/strong&gt; — not crash recovery, just: power cuts mid-write, robot reboots, state is consistent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't the same problem. And almost no embedded database solves all four simultaneously.&lt;/p&gt;




&lt;h2&gt;
  
  
  The MMAP Trap
&lt;/h2&gt;

&lt;p&gt;The most seductive optimization in embedded storage is memory-mapping the database file directly into your address space. Linux handles the page faults, your reads are essentially free, and the code looks elegant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;OpenOptions&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"robot.db"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;mmap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nn"&gt;Mmap&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works beautifully on a server. On a robot? It's a latency landmine.&lt;/p&gt;

&lt;p&gt;When your robot's sensor loop runs at 200Hz (every 5ms), a single page fault during a read stalls the entire loop. MMAP reads are fast &lt;em&gt;on average&lt;/em&gt;. They're unpredictable &lt;em&gt;in the worst case&lt;/em&gt;. And for a real-time control system, average means nothing.&lt;/p&gt;

&lt;p&gt;I benchmarked four databases on a Raspberry Pi 4 running my robot's sensor fusion workload:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Avg Read&lt;/th&gt;
&lt;th&gt;P99 Read&lt;/th&gt;
&lt;th&gt;Write Stall&lt;/th&gt;
&lt;th&gt;Startup Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQLite (WAL)&lt;/td&gt;
&lt;td&gt;0.4ms&lt;/td&gt;
&lt;td&gt;12ms&lt;/td&gt;
&lt;td&gt;23ms&lt;/td&gt;
&lt;td&gt;140ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LMDB&lt;/td&gt;
&lt;td&gt;0.2ms&lt;/td&gt;
&lt;td&gt;0.8ms&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;td&gt;8ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RocksDB&lt;/td&gt;
&lt;td&gt;0.3ms&lt;/td&gt;
&lt;td&gt;1.1ms&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;td&gt;95ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;moteDB&lt;/td&gt;
&lt;td&gt;0.15ms&lt;/td&gt;
&lt;td&gt;0.4ms&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;td&gt;3ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The P99 read latency is the number that matters. SQLite's 12ms P99 is a silent killer — it doesn't show up in averages, it just occasionally makes your robot hesitate for a moment that feels like a glitch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building Around the Access Pattern
&lt;/h2&gt;

&lt;p&gt;The breakthrough came when I stopped trying to build a general-purpose database and started building around how a robot actually accesses data.&lt;/p&gt;

&lt;p&gt;A robot's data access has a specific shape:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Recent data is hot&lt;/strong&gt; — the last 10 seconds of sensor readings are queried constantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Historical data is cold but needs to be queryable&lt;/strong&gt; — "show me all manipulation attempts from yesterday"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured queries need to cross modalities&lt;/strong&gt; — "give me all frames where force &amp;gt; 2N and the gripper was closing"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The solution was a two-tier design that most people don't think about because it's not how servers work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Ring buffer for hot data — no fsync, no WAL, no locks&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;HotStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RingBuffer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// ~10s at 200Hz&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BTreeMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Append-only file for cold data — durable, queryable&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ColdStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BufWriter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;offset_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BTreeMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hot store never touches the filesystem during writes. Readings go into a lock-free ring buffer. Reads are direct memory accesses. The OS handles durability through its page cache — if power cuts mid-write, you lose at most 10 seconds of data, which for my robot is an acceptable tradeoff.&lt;/p&gt;

&lt;p&gt;The cold store is append-only. New readings get written to the end of a binary file. The file never gets overwritten or updated — only appended to. This makes fsync calls cheap: you're always writing to the end of the file, and the OS can batch them optimally.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cross-Modal Query Problem
&lt;/h2&gt;

&lt;p&gt;This is where things got interesting. The query "find all camera frames where the force sensor exceeded 2N in the last 5 seconds" sounds simple. It's not.&lt;/p&gt;

&lt;p&gt;The naive approach is to scan all readings and filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hot_store&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="py"&gt;.timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nf"&gt;.seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
       &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="py"&gt;.force&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;
       &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="py"&gt;.modality&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Camera&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works. It's also O(n) and blocks for 10ms+ on large result sets.&lt;/p&gt;

&lt;p&gt;The better approach is to build a time-indexed data structure that lets you skip irrelevant data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Each modality maintains its own index keyed by timestamp&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;MultiModalIndex&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BTreeMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BTreeMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;imu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BTreeMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Range query that jumps directly to relevant data&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;time_range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Range&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;modalities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Modality&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nf"&gt;Fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;SensorReading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SensorReading&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;modality&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;modalities&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;start_offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;modality&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="nf"&gt;.range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_range&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt;&lt;span class="p"&gt;)|&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start_offset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.read_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;time_range&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="py"&gt;.timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.next_offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: the BTreeMap index lets us find the start of the relevant range in O(log n), and then we read sequentially. We never touch data outside the query window.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Got Right
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Lock-free hot path.&lt;/strong&gt; The sensor loop never blocks on writes. This single decision eliminated 80% of my latency spikes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Append-only cold storage.&lt;/strong&gt; The binary format is stable (typed header + variable payload), and the file is never modified after creation. I can replay the entire history by reading the file sequentially.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typed accessors, not schema migrations.&lt;/strong&gt; Instead of ALTER TABLE migrations, I version the binary format header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[repr(u8)]&lt;/span&gt;
&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;FormatVersion&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;V1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// [timestamp: u64][force: f32][camera: bool]&lt;/span&gt;
    &lt;span class="n"&gt;V2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// [timestamp: u64][force: f32][camera: bool][gyro: [f32; 3]]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New sensor types get their own format version. Old readers skip unknown fields. No migration scripts, no schema locks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Power-cut safe by design.&lt;/strong&gt; The hot store uses a write-ahead copy. Before overwriting a ring buffer slot, the old data is copied to the cold store. This adds ~0.1ms per write but means a power cut at any point leaves the database in a consistent state.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Lesson Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Here's what I didn't find in any database comparison article:&lt;/p&gt;

&lt;p&gt;The best database for your robot isn't the one with the best benchmarks. It's the one that matches your &lt;strong&gt;failure mode&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;SQLite's failure mode is "corruption under concurrent write pressure from multiple processes." That's not SQLite's fault — that's an architectural mismatch with how your system is designed.&lt;/p&gt;

&lt;p&gt;The embedded databases that look impressive in benchmarks are often designed for a different failure mode: "crash on embedded hardware without proper shutdown." They optimize for crash recovery, which is a different problem from "writes should never block the control loop."&lt;/p&gt;

&lt;p&gt;If you're building for robots, ask yourself: what does failure look like? Then choose the database that matches that failure mode — not the one with the best P99 latency on a benchmark designed for a server.&lt;/p&gt;

&lt;p&gt;My robot doesn't crash anymore. The database never does anything interesting. That's exactly the point.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're working on robot memory systems and want to compare notes, I post updates on the moteDB project. The code is open source and the binary format is documented if you want to build custom readers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>database</category>
      <category>embedded</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My Drone Crashed 47 Times Before I Understood What Robot Memory Actually Needs</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Mon, 20 Apr 2026 15:03:46 +0000</pubDate>
      <link>https://dev.to/motedb/my-drone-crashed-47-times-before-i-understood-what-robot-memory-actually-needs-2kfa</link>
      <guid>https://dev.to/motedb/my-drone-crashed-47-times-before-i-understood-what-robot-memory-actually-needs-2kfa</guid>
      <description>&lt;p&gt;Last Tuesday, at 3 AM in a robotics lab that smelled like solder and desperation, my drone — let's call her Doris — smashed into the same wall for the 47th time.&lt;/p&gt;

&lt;p&gt;Doris was running a SLAM algorithm. Making real-time navigation decisions. In simulation, she flew beautifully. In the real world, she became a very expensive wall ornament.&lt;/p&gt;

&lt;p&gt;The algorithms weren't wrong. The motors weren't bad. The sensors were fine.&lt;/p&gt;

&lt;p&gt;The problem? Doris had no memory.&lt;/p&gt;

&lt;p&gt;Not "forgot to save" — Doris literally couldn't remember what she'd seen five seconds ago. Each moment was completely isolated. Process a frame, make a decision, next frame, fresh start. Every. Single. Time.&lt;/p&gt;

&lt;p&gt;I'm a Rust developer and the founder of moteDB. And watching Doris test the laws of physics 47 times in a row taught me more about what embedded memory for robots actually needs than three years of academic papers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI Robotics Textbooks Get Wrong
&lt;/h2&gt;

&lt;p&gt;Every robotics course talks about world models and semantic memory. Then they tell you to use Redis. Or InfluxDB. Or SQLite + Pinecone.&lt;/p&gt;

&lt;p&gt;These solutions assume three things robots don't have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reliable cloud connectivity&lt;/strong&gt; — aisle 12 has no WiFi&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tolerance for 50ms+ database latency&lt;/strong&gt; — at 3 m/s, 50ms is 15cm of blind flight&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A server-grade computer&lt;/strong&gt; — not every robot has a data center in its belly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What robots actually need is something most databases weren't designed to provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Things Robots Need to Remember
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. What they saw — Vectors
&lt;/h3&gt;

&lt;p&gt;Doris's cameras produce 512-dimensional embeddings at 30 frames per second. Over an 8-hour shift, that's 864,000 embeddings. Finding have I seen this place before? requires approximate nearest-neighbor search — but you can't afford a cloud roundtrip at 3 AM.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. What happened when — Time-Series
&lt;/h3&gt;

&lt;p&gt;Doris's motor draws spiked 340% at the same waypoint three times. That pattern matters. Without temporal context, each incident looks like a new problem. With it, you can predict and avoid.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. What they're doing right now — State
&lt;/h3&gt;

&lt;p&gt;Doris's battery was at 12%. Her next task required 18% estimated power. Without state, she couldn't make that calculation. Without persistent state, she couldn't survive a reboot.&lt;/p&gt;

&lt;p&gt;A real robot memory system has to handle all three. Most databases handle one well and duct-tape the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Number That Made Me Stop Using SQLite
&lt;/h2&gt;

&lt;p&gt;Here's what a face-recognition task looked like on my Raspberry Pi 5 with a corpus of 1,000 embeddings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Recognition Latency&lt;/th&gt;
&lt;th&gt;RAM Overhead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQLite + Python cosine sim&lt;/td&gt;
&lt;td&gt;340ms&lt;/td&gt;
&lt;td&gt;180MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;moteDB (native vectors)&lt;/td&gt;
&lt;td&gt;11ms&lt;/td&gt;
&lt;td&gt;62MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;340ms is a third of a second. A robot that pauses to recognize someone it's seen before feels broken. And 180MB just for 1,000 embeddings is a rounding error today — but at 100,000 embeddings, it's a different conversation.&lt;/p&gt;

&lt;p&gt;The bottleneck wasn't SQLite being slow. It was SQLite being the wrong abstraction for vector similarity search.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built Instead
&lt;/h2&gt;

&lt;p&gt;moteDB is an embedded multimodal database written in 100% Rust. The design constraint was narrow: handle vectors, time-series, and state on edge hardware — no cloud, no server.&lt;/p&gt;

&lt;p&gt;The core difference is the data model. Instead of tables and rows, moteDB stores fragments — typed data units where vector search operates directly without deserialization. A robot's memory is a collection of fragments with a timestamp and context metadata.&lt;/p&gt;

&lt;p&gt;Installation is cargo add motedb. The Raspberry Pi binary is under 2MB. No runtime dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Here's What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;If I were starting over, I'd have asked one question before picking any database:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when this robot loses power at 70% through a task?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your answer involves it restarts and... — you have a memory problem, not a compute problem. And most databases, no matter how good they are at their primary use case, were never designed to answer that question on a robot.&lt;/p&gt;

&lt;p&gt;Doris is flying better now. I can't say the same for my remaining wall plaster.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's the most frustrating memory-related bug you've hit in an AI or robotics project? Drop it in the comments — I read every one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rust</category>
      <category>embedded</category>
      <category>robotics</category>
    </item>
    <item>
      <title>I Tried 4 Async Runtimes on a Raspberry Pi — Only One Didn't Make Me Want to Throw It Out the Window</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Fri, 17 Apr 2026 12:08:53 +0000</pubDate>
      <link>https://dev.to/motedb/i-tried-4-async-runtimes-on-a-raspberry-pi-only-one-didnt-make-me-want-to-throw-it-out-the-window-2p77</link>
      <guid>https://dev.to/motedb/i-tried-4-async-runtimes-on-a-raspberry-pi-only-one-didnt-make-me-want-to-throw-it-out-the-window-2p77</guid>
      <description>&lt;p&gt;Last month I spent three weeks doing something that sounds simple: making an HTTP client work reliably on a Raspberry Pi 4 running a custom Rust service. The service needed to periodically sync sensor data to a cloud endpoint while also handling local database writes. Nothing fancy — maybe 200 lines of logic.&lt;/p&gt;

&lt;p&gt;It took me 2,847 lines of code, 4 different async runtimes, and one very close relationship with my debugger to get it working.&lt;/p&gt;

&lt;p&gt;Here's what actually happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1: Tokio — The Standard Choice
&lt;/h2&gt;

&lt;p&gt;Everyone says "just use Tokio." So I did.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;tokio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"full"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my MacBook, it compiled in 12 seconds and ran perfectly. On the Raspberry Pi? Cross-compilation worked, but the binary was 8.3 MB. For a service that was supposed to be lean and embeddable, that felt wrong.&lt;/p&gt;

&lt;p&gt;But the real problem was memory. Under load (simulating 50 concurrent sensor readings + database writes), the RSS crept up to 45 MB. On a Pi with 4 GB of RAM running other services, that's not catastrophic, but it's not great either.&lt;/p&gt;

&lt;p&gt;The worst part: I needed a specific timer implementation that played nice with the Pi's real-time clock, and Tokio's &lt;code&gt;time&lt;/code&gt; module had a subtle drift that accumulated over 24 hours. We're talking milliseconds becoming seconds. When you're timestamping sensor events, that matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Works, but it's like using a sledgehammer to hang a picture frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 2: async-std — The Alternative
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;async-std&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"attributes"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;async-std felt more ergonomic. The API is closer to what you'd expect from Rust's standard library. File I/O felt more natural. The binary was slightly smaller (7.1 MB).&lt;/p&gt;

&lt;p&gt;But then I hit the wall: async-std's networking stack had a bug with DNS resolution on ARM64 that caused a hang every ~6 hours. I found an open issue from 18 months ago with 47 upvotes and no resolution.&lt;/p&gt;

&lt;p&gt;I tried patching it myself. That's when I realized I'd rather rewrite the whole thing than debug someone else's async DNS resolver.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Promising, but production-unsafe on ARM for anything long-running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 3: smol — The Minimalist
&lt;/h2&gt;

&lt;p&gt;smol is beautiful in its simplicity. Small binary (4.2 MB), low memory footprint (22 MB RSS under the same load), and the &lt;code&gt;async-io&lt;/code&gt; crate underneath is surprisingly robust.&lt;/p&gt;

&lt;p&gt;The problem? Dependency hell. smol uses &lt;code&gt;blocking&lt;/code&gt; for sync-to-async bridging, and our database library (SQLite, via &lt;code&gt;rusqlite&lt;/code&gt;) kept deadlocking in subtle ways when called from multiple async tasks. The &lt;code&gt;blocking&lt;/code&gt; crate's thread pool would exhaust, and then... silence. No error, no panic. Just a service that stopped responding.&lt;/p&gt;

&lt;p&gt;I spent two days adding timeout wrappers around every database call before I gave up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Perfect if you control every dependency. We didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 4: embassy — The Embedded Champion
&lt;/h2&gt;

&lt;p&gt;This is where things got interesting. Embassy isn't really an async runtime in the traditional sense — it's an async framework designed for &lt;code&gt;no_std&lt;/code&gt; embedded systems.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;embassy-executor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.6"&lt;/span&gt;
&lt;span class="py"&gt;embassy-time&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.3"&lt;/span&gt;
&lt;span class="py"&gt;embassy-net&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait, can you even run Embassy on a Raspberry Pi? Technically, Embassy targets microcontrollers (STM32, nRF, ESP32). But the networking and I/O abstractions work on Linux too, thanks to &lt;code&gt;embassy-net&lt;/code&gt;'s socket backend.&lt;/p&gt;

&lt;p&gt;The binary was 2.8 MB. Memory usage stayed flat at 15 MB under load. The timer was rock-solid (it uses the hardware timer abstraction, and on Linux it maps to the appropriate clock source).&lt;/p&gt;

&lt;p&gt;There was one catch: the learning curve. Embassy's model is fundamentally different. You don't spawn tasks like Tokio — you use &lt;code&gt;Spawner&lt;/code&gt; and &lt;code&gt;embassy_executor::main&lt;/code&gt;. The networking API expects you to think in terms of &lt;code&gt;TcpSocket&lt;/code&gt; objects rather than streams. It took me a full day to restructure the code.&lt;/p&gt;

&lt;p&gt;But once it compiled? It just worked. No memory leaks, no timer drift, no DNS hangs, no thread pool deadlocks. 72 hours of continuous testing without a single hiccup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[embassy_executor::main]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spawner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Spawner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;embassy_net&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;net_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;spawner&lt;/span&gt;&lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sensor_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;spawner&lt;/span&gt;&lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sync_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Hard Lesson
&lt;/h2&gt;

&lt;p&gt;Here's what I wish someone had told me before I started:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Binary size matters on edge devices.&lt;/strong&gt; 8 MB vs 2.8 MB isn't just a number — it's the difference between fitting in a constrained update partition and failing deployment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Timer accuracy is a silent killer.&lt;/strong&gt; Most people don't notice until they're correlating events across devices and the timestamps don't line up.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Standard" runtimes aren't optimized for your hardware.&lt;/strong&gt; Tokio is amazing for servers. It's not optimized for a $35 ARM board with eMMC storage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The ecosystem lock-in is real.&lt;/strong&gt; Your choice of async runtime determines which libraries you can use, how you handle errors, and what your deployment looks like.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I'm Using Now
&lt;/h2&gt;

&lt;p&gt;For our robotics work at moteDB, we ended up with a hybrid: Embassy for the embedded layer (sensor I/O, real-time control), and a minimal synchronous Rust core for database operations. We intentionally avoided async in the database layer — synchronous code with a dedicated thread is simpler, more debuggable, and has predictable performance characteristics.&lt;/p&gt;

&lt;p&gt;Sometimes the best async architecture includes knowing when NOT to be async.&lt;/p&gt;

&lt;p&gt;Has anyone else run into the async runtime choice problem on constrained hardware? I'm curious if there are other options I missed — especially anything that bridges the gap between Tokio's ecosystem and Embassy's efficiency.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>embedded</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
