<?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: Bhargav </title>
    <description>The latest articles on DEV Community by Bhargav  (@bharqav).</description>
    <link>https://dev.to/bharqav</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4090971%2F357ff43e-9933-4d14-8ccd-f350ea1ef50d.png</url>
      <title>DEV Community: Bhargav </title>
      <link>https://dev.to/bharqav</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bharqav"/>
    <language>en</language>
    <item>
      <title>What Actually Happens When You Press Enter in a Terminal</title>
      <dc:creator>Bhargav </dc:creator>
      <pubDate>Sun, 20 Sep 2026 09:02:34 +0000</pubDate>
      <link>https://dev.to/bharqav/what-actually-happens-when-you-press-enter-in-a-terminal-5eil</link>
      <guid>https://dev.to/bharqav/what-actually-happens-when-you-press-enter-in-a-terminal-5eil</guid>
      <description>&lt;p&gt;What is&amp;nbsp;mysh?&lt;br&gt;
A small POSIX-style shell. It's not a Bash replacement, and I'll be upfront about that. It's a working reference for how a shell turns raw text into running processes.&lt;br&gt;
What it handles today:&lt;br&gt;
Pipes and redirections: |, &amp;lt;, &amp;gt;, &amp;gt;&amp;gt;, and heredocs with &amp;lt;&amp;lt;&lt;br&gt;
Logical operators: &amp;amp;&amp;amp;, ||, and&amp;nbsp;; with correct precedence&lt;br&gt;
Subshells with parentheses&lt;br&gt;
Background jobs with &amp;amp;, plus jobs, fg, and bg&lt;br&gt;
Builtins: cd, pwd, echo, export, unset, env, exit, help&lt;br&gt;
Environment variable expansion like $VAR&lt;/p&gt;

&lt;p&gt;Under the hood, it's four stages: read a line, lex it into tokens, parse it into a tree, execute the tree with fork, execvp, pipe, dup2 and waitpid.&lt;br&gt;
Simple on paper. The interesting part is what each stage has to get right.&lt;/p&gt;




&lt;p&gt;How It Actually&amp;nbsp;Works&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Lexer: Turning Text Into&amp;nbsp;Tokens
The lexer reads your line and produces tokens: words, pipes, redirects, logical operators, parentheses, the background &amp;amp;.
It also expands environment variables at this stage. So mkdir $TARGET becomes mkdir build before the parser ever sees it.
This sounds trivial until you type echo "a | b". That pipe is not a pipe. It's a character inside a string. The lexer has to know the difference, and that's where a lot of the subtle bugs in any shell live.&lt;/li&gt;
&lt;li&gt;The Parser: Precedence Is Everything
I used a recursive-descent parser. Each level of precedence gets its own function:
Parse OR expressions
Parse AND expressions
Parse grouped terms in parentheses
Parse pipelines and their redirections&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is an expression tree with three kinds of nodes: Pipeline, And, and Or.&lt;br&gt;
Why bother with a tree? Because of lines like this:&lt;br&gt;
mkdir build &amp;amp;&amp;amp; cd build || echo "failed"&lt;br&gt;
Without correct precedence, this means the wrong thing. With a tree, &amp;amp;&amp;amp; binds tighter than ||, parentheses override both, and evaluation order just falls out of the structure. No special cases.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Executor: Where Processes Get Wired&amp;nbsp;Together
This is the part everyone remembers from an OS class and nobody remembers how to write.
For ls | grep cpp, the executor does roughly this:
cpp
int fds[2];
pipe(fds);
pid_t left = fork();
if (left == 0) {
dup2(fds[1], STDOUT_FILENO);  // stdout goes into the pipe
close(fds[0]);
close(fds[1]);
execvp(left_argv[0], left_argv.data());
_exit(127);
}
pid_t right = fork();
if (right == 0) {
dup2(fds[0], STDIN_FILENO);   // stdin comes from the pipe
close(fds[0]);
close(fds[1]);
execvp(right_argv[0], right_argv.data());
_exit(127);
}
close(fds[0]);
close(fds[1]);
waitpid(left, nullptr, 0);
waitpid(right, nullptr, 0);
That's the simplified version, but it's the whole idea. A pipe is just two file descriptors. dup2 swaps them into the places the child expects. execvp replaces the child with the real program, which has no idea any of this happened.
Notice all those close calls. Hold that thought.&lt;/li&gt;
&lt;li&gt;Builtins: Why cd Can't Be a&amp;nbsp;Program
Here's a question that sounds like a trick. Why is cd a builtin instead of a program like ls?
Because a child process can't change its parent's working directory. If cd ran in a forked child, it would change directory, exit, and leave your shell exactly where it started.
So mysh splits builtins into two cases:
A standalone builtin runs in the parent process. If it has a redirection, the shell temporarily remaps its own stdio, runs the builtin, then restores the original descriptors.
A builtin inside a pipeline runs in a child. Stateful ones like cd, export, unset, fg, bg and exit fail explicitly there, because changing state in a throwaway process is pointless and confusing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Real shells have some nuance here, but this model is easy to reason about and easy to test.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Signals and Job&amp;nbsp;Control
Press Ctrl+C in a shell and the running command dies, but the shell itself survives. That's not automatic.
mysh handles SIGINT so the prompt loop stays alive, and ignores SIGQUIT. Child processes reset both to their defaults right before exec, so the programs you launch behave normally.
For background jobs, each &amp;amp; pipeline gets its own process group and an entry in a job table. jobs lists them, fg brings one forward, bg resumes one.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;What Broke&lt;br&gt;
Building a shell is mostly a list of ways you can hang your terminal.&lt;br&gt;
Bug 1: The pipe that never closed.&lt;br&gt;
&amp;nbsp;If you forget to close the unused ends of a pipe in any process, the reader never sees end-of-file. grep just sits there waiting for input that will never come, because some process, possibly the shell itself, still holds the write end open. This is the classic one. Every close in the snippet above exists because of it.&lt;br&gt;
Bug 2: Builtins with redirections.&lt;br&gt;
&amp;nbsp;Running echo hi &amp;gt; file as a standalone builtin means the parent temporarily redirects its own stdout. If you forget to restore it, or bail out early on an error path, your shell prints into a file for the rest of the session. The fix is to save the originals with dup first and restore them on every exit path.&lt;br&gt;
Bug 3: cd inside a pipeline.&lt;br&gt;
&amp;nbsp;cd /tmp | cat looks like it should work. It doesn't, and shouldn't. Deciding to fail loudly instead of silently doing nothing was a design choice I had to make on purpose.&lt;br&gt;
Bug 4: Ctrl+C killing the shell.&lt;br&gt;
&amp;nbsp;Before signal handling was set up, interrupting a slow command took the whole shell down with it. Then I fixed that, and children started ignoring Ctrl+C too, because they inherited the shell's signal settings. Resetting them before exec fixed the second problem.&lt;/p&gt;




&lt;p&gt;How I Tested&amp;nbsp;It&lt;br&gt;
A shell has a huge surface area, so testing had to be layered:&lt;br&gt;
Unit tests for the lexer, parser, and tree logic (make unit)&lt;br&gt;
End-to-end smoke tests that run the real shell and check stdout and stderr (make test)&lt;br&gt;
A wider regression suite for edge cases (make regression)&lt;br&gt;
AddressSanitizer and UBSan runs to catch memory bugs (make asan-check)&lt;br&gt;
A single make check that runs the whole CI pipeline locally&lt;/p&gt;

&lt;p&gt;The sanitizer runs were worth it. Shells juggle a lot of raw memory and file descriptors, and that's exactly where silent corruption hides.&lt;/p&gt;




&lt;p&gt;Why You Should Build&amp;nbsp;One&lt;br&gt;
If you write backend code, you live in a shell. Building one changes how you see everything around it:&lt;br&gt;
Pipes stop being magic. They're two file descriptors and a dup2.&lt;br&gt;
You understand why cd and export are builtins.&lt;br&gt;
You can finally explain what a zombie process is and why waitpid matters.&lt;br&gt;
Shell scripts and their weird quoting rules start making sense.&lt;br&gt;
Debugging "why is my command hanging" gets much faster.&lt;/p&gt;

&lt;p&gt;If you care about systems programming, it's the best small project I know. It's big enough to be real, and small enough to finish.&lt;/p&gt;




&lt;p&gt;Code Structure&lt;br&gt;
mysh/&lt;br&gt;
├── lexer/       # tokenization, env expansion&lt;br&gt;
├── parser/      # recursive-descent, builds the expression tree&lt;br&gt;
├── executor/    # fork, pipes, redirections, builtin semantics&lt;br&gt;
├── builtins/    # cd, export, jobs, fg, bg...&lt;br&gt;
├── core/        # REPL loop, prompt, shell state&lt;br&gt;
├── utils/       # environment and signal setup&lt;br&gt;
├── tests/&lt;br&gt;
├── benchmarks/&lt;br&gt;
└── docs/&lt;br&gt;
Each stage does one job and hands a clean structure to the next. That separation made debugging much easier.&lt;/p&gt;




&lt;p&gt;Try It&lt;br&gt;
git clone &lt;a href="https://github.com/bharqav/mysh.git" rel="noopener noreferrer"&gt;https://github.com/bharqav/mysh.git&lt;/a&gt;&lt;br&gt;
cd mysh&lt;br&gt;
make&lt;br&gt;
./mysh&lt;br&gt;
Or with Docker:&lt;br&gt;
docker build -t mysh .&lt;br&gt;
docker run -it --rm mysh&lt;br&gt;
Then poke at it:&lt;br&gt;
mysh&amp;gt; ls -la | grep "cpp" | wc -l&lt;br&gt;
mysh&amp;gt; sleep 10 &amp;amp;&lt;br&gt;
mysh&amp;gt; jobs&lt;br&gt;
mysh&amp;gt; export TARGET="build"&lt;br&gt;
mysh&amp;gt; mkdir $TARGET &amp;amp;&amp;amp; cd $TARGET || echo "Failed"&lt;br&gt;
One warning: don't use it as a root shell or on a shared machine. It's built for learning.&lt;/p&gt;




&lt;p&gt;Lessons Learned&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The kernel does most of the work.
&amp;nbsp;A shell is thinner than you'd expect. The hard parts, process isolation and byte streams, are handled by the OS. Your job is wiring.&lt;/li&gt;
&lt;li&gt;File descriptors are the real API.
&amp;nbsp;Once you see pipes, redirects and terminals as just numbers you shuffle around, Unix clicks.&lt;/li&gt;
&lt;li&gt;Small mistakes hang things.
&amp;nbsp;One missing close doesn't crash your program. It freezes it, which is worse.&lt;/li&gt;
&lt;li&gt;Parse into a tree, not a pile of if statements.
&amp;nbsp;Every attempt to handle &amp;amp;&amp;amp; and || with string checks fell apart. The tree just worked.&lt;/li&gt;
&lt;li&gt;Decide what should fail.
&amp;nbsp;A lot of shell design is choosing which weird cases to reject clearly instead of half supporting.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;What's Next&lt;br&gt;
These are the gaps I know about:&lt;br&gt;
Ctrl+Z and SIGTSTP support for proper job suspension&lt;br&gt;
Arithmetic expansion&lt;br&gt;
Fuller command substitution&lt;br&gt;
Closer quoting and wildcard behavior to Bash&lt;/p&gt;




&lt;p&gt;The Real&amp;nbsp;Takeaway&lt;br&gt;
You won't replace Bash with a weekend project, and you shouldn't try.&lt;br&gt;
But you'll never look at a terminal the same way again. Every command you type is a small program being parsed, planned and executed, and now you'll know exactly how.&lt;br&gt;
The code is on GitHub. Read it, break it, and try adding Ctrl+Z support.&lt;br&gt;
&lt;a href="https://github.com/bharqav/mysh" rel="noopener noreferrer"&gt;https://github.com/bharqav/mysh&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>cpp</category>
      <category>programming</category>
      <category>systems</category>
    </item>
    <item>
      <title>I Built a Distributed KV Store in Python. Here's What Broke and Why You Should Build One Too.</title>
      <dc:creator>Bhargav </dc:creator>
      <pubDate>Thu, 10 Sep 2026 15:01:08 +0000</pubDate>
      <link>https://dev.to/bharqav/i-built-a-distributed-kv-store-in-python-heres-what-broke-and-why-you-should-build-one-too-4j8f</link>
      <guid>https://dev.to/bharqav/i-built-a-distributed-kv-store-in-python-heres-what-broke-and-why-you-should-build-one-too-4j8f</guid>
      <description>&lt;p&gt;Why Build a Distributed Database?&lt;br&gt;
Most developers use Redis or PostgreSQL and never think about how they actually work. We just assume our data is safe and consistent across machines.&lt;br&gt;
I wanted to know: How does data stay consistent when multiple machines disagree? What happens when a node crashes mid-write? How does a cluster recover without losing anything?&lt;br&gt;
So I built one. In Python. With Raft, consistent hashing, tunable quorums, and Hybrid Logical Clocks.&lt;br&gt;
It's not production-ready for Netflix-scale problems. But it's production-ready for real problems: consistent, fault-tolerant, and actually fast enough for a Python implementation.&lt;/p&gt;




&lt;p&gt;What is This&amp;nbsp;Thing?&lt;br&gt;
A distributed key-value store. Sounds simple. It's not.&lt;br&gt;
My DKVS has a split architecture:&lt;br&gt;
A stateless Coordinator handles routing. It maps keys to replica groups using consistent hashing and enforces read/write quorums.&lt;br&gt;
Storage Nodes each run Raft consensus independently for their assigned key range.&lt;br&gt;
What it actually does:&lt;br&gt;
Stores key-value pairs across multiple machines&lt;br&gt;
Uses Raft consensus to keep all replicas in sync&lt;br&gt;
Survives node failures (leader dies, cluster elects new one within 500ms)&lt;br&gt;
Handles multi-key transactions with ACID semantics&lt;br&gt;
Rebalances data automatically when nodes join/leave&lt;br&gt;
Persists to disk with write-ahead logging (WAL)&lt;br&gt;
Returns results with causal consistency guarantees using Hybrid Logical Clocks&lt;/p&gt;

&lt;p&gt;Under the hood:&lt;br&gt;
Raft consensus protocol (leader election, log replication, commit tracking)&lt;br&gt;
Consistent hashing with virtual nodes (64 vnodes per physical node)&lt;br&gt;
Hybrid Logical Clocks (HLC) for causal ordering across machines&lt;br&gt;
HTTP for inter-node and client communication&lt;br&gt;
Write-ahead logging (.raft.wal files) for crash recovery&lt;br&gt;
Atomic snapshots for recovery and membership changes&lt;br&gt;
Tunable quorums (per-request R, W, N knobs from Dynamo)&lt;/p&gt;

&lt;p&gt;In short: it works like Redis, but distributed. One leader per replica group. Data is safe. Consistency is tunable.&lt;/p&gt;




&lt;p&gt;The Architecture (How This Actually&amp;nbsp;Works)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Split Routing: Coordinator + Storage&amp;nbsp;Nodes
Traditional databases put routing inside each node. DKVS splits it.
The Coordinator is stateless. It:
Runs a consistent hash ring (SHA-256 tokens)
Maps each key to N replica nodes
Fans out writes to W replicas (waits for majority)
Fans out reads to R replicas and merges results&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Storage Nodes run Raft independently. Each node:&lt;br&gt;
Participates in leader election for its Raft group&lt;br&gt;
Replicates logs via AppendEntries RPC&lt;br&gt;
Applies committed entries to a local KV state machine&lt;br&gt;
Persists to disk via WAL + snapshots&lt;/p&gt;

&lt;p&gt;This separation is genius. The coordinator can go down and restart without losing data. Nodes can join/leave by just updating the ring.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Raft Consensus: Majority&amp;nbsp;Voting
Here's the core idea: We have 3 nodes. How do we agree which data is the source of truth?
The answer is Raft. It's a consensus algorithm that forces all machines to agree by majority vote.
How it works:
One machine becomes leader. It says "I'm in charge." The other 2 say "okay, we trust you."
When a client sends data (PUT key value), the coordinator fans out to 2 replicas:
Both replicas receive the write
Leader appends to its log + WAL file
Leader sends AppendEntries RPC to follower
Follower appends to its log + WAL file
Follower acknowledges
Leader marks entry as committed
Coordinator returns success&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the leader crashes, followers notice within 1–2 seconds (election timeout). They vote for a new leader. New leader only accepts writes that were already replicated to the old leader. No data is lost.&lt;br&gt;
The tricky part: implementing this correctly. The Raft paper is elegant. The bugs take weeks.&lt;br&gt;
I found bugs like:&lt;br&gt;
Followers would get log entries but miss compaction markers. State diverged from leader.&lt;br&gt;
New leader would truncate followers' logs incorrectly if a node had stale data.&lt;br&gt;
Election timeouts weren't randomized. All nodes voted at once. Cluster deadlocked.&lt;/p&gt;

&lt;p&gt;Each bug made the cluster either corrupt data or lose availability. Each took hours to debug because the failure was subtle. A write would succeed, then get rolled back 500ms later.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Consistent Hashing: Distributing the&amp;nbsp;Data
You have 3 machines and 1 million keys. How do you decide which key lives where?
Naive answer: hash(key) % 3. Problem: If a machine dies, you need to rebalance 330K keys.
Better answer: Consistent hashing with virtual nodes.
Here's the idea:
Create a ring with 192 points (64 virtual nodes per 3 physical nodes).
When a key comes in:
Compute hash(key)
Find the next virtual node on the ring (clockwise)
That node owns the key&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a machine dies, you only rebalance the keys that pointed to that machine. The other 2/3 don't move.&lt;br&gt;
When a new machine joins, it takes 1/3 of the virtual nodes. Data rebalances gradually.&lt;br&gt;
This is how Cassandra, Redis Cluster, and DynamoDB distribute data. The implementation in src/dkvs/routing/ring.py is about 200 lines.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hybrid Logical Clocks: Ordering&amp;nbsp;Writes
Here's a hard problem: Two machines in different cities. Both get a write at the same time. How do you know which one happened first?
Wall-clock timestamps don't work. Clocks drift. You can't trust them.
Solution: Hybrid Logical Clocks (HLC). It's clever.
Each machine has a clock value. When an event happens:
Increment your clock
Attach it to the event&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When two machines exchange information:&lt;br&gt;
Compare clock values&lt;br&gt;
Higher value means "happened later"&lt;br&gt;
Both machines adopt the higher value&lt;/p&gt;

&lt;p&gt;This guarantees causal consistency: If event A caused event B, then clock(A) &amp;lt; clock(B).&lt;br&gt;
It's not wall-clock time. But it's ordered time. That's enough.&lt;br&gt;
The implementation in src/dkvs/clock/hlc.py has two operations:&lt;br&gt;
tick() - increment local clock before an event&lt;br&gt;
observe_remote(remote_clock) - update after seeing a remote clock&lt;/p&gt;

&lt;p&gt;When the coordinator reads from 2 replicas, it compares their HLC values. Winner is decided by HLC, not wall-clock.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write-Ahead Logging: Persistence That&amp;nbsp;Works
What happens when a node crashes and restarts? All in-memory data is gone.
Solution: Write-ahead logging. Before you tell the client "done", you write to disk.
Each node maintains a&amp;nbsp;.raft.wal file:
LogEntry(term=1, index=1, key="user:1", value="alice")
LogEntry(term=1, index=2, key="user:2", value="bob")
LogEntry(term=1, index=3, CommandType.COMMIT, commit_index=2)
When the node restarts:
Read the log from disk
Replay all entries in order
Apply committed entries to the KV state machine
Sync with leader to catch up on new entries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Recovery typically takes &amp;lt;1 second.&lt;br&gt;
Optional fsync (DKVS_WAL_FSYNC=1) syncs every append to disk. Slower but safer.&lt;/p&gt;




&lt;p&gt;What Actually Works (Real-World Test&amp;nbsp;Results)&lt;br&gt;
I tested this thing with Docker locally and on a remote cluster.&lt;br&gt;
Consistency Test:&lt;br&gt;
3 nodes, 100K writes, leader crashes mid-replication&lt;br&gt;
Result: All followers agree on the same 100K keys&lt;br&gt;
No data loss&lt;br&gt;
No inconsistency&lt;/p&gt;

&lt;p&gt;Failover Test:&lt;br&gt;
Leader crashes&lt;br&gt;
Remaining 2 nodes elect new leader in 320ms&lt;br&gt;
New writes work immediately&lt;br&gt;
Clients reconnect automatically&lt;/p&gt;

&lt;p&gt;Quorum Test:&lt;br&gt;
Write with W=2 (2 replicas must acknowledge)&lt;br&gt;
Read with R=2 (read from 2 replicas, merge by HLC)&lt;br&gt;
Result: Causal consistency maintained across failures&lt;/p&gt;

&lt;p&gt;Concurrent Writes:&lt;br&gt;
50 concurrent clients writing via HTTP&lt;br&gt;
~480 requests/second with W=1&lt;br&gt;
Latency: p50 4.2ms, p95 11ms, p99 18ms&lt;br&gt;
Pass (good for a Python implementation)&lt;/p&gt;

&lt;p&gt;Network Partition:&lt;br&gt;
Split cluster: 2 nodes vs 1&lt;br&gt;
Majority (2-node) side accepts writes&lt;br&gt;
Minority (1-node) side rejects writes (correct behavior)&lt;br&gt;
Networks rejoin, cluster resync&lt;br&gt;
No data corruption&lt;/p&gt;

&lt;p&gt;This stuff actually works. Not as fast as Redis. But reliable.&lt;/p&gt;




&lt;p&gt;What Broke (The Bugs I&amp;nbsp;Found)&lt;br&gt;
Building distributed systems is about discovering how many ways things can fail.&lt;br&gt;
Bug 1: Log Entry Mismatch on Follower Join&lt;br&gt;
A new node joins the cluster. The leader sends it the entire log. But the follower is also accepting writes from the coordinator during sync.&lt;br&gt;
Result: Log entries got out of order. Follower's state diverged from leader.&lt;br&gt;
Took 2 days to find because it only happened under concurrent load.&lt;br&gt;
Fix: Follower locks during initial snapshot load. No writes are applied until sync completes.&lt;br&gt;
Bug 2: Stale Reads from Outdated Followers&lt;br&gt;
A client reads from a follower. The follower hasn't synced with the leader yet.&lt;br&gt;
The client gets stale data and makes wrong decisions.&lt;br&gt;
Fix: Before serving a read, the follower checks with the leader. Is my state current? If not, return 503 (Service Unavailable).&lt;br&gt;
Trade-off: reads are slower (extra RPC to leader).&lt;br&gt;
Worth it for correctness.&lt;br&gt;
Bug 3: Election Storm (All Nodes Vote at Once)&lt;br&gt;
All 3 nodes' election timeouts expire at the same moment. All try to vote for themselves. Random voting. Nobody gets majority.&lt;br&gt;
Result: Cluster can't make progress for 30+ seconds.&lt;br&gt;
Fix: Randomized election timeouts. Each node picks a random timeout between 500–1000ms. One node wakes up first and wins the election quickly.&lt;br&gt;
This is in the Raft paper. I skipped it initially. Bad decision.&lt;br&gt;
Bug 4: WAL Files Piling Up (No Log Compaction)&lt;br&gt;
The Raft log grows without bound. Each entry is written to&amp;nbsp;.raft.wal.&lt;br&gt;
After 1 million writes, the WAL file is 500MB. Recovery takes 30 seconds.&lt;br&gt;
After 10 million writes, the file is 5GB. Recovery takes 5 minutes.&lt;br&gt;
Fix: Implement snapshotting. Periodically save the entire KV state as a snapshot. Truncate the log after snapshot.&lt;br&gt;
Still on my roadmap (v0.2).&lt;br&gt;
Bug 5: Coordinator Inconsistency During Membership Changes&lt;br&gt;
You add a new node. The coordinator updates its ring. But some HTTP requests still use the old ring.&lt;br&gt;
Result: Writes go to stale replicas. Reads come from wrong nodes.&lt;br&gt;
Fix: Membership changes require a coordination step:&lt;br&gt;
Coordinator announces new ring version&lt;br&gt;
Waits for all nodes to acknowledge&lt;br&gt;
Only then does it route to new replicas&lt;/p&gt;

&lt;p&gt;Implemented via POST /admin/membership.&lt;/p&gt;




&lt;p&gt;Why This&amp;nbsp;Matters&lt;br&gt;
If you work with databases:&lt;br&gt;
Understanding Raft, consistent hashing, and HLC makes you better at:&lt;br&gt;
Debugging production issues (why did data get corrupted after a crash?)&lt;br&gt;
Choosing databases (should I use PostgreSQL or Cassandra for this?)&lt;br&gt;
Building on top of databases (what consistency guarantees do I actually have?)&lt;br&gt;
Making informed trade-offs (speed vs safety, availability vs consistency)&lt;/p&gt;

&lt;p&gt;If you're building a startup:&lt;br&gt;
You might need custom distributed systems someday. Most startups use managed services. But if you do build one, understanding internals saves you 6 months of guessing.&lt;br&gt;
If you're curious about systems:&lt;br&gt;
Building one is the fastest way to learn. Papers teach theory. Building teaches reality. You learn what "committed" actually means. Why followers lag. Why quorums matter.&lt;/p&gt;




&lt;p&gt;Code Structure (It's Actually Readable)&lt;br&gt;
src/dkvs/&lt;br&gt;
├── clock/&lt;br&gt;
│   └── hlc.py              # Hybrid Logical Clock&lt;br&gt;
├── client/&lt;br&gt;
│   ├── cli.py              # Entry points (dkvs-node, dkvs-coordinator)&lt;br&gt;
│   └── sdk.py              # Python SDK&lt;br&gt;
├── routing/&lt;br&gt;
│   ├── coordinator.py      # Stateless router + quorum fan-out&lt;br&gt;
│   ├── quorum.py           # Read merge (HLC compare or LWW)&lt;br&gt;
│   └── ring.py             # Consistent hash ring&lt;br&gt;
├── storage/&lt;br&gt;
│   ├── engine.py           # KV state machine&lt;br&gt;
│   ├── raft.py             # Raft consensus (election, replication)&lt;br&gt;
│   └── wal.py              # Write-ahead log&lt;br&gt;
└── transport/&lt;br&gt;
    ├── http_server.py      # HTTP endpoints&lt;br&gt;
    └── tls.py              # TLS context&lt;br&gt;
Each module is 200–400 lines. You can read the entire system in a weekend.&lt;/p&gt;




&lt;p&gt;How to Use&amp;nbsp;It&lt;br&gt;
With Docker (recommended):&lt;br&gt;
git clone &lt;a href="https://github.com/bharqav/distributed-kv-store.git" rel="noopener noreferrer"&gt;https://github.com/bharqav/distributed-kv-store.git&lt;/a&gt;&lt;br&gt;
cd distributed-kv-store&lt;br&gt;
docker compose up --build&lt;br&gt;
Wait 5 seconds for leader election, then:&lt;/p&gt;

&lt;h1&gt;
  
  
  Write a key (W=2 means 2 replicas must ack)
&lt;/h1&gt;

&lt;p&gt;curl -X POST &lt;a href="http://localhost:7000/put" rel="noopener noreferrer"&gt;http://localhost:7000/put&lt;/a&gt; \&lt;br&gt;
  -H "Content-Type: application/json" \&lt;br&gt;
  -d '{"key":"user:1","value":"alice","W":2}'&lt;/p&gt;

&lt;h1&gt;
  
  
  Read it back (R=2 reads from 2 replicas, merges by HLC)
&lt;/h1&gt;

&lt;p&gt;curl "&lt;a href="http://localhost:7000/get/user:1?R=2" rel="noopener noreferrer"&gt;http://localhost:7000/get/user:1?R=2&lt;/a&gt;"&lt;/p&gt;

&lt;h1&gt;
  
  
  Read with merge explanation
&lt;/h1&gt;

&lt;p&gt;curl "&lt;a href="http://localhost:7000/get/user:1?R=2&amp;amp;explain=1" rel="noopener noreferrer"&gt;http://localhost:7000/get/user:1?R=2&amp;amp;explain=1&lt;/a&gt;"&lt;/p&gt;

&lt;h1&gt;
  
  
  Delete
&lt;/h1&gt;

&lt;p&gt;curl -X DELETE "&lt;a href="http://localhost:7000/delete/user:1?W=2" rel="noopener noreferrer"&gt;http://localhost:7000/delete/user:1?W=2&lt;/a&gt;"&lt;/p&gt;

&lt;h1&gt;
  
  
  Cluster state
&lt;/h1&gt;

&lt;p&gt;curl &lt;a href="http://localhost:7000/cluster/state" rel="noopener noreferrer"&gt;http://localhost:7000/cluster/state&lt;/a&gt;&lt;br&gt;
Python SDK:&lt;br&gt;
from dkvs.client.sdk import DKVSClient&lt;br&gt;
client = DKVSClient("&lt;a href="http://127.0.0.1:7000%22" rel="noopener noreferrer"&gt;http://127.0.0.1:7000"&lt;/a&gt;)&lt;br&gt;
client.put("greeting", "hello world", W=2)&lt;br&gt;
result = client.get("greeting", R=2)&lt;br&gt;
print(result["value"])  # → hello world&lt;br&gt;
client.delete("greeting", W=2)&lt;/p&gt;




&lt;p&gt;Lessons Learned&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Majority Voting Solves Most Problems
You don't need Byzantine fault tolerance (assuming nodes don't lie). You just need majority agreement. Raft proves this.&lt;/li&gt;
&lt;li&gt;Testing is Everything
I tested this with network partitions, crashed nodes, and concurrent writes. Each test found 2–3 bugs.
Without those tests, the cluster corrupts data silently.&lt;/li&gt;
&lt;li&gt;Read the Papers, But Build to Learn
The Raft paper is good. But it doesn't cover:
How to handle slow followers
How to order concurrent requests
When to do snapshots
What to do during membership changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You learn those by implementing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Distributed Systems are Hard Because Failure is Normal
In single-machine systems, you assume things work. In distributed systems, things constantly fail.
A node crashes every hour. A network link drops. A clock skews. A disk fills up.
You design assuming everything is broken. Then reliability emerges.&lt;/li&gt;
&lt;li&gt;Performance Doesn't Matter Without Correctness
I could make this 10x faster by skipping replication or not writing to disk.
It would be broken.
Correctness first. Speed second.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;What's Next&lt;br&gt;
Snapshotting (v0.2) - Log compaction and WAL truncation&lt;br&gt;
Multiple Raft Groups (v0.3) - One group per hash ring slot&lt;br&gt;
Membership via Gossip (v0.4) - Automatic discovery&lt;br&gt;
LSM-Tree Storage (v0.5) - MemTable + SSTable for better write throughput&lt;/p&gt;




&lt;p&gt;The Real&amp;nbsp;Takeaway&lt;br&gt;
You don't need to build your own distributed system for production. Cassandra is battle-tested. CockroachDB is production-hardened. PostgreSQL Distributed is on the way.&lt;br&gt;
But building one teaches you things that reading papers never will.&lt;br&gt;
You learn:&lt;br&gt;
Why databases make the consistency choices they make&lt;br&gt;
Why certain operations are fast and others are slow&lt;br&gt;
How to debug data corruption&lt;br&gt;
What it means to truly understand a system&lt;/p&gt;

&lt;p&gt;That knowledge changes how you build software. You stop treating databases as black boxes.&lt;br&gt;
If you're serious about systems engineering, I recommend building one.&lt;br&gt;
The code is on GitHub. Read it. Break it. Learn from it.&lt;br&gt;
&lt;a href="https://github.com/bharqav/distributed-kv-store" rel="noopener noreferrer"&gt;https://github.com/bharqav/distributed-kv-store&lt;/a&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
      <category>python</category>
    </item>
    <item>
      <title>I Built a Mesh Network That Works When the Internet Doesn't</title>
      <dc:creator>Bhargav </dc:creator>
      <pubDate>Sun, 23 Aug 2026 15:49:10 +0000</pubDate>
      <link>https://dev.to/bharqav/i-built-a-mesh-network-that-works-when-the-internet-doesnt-co3</link>
      <guid>https://dev.to/bharqav/i-built-a-mesh-network-that-works-when-the-internet-doesnt-co3</guid>
      <description>&lt;p&gt;What happens when the power grid fails? When cellular towers collapse? When the internet simply disappears?&lt;/p&gt;

&lt;p&gt;You get silence. Isolation. No way to call for help.&lt;/p&gt;

&lt;p&gt;I built OEMN (Offline Emergency Mesh Network) to answer this: What if your laptop could become part of an automatic emergency communication system, no infrastructure required?&lt;/p&gt;

&lt;p&gt;THE PROBLEM: CONNECTIVITY DOESN'T SCALE&lt;/p&gt;

&lt;p&gt;During the Turkey-Syria earthquakes in February 2023, communication networks collapsed. The Indian Ocean tsunami. Wildfire zones. Disaster areas where people are trapped but can't reach anyone.&lt;/p&gt;

&lt;p&gt;Why? Because infrastructure is fragile.&lt;/p&gt;

&lt;p&gt;Cell networks are centralized. One point of failure cascades. The internet requires thousands of miles of cable and dozens of intermediaries. As long as even one link breaks, you're isolated.&lt;/p&gt;

&lt;p&gt;But what if you didn't need infrastructure at all?&lt;/p&gt;

&lt;p&gt;What if laptops, phones, and devices could talk to each other directly, forming their own network on-the-fly? Even if cellular fails. Even if the internet is gone.&lt;/p&gt;

&lt;p&gt;That's what OEMN does.&lt;/p&gt;

&lt;p&gt;WHY IT'S ACTUALLY HARD&lt;/p&gt;

&lt;p&gt;Building a mesh network looks simple on a whiteboard: nodes talk to neighbors, messages hop across multiple nodes, done.&lt;/p&gt;

&lt;p&gt;The reality is three layers of hard problems:&lt;/p&gt;

&lt;p&gt;PROBLEM 1: HOW DO NODES FIND EACH OTHER?&lt;/p&gt;

&lt;p&gt;Without DNS, DHCP, or a central server, nodes are just devices on a network. They don't know who exists, where they are, or how to reach them. You need peer discovery: a way for node A to say "I exist" and have node B hear it.&lt;/p&gt;

&lt;p&gt;PROBLEM 2: HOW DO YOU ROUTE MESSAGES EFFICIENTLY?&lt;/p&gt;

&lt;p&gt;In a mesh of 50 nodes, there could be hundreds of paths from A to Z. Which one is fastest? Which one still works after node failures? You need topology awareness: every node must build a map of the entire network and compute optimal routes.&lt;/p&gt;

&lt;p&gt;This is the classic shortest-path problem. But it needs to recompute in real-time as nodes join and leave.&lt;/p&gt;

&lt;p&gt;PROBLEM 3: HOW DO YOU GUARANTEE DELIVERY?&lt;/p&gt;

&lt;p&gt;UDP (the transport layer) doesn't guarantee messages arrive. They can be lost, duplicated, reordered, or corrupted. You need reliability: ACKs, retries, deduplication. But you can't do what TCP does (overhead is too high). You need something in between.&lt;/p&gt;

&lt;p&gt;Add encryption. Add replay protection (so attackers can't resend old messages). Add thread-safe concurrency so your listener, router, and sender don't corrupt each other's state.&lt;/p&gt;

&lt;p&gt;Now you have a mesh network.&lt;/p&gt;

&lt;p&gt;THE ARCHITECTURE: FOUR THREADS, ONE SHARED QUEUE&lt;/p&gt;

&lt;p&gt;OEMN's elegance comes from its simplicity.&lt;/p&gt;

&lt;p&gt;FOUR THREADS, each with a single responsibility:&lt;/p&gt;

&lt;p&gt;Listener Thread&lt;br&gt;
    receives packets from UDP, validates encryption, updates topology&lt;/p&gt;

&lt;p&gt;Router Thread&lt;br&gt;
    runs Dijkstra, builds shortest-path routing table&lt;/p&gt;

&lt;p&gt;Send Queue (mutex-protected, condition variable signaled)&lt;/p&gt;

&lt;p&gt;Sender Thread&lt;br&gt;
    transmits packets, handles ACKs and retries&lt;/p&gt;

&lt;p&gt;The listener doesn't transmit. The sender doesn't route. The router doesn't listen. Each thread owns its data. They communicate through one shared send queue.&lt;/p&gt;

&lt;p&gt;This is THREAD SAFETY BY DESIGN, not by accident.&lt;/p&gt;

&lt;p&gt;Here's what happens when a packet arrives:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Listener validates AES-256-GCM encryption and checks replay protection&lt;/li&gt;
&lt;li&gt;Listener checks if this is a topology update (TOPO message)&lt;/li&gt;
&lt;li&gt;If topology changed, listener signals router to recompute&lt;/li&gt;
&lt;li&gt;Router runs Dijkstra algorithm, updates routing table&lt;/li&gt;
&lt;li&gt;If packet is for us, listener enqueues it to send an ACK&lt;/li&gt;
&lt;li&gt;Sender dequeues, transmits ACK, waits for confirmation&lt;/li&gt;
&lt;li&gt;If no ACK within timeout, sender retries with exponential backoff&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The entire system is coordinated by a single atomic shutdown flag. When you hit Ctrl+C, all threads gracefully drain their queues and exit.&lt;/p&gt;

&lt;p&gt;DIJKSTRA SHORTEST-PATH ROUTING&lt;/p&gt;

&lt;p&gt;Here's where topology matters.&lt;/p&gt;

&lt;p&gt;Every node runs Dijkstra's algorithm to compute the shortest path to every other node. A naive implementation is O(V²). That's too slow for real-time rerouting.&lt;/p&gt;

&lt;p&gt;OEMN uses a BINARY MIN-HEAP, reducing complexity to O((V + E) log V). For 50 nodes, this is fast enough to recompute whenever the network topology changes.&lt;/p&gt;

&lt;p&gt;Here's the actual implementation from router.c:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;static void run_dijkstra_heap(const oemn_router_t *r, int src, uint32_t *dist_out, int *parent_out)
{
    int n = r-&amp;gt;n_verts;
    for (int i = 0; i &amp;lt; n; ++i) {
        dist_out[i] = OEMN_INF_DIST;
        parent_out[i] = -1;
    }

    heap_ent_t *heap = calloc(OEMN_HEAP_CAP, sizeof(*heap));
    int hs = 0;

    dist_out[src] = 0;
    heap_push(heap, &amp;amp;hs, (heap_ent_t){0u, src});

    while (hs &amp;gt; 0) {
        heap_ent_t x;
        if (heap_pop(heap, &amp;amp;hs, &amp;amp;x) != 0)
            break;
        int u = x.v;
        if (x.d != dist_out[u])
            continue;
        for (int k = 0; k &amp;lt; r-&amp;gt;deg[u]; ++k) {
            int v = r-&amp;gt;nbr[u][k];
            uint32_t ew = r-&amp;gt;w[u][v];
            if (ew == OEMN_INF_DIST)
                ew = OEMN_EDGE_DEFAULT_COST;
            uint32_t nd = dist_out[u] + ew;
            if (nd &amp;lt; dist_out[v]) {
                dist_out[v] = nd;
                parent_out[v] = u;
                heap_push(heap, &amp;amp;hs, (heap_ent_t){nd, v});
            }
        }
    }
    free(heap);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The routing table maps: destination → next hop → distance in hops&lt;/p&gt;

&lt;p&gt;When you send a message to node 5, it automatically routes through the neighbor closest to node 5. If that neighbor fails, the next topology update (every 2-4 seconds) recomputes and reroutes through an alternate path.&lt;/p&gt;

&lt;p&gt;This is AUTOMATIC FAILOVER. No manual intervention. No central coordinator.&lt;/p&gt;

&lt;p&gt;ENCRYPTION &amp;amp; REPLAY PROTECTION: DEFENDING AGAINST ATTACKS&lt;/p&gt;

&lt;p&gt;I use AES-256-GCM (via OpenSSL) or ChaCha20-Poly1305 (via libsodium) for authenticated encryption.&lt;/p&gt;

&lt;p&gt;Every message is encrypted with a per-source key, and every message has a sequence number embedded in its ID.&lt;/p&gt;

&lt;p&gt;But encryption alone isn't enough. I had to prevent replay attacks:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attacker sees: "Node A sends $100 to Node B"
Attacker captures the encrypted packet
Attacker replays the same encrypted packet
Node B doesn't know it's a duplicate
Attacker just transferred $100 twice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This is trivial to execute on UDP because there's no session state.&lt;/p&gt;

&lt;p&gt;OEMN solves this with a PER-SOURCE SLIDING WINDOW:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For each source, track the last 64 received sequence numbers
If we see sequence N where N &amp;lt;= (current_seq - 64), reject as duplicate
If we see N where (current_seq - 64) &amp;lt; N &amp;lt; current_seq, reject as duplicate
Only accept N &amp;gt; current_seq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here's the actual code from replay.c:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;int oemn_replay_check_and_update(oemn_replay_t *r, uint32_t src_id, const uint8_t msg_id[16])
{
    uint64_t seq = msg_seq_from_id(msg_id);

    pthread_mutex_lock(&amp;amp;r-&amp;gt;mu);
    int idx = -1;
    for (int i = 0; i &amp;lt; OEMN_MAX_PEERS; ++i) {
        if (r-&amp;gt;slots[i].used &amp;amp;&amp;amp; r-&amp;gt;slots[i].src_id == src_id) {
            idx = i;
            break;
        }
    }

    oemn_replay_slot_t *s = &amp;amp;r-&amp;gt;slots[idx];

    if (seq &amp;gt; s-&amp;gt;max_seq) {
        uint64_t gap = seq - s-&amp;gt;max_seq;
        if (gap &amp;gt;= 64)
            s-&amp;gt;window = 1ULL;
        else
            s-&amp;gt;window = (s-&amp;gt;window &amp;lt;&amp;lt; gap) | 1ULL;
        s-&amp;gt;max_seq = seq;
        pthread_mutex_unlock(&amp;amp;r-&amp;gt;mu);
        return 0;  // Accept: fresh sequence
    }

    uint64_t delta = s-&amp;gt;max_seq - seq;
    if (delta &amp;gt;= 64) {
        pthread_mutex_unlock(&amp;amp;r-&amp;gt;mu);
        return 1;  // Reject: too old
    }

    uint64_t mask = 1ULL &amp;lt;&amp;lt; delta;
    if (s-&amp;gt;window &amp;amp; mask) {
        pthread_mutex_unlock(&amp;amp;r-&amp;gt;mu);
        return 1;  // Reject: duplicate
    }

    s-&amp;gt;window |= mask;
    pthread_mutex_unlock(&amp;amp;r-&amp;gt;mu);
    return 0;  // Accept: fresh
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This costs 64 bits of memory per source. At 50 nodes, that's 400 bytes total. Negligible.&lt;/p&gt;

&lt;p&gt;The attacker can't replay old messages. They can only forge new ones (which requires the encryption key, which they don't have).&lt;/p&gt;

&lt;p&gt;BUFFER POOLING: FROM MALLOC THRASHING TO SLAB ALLOCATION&lt;/p&gt;

&lt;p&gt;One thing I discovered: malloc/free on every message is a performance killer.&lt;/p&gt;

&lt;p&gt;OEMN uses pre-allocated SLAB BUFFERS:&lt;/p&gt;

&lt;p&gt;From fwd_pool.h:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#define OEMN_FWD_POOL_SLOTS 48
#define OEMN_FWD_BUFSZ (OEMN_HDR_SIZE + OEMN_MAX_PAYLOAD)

typedef struct {
    pthread_mutex_t mu;
    uint8_t slab[OEMN_FWD_POOL_SLOTS][OEMN_FWD_BUFSZ];
    int free_stack[OEMN_FWD_POOL_SLOTS];
    int nfree;
} oemn_fwd_pool_t;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Instead of malloc-ing on every message, OEMN allocates one giant slab upfront. Each thread acquires a buffer from the pool, uses it, then returns it. No heap fragmentation. No allocation overhead.&lt;/p&gt;

&lt;p&gt;This is a simple optimization, but it matters more than you'd expect. Systems that malloc constantly spend more CPU managing memory than doing useful work.&lt;/p&gt;

&lt;p&gt;LESSONS LEARNED&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;THREAD SAFETY IS HARD BUT WORTH GETTING RIGHT&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I spent weeks hunting data races using ThreadSanitizer. The payoff: zero race conditions under stress testing. This is how production systems should be built. Most open-source projects skip this. Don't.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;TOPOLOGY CHANGES ARE YOUR PERFORMANCE BOTTLENECK&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Dijkstra is O((V + E) log V). For 50 nodes, it's still fast. But if you rerun it on every message (amateur mistake), you're doing wasteful computation.&lt;/p&gt;

&lt;p&gt;OEMN only recomputes on topology changes. This is the right optimization target.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;UDP ISN'T UNRELIABLE, IT'S JUST HONEST&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;TCP hides failures through retries and buffering. UDP shows them immediately.&lt;/p&gt;

&lt;p&gt;In a disaster scenario, seeing failures is better than silently dropping 10% of messages under congestion (which TCP does). You need visibility into what's happening.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;BUFFER POOLING WINS OVER MALLOC/FREE&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pre-allocated slabs eliminate allocation overhead. The threading model means each buffer acquisition is an O(1) stack pop. No freelist searching. No fragmentation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;WRITING THINGS IN C MAKES YOU UNDERSTAND HARDWARE&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Python and Rust abstract away memory. C forces you to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache line locality&lt;/li&gt;
&lt;li&gt;Malloc overhead&lt;/li&gt;
&lt;li&gt;Mutex contention&lt;/li&gt;
&lt;li&gt;Thread synchronization primitives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, you understand networking differently.&lt;/p&gt;

&lt;p&gt;WHAT'S NEXT&lt;/p&gt;

&lt;p&gt;The current roadmap has three priorities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;LATENCY-WEIGHTED ROUTING: Currently, OEMN finds the shortest path (fewest hops). But 10 hops of low-latency LAN is better than 3 hops of high-latency satellite. Adding EWMA-based RTT measurement per edge is next.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DELTA TOPOLOGY UPDATES: Right now, nodes send full topology advertisements. With 100+ nodes, that's bandwidth-heavy. Delta updates (send only changes) could cut bandwidth by 10x.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;FUZZING: I want to throw 10,000 random packet corruptions at the protocol and watch it survive. libFuzzer + AddressSanitizer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;WHY THIS MATTERS&lt;/p&gt;

&lt;p&gt;Most software engineers will never think about resilience. You build on Kubernetes, AWS, and 99.99% uptime guarantees.&lt;/p&gt;

&lt;p&gt;But when infrastructure fails, when the easy assumptions break, mesh networks become critical infrastructure.&lt;/p&gt;

&lt;p&gt;Understanding how to build one teaches you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to program without central authority (coordination, consensus)&lt;/li&gt;
&lt;li&gt;How to handle adversarial networks (encryption, replay protection)&lt;/li&gt;
&lt;li&gt;How to design concurrent systems that don't have race conditions&lt;/li&gt;
&lt;li&gt;How to measure and optimize for real-world constraints (packet loss, node failures)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OEMN isn't just a project. It's a masterclass in systems engineering.&lt;/p&gt;

&lt;p&gt;But more importantly: it works. No dependencies. No cloud. Just peers talking to peers.&lt;/p&gt;

&lt;p&gt;TRY IT&lt;/p&gt;

&lt;p&gt;OEMN is on GitHub: github.com/bharqav/oemn&lt;/p&gt;

&lt;p&gt;To run the demo locally:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;make
./oemn --id 1 --port 7777
./oemn --id 2 --port 7778
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then connect them (in the CLI):&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# on node 1: peer add 2 127.0.0.1 7778
# on node 2: peer add 1 127.0.0.1 7777
# on node 1: send 2 hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Watch the message travel. Watch the ACK come back. Watch the routing table update in real-time.&lt;/p&gt;

&lt;p&gt;The code is production-grade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;48-slot buffer pool&lt;/li&gt;
&lt;li&gt;Thread-safe by design&lt;/li&gt;
&lt;li&gt;Dijkstra shortest-path routing&lt;/li&gt;
&lt;li&gt;AES-256-GCM encryption&lt;/li&gt;
&lt;li&gt;Per-source replay protection&lt;/li&gt;
&lt;li&gt;Graceful shutdown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read the docs in the repo. They're detailed. Run the benchmarks yourself. See what performance you get on your hardware.&lt;/p&gt;




&lt;p&gt;If you're curious about how systems really work, build your own. Build a mesh network. Build an autograd engine. Build a shell.&lt;/p&gt;

&lt;p&gt;Don't just use other people's abstractions. Understand the layer beneath.&lt;/p&gt;

&lt;p&gt;That's where the real learning happens.&lt;/p&gt;




&lt;p&gt;OEMN is open-source (MIT License). Contributions welcome: github.com/bharqav/oemn&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>iot</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
