<?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.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 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>
    <item>
      <title>I Spent 3 Months Tuning a Tokio Runtime for My Robot - Here's What No Tutorial Tells You</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Fri, 17 Apr 2026 09:37:50 +0000</pubDate>
      <link>https://dev.to/motedb/i-spent-3-months-tuning-a-tokio-runtime-for-my-robot-heres-what-no-tutorial-tells-you-bdj</link>
      <guid>https://dev.to/motedb/i-spent-3-months-tuning-a-tokio-runtime-for-my-robot-heres-what-no-tutorial-tells-you-bdj</guid>
      <description>&lt;p&gt;Last November, my robot arm started dropping sensor frames at exactly 47ms intervals. Not randomly - exactly 47ms, like clockwork. It would read joint angles perfectly for a while, then miss a window, then recover. The anomaly detector we'd wired into the control loop kept triggering false positives. My teammate Rui and I spent two full weeks convinced the CAN bus driver was broken.&lt;/p&gt;

&lt;p&gt;It wasn't the driver.&lt;/p&gt;

&lt;p&gt;It was &lt;code&gt;#[tokio::main]&lt;/code&gt;.&lt;/p&gt;




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

&lt;p&gt;We were building an AI-driven robot arm that does pick-and-place with semantic understanding. The control loop needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1ms cycle time&lt;/strong&gt; for joint position updates
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10ms window&lt;/strong&gt; to fuse sensor data before inference
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background persistence&lt;/strong&gt; - log everything to a local database so we can replay sessions offline
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stack was reasonable: Rust, Tokio for async, a custom message bus, moteDB for embedded storage (vectors + time-series + structured state in one engine). We used &lt;code&gt;#[tokio::main]&lt;/code&gt; because that's what every tutorial shows, with a thread pool spawned for the heavy inference work.&lt;/p&gt;

&lt;p&gt;It worked great on my laptop. It fell apart on the robot.&lt;/p&gt;




&lt;h2&gt;
  
  
  What #[tokio::main] Actually Does (And Doesn't Do)
&lt;/h2&gt;

&lt;p&gt;Here is the thing nobody explains in the "Getting Started with Tokio" guides: &lt;code&gt;#[tokio::main]&lt;/code&gt; spins up a multi-threaded runtime with a number of worker threads equal to the number of logical CPU cores. On a modern dev machine that's 8-16. On a Raspberry Pi 5? 4 cores - and two of them are already pressured by the camera pipeline and the neural inference engine.&lt;/p&gt;

&lt;p&gt;The bigger problem: Tokio's work-stealing scheduler doesn't know anything about real-time priorities. It will cheerfully preempt your 1ms control loop task to service a database flush, a log write, or a DNS resolution that some library decided to make async under the hood.&lt;/p&gt;

&lt;p&gt;That 47ms drop? The Tokio scheduler was occasionally parking our sensor polling task while flushing a batch write to moteDB. The flush was async, perfectly polite, and completely invisible in any standard profiling tool because it showed up as I/O wait rather than CPU time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: Surgical Runtime Configuration
&lt;/h2&gt;

&lt;p&gt;Instead of &lt;code&gt;#[tokio::main]&lt;/code&gt;, we switched to:&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;fn&lt;/span&gt; &lt;span class="nf"&gt;main&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;control_rt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_multi_thread&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.worker_threads&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;.thread_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"control-loop"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.thread_priority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ThreadPriority&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// with tokio-runtime-extensions&lt;/span&gt;
        &lt;span class="nf"&gt;.build&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;io_rt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_multi_thread&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.worker_threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.thread_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"background-io"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.build&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="n"&gt;control_rt&lt;/span&gt;&lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;run_control_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;io_rt&lt;/span&gt;&lt;span class="nf"&gt;.block_on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;run_support_tasks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&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;Two runtimes. The control loop never shares a thread pool with storage I/O or inference scheduling. After this change, our 47ms drops disappeared entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Things I Wish Someone Had Told Me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;spawn_blocking&lt;/code&gt; is not free&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every call to &lt;code&gt;spawn_blocking&lt;/code&gt; steals a thread from a shared blocking thread pool (default: 512 threads). If you're calling it in a tight loop for sensor serialization, you will exhaust the pool under load. We switched to dedicated &lt;code&gt;std::thread::spawn&lt;/code&gt; for our serialization hot path and kept &lt;code&gt;spawn_blocking&lt;/code&gt; only for true one-offs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Async mutex is slower than you think at high frequency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tokio::sync::Mutex&lt;/code&gt; parks the task and hands control to the scheduler when it contends. At 1ms cycle time, this is catastrophic. For shared state between the control loop and the storage layer, we used &lt;code&gt;std::sync::Mutex&lt;/code&gt; - a blocking primitive - because the lock hold time was microseconds and the task switch overhead of the async version was larger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The database write path must not block the runtime&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where moteDB's design helped us: writes are append-only to a WAL first (sub-microsecond), with the actual B-tree / vector index update deferred to the background runtime. If your embedded database does synchronous index updates on every write, you will feel it in your control loop latency. The write path and the read path need different scheduling contracts.&lt;/p&gt;




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

&lt;p&gt;Embedded AI systems are not web servers. On a web server, a 50ms hiccup on one request is invisible to other requests. On a robot, a 50ms hiccup in your control loop is a dropped object, a wrong turn, or a crash.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;#[tokio::main]&lt;/code&gt; default was designed for web services where fairness across tasks is the right trade-off. For real-time embedded work, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolation&lt;/strong&gt;: critical tasks on dedicated runtimes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Priority&lt;/strong&gt;: OS-level thread priorities for the control loop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-blocking storage&lt;/strong&gt;: a database whose write path does not block the scheduler&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We ended up with a three-layer architecture: hard real-time control loop, soft real-time sensor fusion + inference, and best-effort persistence and telemetry. Each layer has its own Tokio runtime, and they communicate via lock-free channels (tokio::sync::mpsc with bounded capacity).&lt;/p&gt;

&lt;p&gt;The 47ms drops are gone. We have been running stable for three months.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#[tokio::main]&lt;/code&gt; is fine for most things. For embedded real-time, it is a footgun.&lt;/li&gt;
&lt;li&gt;Use separate &lt;code&gt;tokio::runtime::Builder&lt;/code&gt; instances to isolate critical paths.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;std::sync::Mutex&lt;/code&gt; beats &lt;code&gt;tokio::sync::Mutex&lt;/code&gt; when lock hold time is microseconds.&lt;/li&gt;
&lt;li&gt;Make sure your storage layer (whatever it is) has non-blocking write semantics.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Has anyone else hit scheduler interference issues in embedded Rust? I'm curious whether the community has converged on better patterns here - or whether this is still a "figure it out yourself" problem.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>iot</category>
      <category>performance</category>
      <category>rust</category>
    </item>
    <item>
      <title>I Spent 3 Months Tuning a Tokio Runtime for My Robot — Here's What No Tutorial Tells You</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Fri, 17 Apr 2026 09:31:04 +0000</pubDate>
      <link>https://dev.to/motedb/i-spent-3-months-tuning-a-tokio-runtime-for-my-robot-heres-what-no-tutorial-tells-you-4oj7</link>
      <guid>https://dev.to/motedb/i-spent-3-months-tuning-a-tokio-runtime-for-my-robot-heres-what-no-tutorial-tells-you-4oj7</guid>
      <description>&lt;p&gt;I Spent 3 Months Tuning a Tokio Runtime for My Robot — Here's What No Tutorial Tells You&lt;/p&gt;

&lt;p&gt;Last November, my robot arm started dropping sensor frames at exactly 47ms intervals. Not randomly — exactly 47ms, like clockwork. It would read joint angles perfectly for a while, then miss a window, then recover. The anomaly detector we'd wired into the control loop kept triggering false positives. My teammate Rui and I spent two full weeks convinced the CAN bus driver was broken.&lt;/p&gt;

&lt;p&gt;It wasn't the driver.&lt;/p&gt;

&lt;p&gt;It was &lt;code&gt;#[tokio::main]&lt;/code&gt;.&lt;/p&gt;




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

&lt;p&gt;We were building an AI-driven robot arm that does pick-and-place with semantic understanding. The control loop needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1ms cycle time&lt;/strong&gt; for joint position updates
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10ms window&lt;/strong&gt; to fuse sensor data before inference
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background persistence&lt;/strong&gt; — log everything to a local database so we can replay sessions offline
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stack was reasonable: Rust, Tokio for async, a custom message bus, moteDB for embedded storage (vectors + time-series + structured state in one engine). We used &lt;code&gt;#[tokio::main]&lt;/code&gt; because that's what every tutorial shows, with a thread pool spawned for the heavy inference work.&lt;/p&gt;

&lt;p&gt;It worked great on my laptop. It fell apart on the robot.&lt;/p&gt;




&lt;h2&gt;
  
  
  What #[tokio::main] Actually Does (And Doesn't Do)
&lt;/h2&gt;

&lt;p&gt;Here is the thing nobody explains in the "Getting Started with Tokio" guides: &lt;code&gt;#[tokio::main]&lt;/code&gt; spins up a multi-threaded runtime with a number of worker threads equal to the number of logical CPU cores. On a modern dev machine that's 8-16. On a Raspberry Pi 5? 4 cores — and two of them are already pressured by the camera pipeline and the neural inference engine.&lt;/p&gt;

&lt;p&gt;The bigger problem: Tokio's work-stealing scheduler doesn't know anything about real-time priorities. It will cheerfully preempt your 1ms control loop task to service a database flush, a log write, or a DNS resolution that some library decided to make async under the hood.&lt;/p&gt;

&lt;p&gt;That 47ms drop? The Tokio scheduler was occasionally parking our sensor polling task while flushing a batch write to moteDB. The flush was async, perfectly polite, and completely invisible in any standard profiling tool because it showed up as I/O wait rather than CPU time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: Surgical Runtime Configuration
&lt;/h2&gt;

&lt;p&gt;Instead of &lt;code&gt;#[tokio::main]&lt;/code&gt;, we switched to:&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;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Dedicated 1-thread runtime for the control loop&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;control_rt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_multi_thread&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.worker_threads&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;.thread_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"control-loop"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.thread_priority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ThreadPriority&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// with tokio-runtime-extensions&lt;/span&gt;
        &lt;span class="nf"&gt;.build&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="c1"&gt;// Separate runtime for background I/O (storage, logging, telemetry)&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;io_rt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_multi_thread&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.worker_threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.thread_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"background-io"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.build&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="c1"&gt;// Spawn control loop on dedicated runtime&lt;/span&gt;
    &lt;span class="n"&gt;control_rt&lt;/span&gt;&lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;run_control_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Spawn storage + inference on IO runtime&lt;/span&gt;
    &lt;span class="n"&gt;io_rt&lt;/span&gt;&lt;span class="nf"&gt;.block_on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;run_support_tasks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&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;Two runtimes. The control loop never shares a thread pool with storage I/O or inference scheduling. After this change, our 47ms drops disappeared entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Things I Wish Someone Had Told Me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;spawn_blocking&lt;/code&gt; is not free&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every call to &lt;code&gt;spawn_blocking&lt;/code&gt; steals a thread from a shared blocking thread pool (default: 512 threads). If you're calling it in a tight loop for sensor serialization, you will exhaust the pool under load. We switched to dedicated &lt;code&gt;std::thread::spawn&lt;/code&gt; for our serialization hot path and kept &lt;code&gt;spawn_blocking&lt;/code&gt; only for true one-offs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Async mutex is slower than you think at high frequency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tokio::sync::Mutex&lt;/code&gt; parks the task and hands control to the scheduler when it contends. At 1ms cycle time, this is catastrophic. For shared state between the control loop and the storage layer, we used &lt;code&gt;std::sync::Mutex&lt;/code&gt; — a blocking primitive — because the lock hold time was microseconds and the task switch overhead of the async version was larger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The database write path must not block the runtime&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where moteDB's design helped us: writes are append-only to a WAL first (sub-microsecond), with the actual B-tree / vector index update deferred to the background runtime. If your embedded database does synchronous index updates on every write, you will feel it in your control loop latency. The write path and the read path need different scheduling contracts.&lt;/p&gt;




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

&lt;p&gt;Embedded AI systems are not web servers. On a web server, a 50ms hiccup on one request is invisible to other requests. On a robot, a 50ms hiccup in your control loop is a dropped object, a wrong turn, or a crash.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;#[tokio::main]&lt;/code&gt; default was designed for web services where fairness across tasks is the right trade-off. For real-time embedded work, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolation&lt;/strong&gt;: critical tasks on dedicated runtimes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Priority&lt;/strong&gt;: OS-level thread priorities for the control loop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-blocking storage&lt;/strong&gt;: a database whose write path does not block the scheduler&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We ended up with a three-layer architecture: hard real-time control loop, soft real-time sensor fusion + inference, and best-effort persistence and telemetry. Each layer has its own Tokio runtime, and they communicate via lock-free channels (tokio::sync::mpsc with bounded capacity).&lt;/p&gt;

&lt;p&gt;The 47ms drops are gone. We have been running stable for three months.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#[tokio::main]&lt;/code&gt; is fine for most things. For embedded real-time, it is a footgun.&lt;/li&gt;
&lt;li&gt;Use separate &lt;code&gt;tokio::runtime::Builder&lt;/code&gt; instances to isolate critical paths.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;std::sync::Mutex&lt;/code&gt; beats &lt;code&gt;tokio::sync::Mutex&lt;/code&gt; when lock hold time is microseconds.&lt;/li&gt;
&lt;li&gt;Make sure your storage layer (whatever it is) has non-blocking write semantics.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Has anyone else hit scheduler interference issues in embedded Rust? I'm curious whether the community has converged on better patterns here — or whether this is still a "figure it out yourself" problem.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>performance</category>
      <category>programming</category>
      <category>rust</category>
    </item>
    <item>
      <title>I Stopped Treating My AI Agent's Memory Like a Log File</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Thu, 16 Apr 2026 11:20:00 +0000</pubDate>
      <link>https://dev.to/motedb/i-stopped-treating-my-ai-agents-memory-like-a-log-file-3fj1</link>
      <guid>https://dev.to/motedb/i-stopped-treating-my-ai-agents-memory-like-a-log-file-3fj1</guid>
      <description>&lt;p&gt;Last year, I spent two weeks debugging why my robot kept repeating the same mistake.&lt;/p&gt;

&lt;p&gt;Not a code bug. Not a hardware failure. The robot &lt;em&gt;knew&lt;/em&gt; what it had done wrong the last time. I could see it in the logs. It had stored the error. It just didn't... use that knowledge when the same situation came up again.&lt;/p&gt;

&lt;p&gt;That's when I realized I had been solving the wrong problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Log File Mental Model
&lt;/h2&gt;

&lt;p&gt;Most agent memory systems I've seen follow the same pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agent does something&lt;/li&gt;
&lt;li&gt;Store a text description of what happened&lt;/li&gt;
&lt;li&gt;Later, embed it and retrieve it with semantic search&lt;/li&gt;
&lt;li&gt;Inject retrieved context into the next prompt&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's elegant. It works well for conversational AI — the kind that lives in a chat window and helps you write emails.&lt;/p&gt;

&lt;p&gt;But I'm building robots. And the log-file model breaks in ways that aren't obvious until your robot crashes into the same wall for the third time.&lt;/p&gt;

&lt;p&gt;Here's why.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Goes Wrong on the Edge
&lt;/h2&gt;

&lt;p&gt;A robot's environment produces data that doesn't fit in a text string.&lt;/p&gt;

&lt;p&gt;When my drone hit an airflow problem near a building edge, what it experienced was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 47ms IMU reading spike (accelerometer Z-axis: +3.8g)&lt;/li&gt;
&lt;li&gt;A camera frame showing a glass surface at 0.4m&lt;/li&gt;
&lt;li&gt;A motor throttle log&lt;/li&gt;
&lt;li&gt;A GPS coordinate&lt;/li&gt;
&lt;li&gt;A vibration frequency pattern&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I stored a text note: &lt;em&gt;"Building edge caused unexpected turbulence, compensated with throttle adjustment."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two weeks later, same building edge. The agent retrieved the text note. The text said "throttle adjustment." The agent adjusted throttle. It still struggled, because the &lt;em&gt;actual&lt;/em&gt; recovery wasn't about throttle — it was about yaw correction combined with a specific altitude hold. The text summary had lost the operational precision.&lt;/p&gt;

&lt;p&gt;This is the binding problem. The memory exists. The retrieval works. But the stored representation can't carry the real-world nuance.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Memory Actually Needs to Do (For Agents That Act)
&lt;/h2&gt;

&lt;p&gt;After a lot of iteration, I've landed on a different model:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory for acting agents is not a recall system. It's a structured experience index.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-modal by default.&lt;/strong&gt; An experience in the physical world involves sensor readings, visual data, timing, and spatial context — not just a text description of what happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queryable by context, not just keywords.&lt;/strong&gt; "What did I do last time the accelerometer reading was above 3g near a glass surface?" is a different query than "what happened near buildings?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight enough to run on the device.&lt;/strong&gt; A Raspberry Pi can't afford 500ms vector search round-trips mid-flight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent across power cycles.&lt;/strong&gt; Edge AI devices reboot. Memory has to survive that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of the existing options — SQLite, Redis, Chroma, even small embedded vector stores — were designed for this combination.&lt;/p&gt;




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

&lt;p&gt;I spent about four months building &lt;a href="https://github.com/motedb/motedb" rel="noopener noreferrer"&gt;moteDB&lt;/a&gt;, a Rust-native embedded database specifically for this use case.&lt;/p&gt;

&lt;p&gt;The core design decisions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Multi-modal storage as a first-class concept.&lt;/strong&gt;&lt;br&gt;
A "record" can contain a vector, a binary blob, structured fields, and a timestamp — not an ORM abstraction on top of text columns. When the drone stores an experience, it stores all modalities together, not in separate tables that need joining later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. No runtime dependencies.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;cargo add motedb&lt;/code&gt; — that's it. It runs in the same process as your agent. No daemon, no network round-trip, no container to manage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Designed for embedded constraints.&lt;/strong&gt;&lt;br&gt;
Memory budget is explicit. Old records get evicted by policy. It doesn't assume you have 32GB of RAM or SSD storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Query by structured context.&lt;/strong&gt;&lt;br&gt;
You can ask: "Give me the 3 most similar past experiences where sensor reading X was above threshold Y and the outcome was Z." That's a spatial + vector + structured query — all in one call.&lt;/p&gt;




&lt;h2&gt;
  
  
  Does It Actually Help?
&lt;/h2&gt;

&lt;p&gt;The real test: same building edge, three months after I started using moteDB.&lt;/p&gt;

&lt;p&gt;The drone recalled the actual IMU trace from the previous incident — not a text summary of it. Its flight controller could compare the current sensor pattern directly to the stored pattern. Yaw correction happened 400ms earlier than it had previously.&lt;/p&gt;

&lt;p&gt;No crash.&lt;/p&gt;

&lt;p&gt;More importantly: I didn't have to rewrite the memory system every time the robot encountered a new type of sensor data. The schema is flexible. Text, vectors, binary — it stores what the agent actually experiences.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Broader Point
&lt;/h2&gt;

&lt;p&gt;The "agent memory = semantic text search" assumption works for chat-based agents because their world &lt;em&gt;is&lt;/em&gt; text. But if you're building agents that act in physical or structured-data environments, the mismatch compounds fast.&lt;/p&gt;

&lt;p&gt;The memory system needs to match the modality of the environment, not just the modality of the LLM interface.&lt;/p&gt;

&lt;p&gt;I'm still iterating on this. The drone problem is mostly solved. The harder one is multi-agent memory sharing — when two robots have different experiences of the same environment, how do you merge those sensibly?&lt;/p&gt;

&lt;p&gt;Working on it.&lt;/p&gt;




&lt;p&gt;If you're building agents that operate in physical or data-heavy environments, I'd be curious what memory architecture you're using. Are you hitting the same log-file limitation, or have you found something that works well?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cargo add motedb&lt;/code&gt; if you want to experiment. GitHub: &lt;a href="https://github.com/motedb/motedb" rel="noopener noreferrer"&gt;https://github.com/motedb/motedb&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rust</category>
      <category>database</category>
      <category>embedded</category>
    </item>
    <item>
      <title>I Spent 3 Months Tuning a Tokio Runtime for My Robot — Here's What No Tutorial Tells You</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Mon, 13 Apr 2026 10:20:41 +0000</pubDate>
      <link>https://dev.to/motedb/i-spent-3-months-tuning-a-tokio-runtime-for-my-robot-heres-what-no-tutorial-tells-you-5fj6</link>
      <guid>https://dev.to/motedb/i-spent-3-months-tuning-a-tokio-runtime-for-my-robot-heres-what-no-tutorial-tells-you-5fj6</guid>
      <description>&lt;p&gt;I Spent 3 Months Tuning a Tokio Runtime for My Robot — Here's What No Tutorial Tells You&lt;/p&gt;

&lt;p&gt;Last November, my robot arm started dropping sensor frames at exactly 47ms intervals. Not randomly — exactly 47ms, like clockwork. It would read joint angles perfectly for a while, then miss a window, then recover. The anomaly detector we'd wired into the control loop kept triggering false positives. My teammate Rui and I spent two full weeks convinced the CAN bus driver was broken.&lt;/p&gt;

&lt;p&gt;It wasn't the driver.&lt;/p&gt;

&lt;p&gt;It was &lt;code&gt;#[tokio::main]&lt;/code&gt;.&lt;/p&gt;




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

&lt;p&gt;We were building an AI-driven robot arm that does pick-and-place with semantic understanding. The control loop needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1ms cycle time&lt;/strong&gt; for joint position updates
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10ms window&lt;/strong&gt; to fuse sensor data before inference
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background persistence&lt;/strong&gt; — log everything to a local database so we can replay sessions offline
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stack was reasonable: Rust, Tokio for async, a custom message bus, moteDB for embedded storage (vectors + time-series + structured state in one engine). We used &lt;code&gt;#[tokio::main]&lt;/code&gt; because that's what every tutorial shows, with a thread pool spawned for the heavy inference work.&lt;/p&gt;

&lt;p&gt;It worked great on my laptop. It fell apart on the robot.&lt;/p&gt;




&lt;h2&gt;
  
  
  What #[tokio::main] Actually Does (And Doesn't Do)
&lt;/h2&gt;

&lt;p&gt;Here is the thing nobody explains in the "Getting Started with Tokio" guides: &lt;code&gt;#[tokio::main]&lt;/code&gt; spins up a multi-threaded runtime with a number of worker threads equal to the number of logical CPU cores. On a modern dev machine that's 8-16. On a Raspberry Pi 5? 4 cores — and two of them are already pressured by the camera pipeline and the neural inference engine.&lt;/p&gt;

&lt;p&gt;The bigger problem: Tokio's work-stealing scheduler doesn't know anything about real-time priorities. It will cheerfully preempt your 1ms control loop task to service a database flush, a log write, or a DNS resolution that some library decided to make async under the hood.&lt;/p&gt;

&lt;p&gt;That 47ms drop? The Tokio scheduler was occasionally parking our sensor polling task while flushing a batch write to moteDB. The flush was async, perfectly polite, and completely invisible in any standard profiling tool because it showed up as I/O wait rather than CPU time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: Surgical Runtime Configuration
&lt;/h2&gt;

&lt;p&gt;Instead of &lt;code&gt;#[tokio::main]&lt;/code&gt;, we switched to:&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;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Dedicated 1-thread runtime for the control loop&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;control_rt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_multi_thread&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.worker_threads&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;.thread_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"control-loop"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.thread_priority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ThreadPriority&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// with tokio-runtime-extensions&lt;/span&gt;
        &lt;span class="nf"&gt;.build&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="c1"&gt;// Separate runtime for background I/O (storage, logging, telemetry)&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;io_rt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_multi_thread&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.worker_threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.thread_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"background-io"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.build&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="c1"&gt;// Spawn control loop on dedicated runtime&lt;/span&gt;
    &lt;span class="n"&gt;control_rt&lt;/span&gt;&lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;run_control_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Spawn storage + inference on IO runtime&lt;/span&gt;
    &lt;span class="n"&gt;io_rt&lt;/span&gt;&lt;span class="nf"&gt;.block_on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;run_support_tasks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&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;Two runtimes. The control loop never shares a thread pool with storage I/O or inference scheduling. After this change, our 47ms drops disappeared entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Things I Wish Someone Had Told Me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;spawn_blocking&lt;/code&gt; is not free&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every call to &lt;code&gt;spawn_blocking&lt;/code&gt; steals a thread from a shared blocking thread pool (default: 512 threads). If you're calling it in a tight loop for sensor serialization, you will exhaust the pool under load. We switched to dedicated &lt;code&gt;std::thread::spawn&lt;/code&gt; for our serialization hot path and kept &lt;code&gt;spawn_blocking&lt;/code&gt; only for true one-offs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Async mutex is slower than you think at high frequency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tokio::sync::Mutex&lt;/code&gt; parks the task and hands control to the scheduler when it contends. At 1ms cycle time, this is catastrophic. For shared state between the control loop and the storage layer, we used &lt;code&gt;std::sync::Mutex&lt;/code&gt; — a blocking primitive — because the lock hold time was microseconds and the task switch overhead of the async version was larger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The database write path must not block the runtime&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where moteDB's design helped us: writes are append-only to a WAL first (sub-microsecond), with the actual B-tree / vector index update deferred to the background runtime. If your embedded database does synchronous index updates on every write, you will feel it in your control loop latency. The write path and the read path need different scheduling contracts.&lt;/p&gt;




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

&lt;p&gt;Embedded AI systems are not web servers. On a web server, a 50ms hiccup on one request is invisible to other requests. On a robot, a 50ms hiccup in your control loop is a dropped object, a wrong turn, or a crash.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;#[tokio::main]&lt;/code&gt; default was designed for web services where fairness across tasks is the right trade-off. For real-time embedded work, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolation&lt;/strong&gt;: critical tasks on dedicated runtimes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Priority&lt;/strong&gt;: OS-level thread priorities for the control loop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-blocking storage&lt;/strong&gt;: a database whose write path does not block the scheduler&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We ended up with a three-layer architecture: hard real-time control loop, soft real-time sensor fusion + inference, and best-effort persistence and telemetry. Each layer has its own Tokio runtime, and they communicate via lock-free channels (tokio::sync::mpsc with bounded capacity).&lt;/p&gt;

&lt;p&gt;The 47ms drops are gone. We have been running stable for three months.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#[tokio::main]&lt;/code&gt; is fine for most things. For embedded real-time, it is a footgun.&lt;/li&gt;
&lt;li&gt;Use separate &lt;code&gt;tokio::runtime::Builder&lt;/code&gt; instances to isolate critical paths.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;std::sync::Mutex&lt;/code&gt; beats &lt;code&gt;tokio::sync::Mutex&lt;/code&gt; when lock hold time is microseconds.&lt;/li&gt;
&lt;li&gt;Make sure your storage layer (whatever it is) has non-blocking write semantics.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Has anyone else hit scheduler interference issues in embedded Rust? I'm curious whether the community has converged on better patterns here — or whether this is still a "figure it out yourself" problem.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>embedded</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Replaced SQLite with a Rust Database in My AI Robot — Here's What Happened</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Mon, 13 Apr 2026 01:36:15 +0000</pubDate>
      <link>https://dev.to/motedb/i-replaced-sqlite-with-a-rust-database-in-my-ai-robot-heres-what-happened-1n6k</link>
      <guid>https://dev.to/motedb/i-replaced-sqlite-with-a-rust-database-in-my-ai-robot-heres-what-happened-1n6k</guid>
      <description>&lt;p&gt;I used SQLite for everything.&lt;/p&gt;

&lt;p&gt;For years, it was my default answer to "where do I store stuff." Config files, sensor logs, user data, even model outputs — SQLite handled it. When I started building an AI-powered robot that needed on-device memory, the choice felt obvious: embed SQLite, done.&lt;/p&gt;

&lt;p&gt;Three weeks later, the robot's memory was a mess of incompatible schemas, I was writing custom serialization code for every data type, and I'd accumulated roughly 400 lines of glue code just to store and retrieve things that weren't text or numbers. Image embeddings. Audio fingerprints. Sensor state vectors.&lt;/p&gt;

&lt;p&gt;That's when I started questioning whether SQLite was actually the right tool for this problem, or just the familiar one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI Agents Actually Need to Remember
&lt;/h2&gt;

&lt;p&gt;Here's the thing about robot memory that isn't obvious until you're building it: AI agents don't remember rows of data. They remember &lt;em&gt;moments&lt;/em&gt; — multimodal snapshots of state.&lt;/p&gt;

&lt;p&gt;A moment might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 512-dimensional face embedding from the camera&lt;/li&gt;
&lt;li&gt;A timestamp&lt;/li&gt;
&lt;li&gt;An associated audio clip (4KB PCM)&lt;/li&gt;
&lt;li&gt;A confidence score&lt;/li&gt;
&lt;li&gt;A label ("this is the owner")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In SQLite, storing this requires: a TEXT field for the embedding (serialized JSON or base64), a BLOB for audio, two REAL fields, and a TEXT field. Then on retrieval, you deserialize everything back. For one record, fine. For 10,000 records when you need to find the 5 most similar faces to the one in the current frame? Now you're loading blobs you don't need, deserializing vectors you'll immediately re-encode for comparison, and doing similarity math in Python one row at a time.&lt;/p&gt;

&lt;p&gt;I was making SQLite solve a problem it wasn't designed for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Performance Problem
&lt;/h2&gt;

&lt;p&gt;Let me share the actual numbers from my setup (Raspberry Pi 5, 8GB RAM):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQLite approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store a face embedding (512-dim float32 array): ~2.1ms avg&lt;/li&gt;
&lt;li&gt;Find top-5 similar faces in corpus of 1,000 records: ~340ms (full scan + Python cosine similarity)&lt;/li&gt;
&lt;li&gt;RAM overhead for 1,000 embeddings loaded for comparison: ~180MB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; 340ms for recognition is perceptible. A robot that pauses for a third of a second to recognize someone it's seen before feels broken. And 180MB just for embeddings in a device with 8GB — fine now, but what happens at 10,000 records?&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Enter moteDB
&lt;/h2&gt;

&lt;p&gt;I started working on &lt;a href="https://github.com/motedb/motedb" rel="noopener noreferrer"&gt;moteDB&lt;/a&gt; after hitting this wall. The design goal was narrow: an embedded database written in Rust that handles multimodal data natively and makes vector search a first-class operation, specifically on edge/IoT hardware.&lt;/p&gt;

&lt;p&gt;The core difference from SQLite is the data model. Instead of tables and rows, moteDB stores &lt;em&gt;fragments&lt;/em&gt; — typed data units that can be embeddings, blobs, scalars, or structured records. A "memory" is a collection of fragments with a timestamp and context metadata. Vector search operates on embedding fragments directly without deserialization.&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="c"&gt;# Cargo.toml&lt;/span&gt;
&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;motedb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.6"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;Mote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Fragment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VecQuery&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Store a face recognition moment&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;mote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Mote&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;.fragment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Fragment&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"face"&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_512d&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;.fragment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Fragment&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"audio_clip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audio_bytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;.fragment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Fragment&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.94&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;.label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;db&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;mote&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;// Find top-5 similar faces&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;results&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="nn"&gt;VecQuery&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="s"&gt;"face"&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;current_embedding&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same logical operation. No serialization. No schema definition. No glue code.&lt;/p&gt;

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

&lt;p&gt;Running the same Raspberry Pi 5 benchmarks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;moteDB approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store a face embedding: ~0.3ms avg&lt;/li&gt;
&lt;li&gt;Find top-5 similar in 1,000 records: ~8ms&lt;/li&gt;
&lt;li&gt;RAM for 1,000 embeddings active index: ~22MB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 340ms → 8ms improvement on search is the one that changed the robot's behavior. Recognition went from perceptible pause to imperceptible. The 180MB → 22MB improvement means I can scale to 10x the memory corpus before hitting constraints.&lt;/p&gt;

&lt;p&gt;Are these dramatic numbers? Yes, but the comparison is a bit unfair — SQLite is doing general-purpose relational work, moteDB is doing one specific thing. The point isn't "moteDB is faster than SQLite," it's that you're comparing apples to purpose-built apples.&lt;/p&gt;

&lt;h2&gt;
  
  
  When SQLite Is Still the Right Answer
&lt;/h2&gt;

&lt;p&gt;This isn't an "SQLite is bad" post. SQLite remains the right choice for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Config and settings storage&lt;/strong&gt; — nothing beats SQLite's simplicity for "remember this preference"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relational queries across structured data&lt;/strong&gt; — if your data is naturally tabular with joins, use SQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maximum ecosystem compatibility&lt;/strong&gt; — SQLite has bindings in every language, tooling everywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit logs and event history&lt;/strong&gt; — append-only structured records are exactly what SQLite is great at&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The switch to moteDB made sense for my use case because my data was fundamentally multimodal and similarity search was a core operation. If I'd been building a robot that needed to remember &lt;em&gt;facts&lt;/em&gt; and query them relationally ("what's the last time I saw Alice?"), SQLite would have been fine.&lt;/p&gt;

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

&lt;p&gt;I wasted three weeks because I reached for the familiar tool instead of asking what the problem actually required. SQLite is excellent — but "excellent general-purpose database" and "right tool for AI agent memory" are different things.&lt;/p&gt;

&lt;p&gt;If you're building AI systems that work with embeddings, sensor data, and multimodal inputs — especially on edge hardware — the database choice deserves more deliberate thought than it usually gets. The ecosystem defaults (SQLite, PostgreSQL with pgvector) work, but they're not optimized for this access pattern.&lt;/p&gt;

&lt;p&gt;I'm curious: what are others using for on-device AI memory storage? Have you hit similar schema/performance walls, or found ways to make SQLite work cleanly with embedding data?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;moteDB is open-source and written in Rust. If you're working on edge AI and want to try it: &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>
      <category>embedded</category>
    </item>
    <item>
      <title>Your Robot Doesn't Need a Vector Store — It Needs a Memory System</title>
      <dc:creator>mote</dc:creator>
      <pubDate>Sun, 12 Apr 2026 23:40:22 +0000</pubDate>
      <link>https://dev.to/motedb/your-robot-doesnt-need-a-vector-store-it-needs-a-memory-system-26fa</link>
      <guid>https://dev.to/motedb/your-robot-doesnt-need-a-vector-store-it-needs-a-memory-system-26fa</guid>
      <description>&lt;p&gt;Last Tuesday, our robot walked into the kitchen for the 31st time that week. Same route. Same question: "What should I prepare for lunch?"&lt;/p&gt;

&lt;p&gt;It had already asked that question. And answered it. And remembered the answer â€” for exactly 8 hours, until the vector store's TTL kicked in and silently wiped it clean.&lt;/p&gt;

&lt;p&gt;That's when I understood: vector databases are solving the wrong problem for embodied AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Semantic Search Trap
&lt;/h2&gt;

&lt;p&gt;Vector databases are brilliant at one thing: finding similar things. "Show me documents about cooking." "Find images that look like this." Great. Wonderful.&lt;/p&gt;

&lt;p&gt;But a robot standing in a kitchen isn't asking semantic questions. It's asking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Have I been here before?" (spatial + temporal)&lt;/li&gt;
&lt;li&gt;"How long has the stove been on?" (time-series)&lt;/li&gt;
&lt;li&gt;"Is this object in the same position as last time?" (structured state)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vector similarity can't answer any of these. You need a database that thinks in sequences, timestamps, and relationships â€” not just embeddings.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Robots Actually Need
&lt;/h2&gt;

&lt;p&gt;After 3 months of building memory systems for autonomous robots, here's what actually matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Temporal continuity.&lt;/strong&gt; A robot doesn't just need to know what happened. It needs to know when it happened and in what order. Did the person enter before or after the door opened? Vector stores have no concept of time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Spatial context.&lt;/strong&gt; "Kitchen" isn't a semantic cluster â€” it's a location with boundaries, objects, and states. Storing "kitchen" as a vector means you can find similar contexts, but you can't answer "has this specific kitchen been visited in the last hour?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Structured state.&lt;/strong&gt; The stove's temperature. The door's position. The battery level. These are key-value pairs that change over time and need to be queried efficiently without converting everything into vectors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Multimodal fusion.&lt;/strong&gt; Sensor data, LLM outputs, camera frames â€” all need to be stored together and queried across modalities.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hybrid Stack Problem
&lt;/h2&gt;

&lt;p&gt;The industry answer to this is usually: "Use a vector DB for embeddings + Redis for caching + InfluxDB for time-series + PostgreSQL for state."&lt;/p&gt;

&lt;p&gt;That's 4 databases. 4 connection pools. 4 failure modes. And about 50ms of latency per query on a good day.&lt;/p&gt;

&lt;p&gt;For a robot making decisions in real-time, that's not engineering â€” that's architectural debt.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Better Approach: Embedded Multimodal Storage
&lt;/h2&gt;

&lt;p&gt;The alternative is a single embedded database that handles all of these natively:&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;Database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Store&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;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Database&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_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;visit&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;.store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"visits"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"kitchen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;Utc&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="s"&gt;"robot_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"unit_7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"duration_seconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;342&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;db&lt;/span&gt;&lt;span class="nf"&gt;.store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sensors"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stove_temp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;187.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"celsius"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"last_updated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;Utc&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="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;recent&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;.store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"visits"&lt;/span&gt;&lt;span class="p"&gt;)&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;.filter&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"kitchen"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.filter&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Utc&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="o"&gt;-&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;hours&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;.first&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;No separate services. No network calls. No latency budget eaten by database round-trips.&lt;/p&gt;

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

&lt;p&gt;Vector databases are infrastructure for retrieval. Robots need infrastructure for memory.&lt;/p&gt;

&lt;p&gt;Memory isn't just "find the most similar thing." It's: what happened, when, where, and how does it affect what happens next?&lt;/p&gt;

&lt;p&gt;That's a fundamentally different data problem. And it's why the next generation of embedded databases â€” built in Rust, designed for edge AI â€” are approaching it differently.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What's your experience with robot memory systems? Do you use separate databases for different data types, or have you found a better unified approach?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>database</category>
      <category>ai</category>
      <category>robotics</category>
    </item>
  </channel>
</rss>
