<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Mohammed Abdul Raqueeb]]></title><description><![CDATA[Mohammed Abdul Raqueeb]]></description><link>https://raqueeb.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 13 Nov 2026 18:36:20 GMT</lastBuildDate><atom:link href="https://raqueeb.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Crash-Safe Write-Ahead Log in Go, and the 24 Ways I Tried to Break It]]></title><description><![CDATA[How KVStore's write-ahead log survives kill -9: CRC32C checksums, group commit, torn-tail vs corruption recovery, and a 24-run crash-test suite.]]></description><link>https://raqueeb.hashnode.dev/building-a-crash-safe-write-ahead-log-in-go-and-the-24-ways-i-tried-to-break-it</link><guid isPermaLink="true">https://raqueeb.hashnode.dev/building-a-crash-safe-write-ahead-log-in-go-and-the-24-ways-i-tried-to-break-it</guid><category><![CDATA[Go Language]]></category><category><![CDATA[Databases]]></category><category><![CDATA[backend]]></category><category><![CDATA[Systems Programming]]></category><dc:creator><![CDATA[Mohammed Abdul Raqueeb]]></dc:creator><pubDate>Sat, 07 Nov 2026 16:46:33 GMT</pubDate><content:encoded><![CDATA[<p>By Mohammed Abdul Raqueeb (<a href="https://dev.to/raqueeb">@raqueeb</a>) · 2026-11-07</p>
<p>KVStore is a key-value database I built from scratch in Go — no Redis, no RocksDB, no embedded database, just the standard library — and the one feature the whole project exists to prove is durability: once the server says a write succeeded, that write has to survive a <code>kill -9</code>, a torn log segment, or the machine losing power mid-fsync. This is a tour of the write-ahead log that makes that promise good, and the test suite that tries hard to prove it wrong.</p>
<h2>Why "the write succeeded" is a hard sentence to earn</h2>
<p>An in-memory map is fast and it is also a lie the moment the process dies. KVStore's storage engine is a sharded map — per-shard <code>RWMutex</code>, so reads and writes on different shards never block each other — and on its own that map is exactly as durable as the process holding it, which is to say not at all.</p>
<p>The fix everyone reaches for is a write-ahead log: before the server acknowledges a write, it has to be on disk somewhere it can be replayed from. That's a one-sentence idea with a lot of ways to get wrong — the log itself has to survive the same crash the in-memory state doesn't, which means thinking about partial writes, fsync timing, and what "on disk" actually guarantees on the filesystem you're running on.</p>
<h2>Group commit, CRC32C, and three fsync policies</h2>
<p>Every WAL record carries a CRC32C checksum, and records are written into rotating segment files rather than one ever-growing log — that bounds how much a single corrupt segment can cost you, and it's what lets recovery reason about "this segment" instead of "the log, somewhere."</p>
<p>The part that actually costs performance is <code>fsync</code>, so KVStore batches concurrent writers into group commits: instead of one fsync per write, several writers that land in the same short window share a single fsync call. That turns a design question — how durable do you need this write to be — into a runtime flag instead of a rewrite: <code>--fsync always</code> syncs every record immediately (safest, slowest), <code>--fsync everysec</code> syncs on a timer (the usual compromise, bounded data loss on a crash), and <code>--fsync no</code> leaves it to the OS (fastest, no durability guarantee at all). Same knob either as a flag or an environment variable — <code>KV_FSYNC</code>, following the project's own convention that every flag has an env equivalent and an explicit flag always wins.</p>
<h2>Recovery: a torn tail is not the same as corruption</h2>
<p>On restart, the engine loads the most recent snapshot and replays the WAL segments written since it — and that replay has to make a judgment call every single time: is this broken record at the end of the log a <em>torn tail</em> (the process died mid-write, exactly as expected, truncate it and move on) or <em>real corruption</em> (a checksum mismatch somewhere in the middle, which is a bug and should fail loudly rather than silently drop data)? Getting that distinction wrong in either direction is bad — refuse to start on every ordinary crash, or silently eat a corrupted key you should have flagged.</p>
<p>Snapshots get the same care. Writing one is atomic in the boring, correct way: write to a temp file, fsync it, rename it into place, then fsync the <em>directory</em> so the rename itself is durable, with a CRC32C footer over the whole file so a half-written snapshot can never be mistaken for a good one. None of this is exotic — it's the standard recipe — but every step in that recipe is a step someone skipped in a database that later lost data.</p>
<h2>Proving it: kill -9, for real</h2>
<p>You can't SIGKILL a goroutine, so the crash-test suite spawns real, separate OS processes running the actual server binary. A writer records every key the server said "OK" to, the process gets SIGKILLed at a random moment (200–999ms in, with writers still active), and after restart every single acknowledged key has to be present and correct — <code>make test-crash</code> runs this across two fsync policies, always and everysec, twelve iterations each: 24 kills in total, timed randomly rather than targeted at a specific point in the write path.</p>
<p>That suite doesn't stand alone. A differential test runs random operation sequences against both the real store and a twenty-line reference map and compares every result, automatically shrinking any failing sequence down to a minimal reproduction. A chaos suite throws forged length fields, mid-handshake TLS bytes on the plain KV port, and clients that disconnect mid-frame at the server, then checks a well-behaved client still gets correct service afterward. And the protocol decoder is fuzzed directly, asserting it never panics and never reads past its own input. Four different ways of trying to be wrong, because "the tests pass" and "this is actually durable" are not the same claim.</p>
<h2>One honest gap: directory fsync on Windows</h2>
<p>On Linux and macOS, "fsync the directory" is a real syscall — <code>fsync(2)</code> on the directory's own file handle — and that's what makes a brand-new file's directory entry durable, not just its contents. Windows has no equivalent, so that specific guarantee is weaker there: record <em>contents</em> are still fsynced under <code>--fsync always</code>, but the durability of a new file's directory entry is not. This is documented rather than hidden — it's written up as its own decision record — and it's visible at runtime too: every <code>STATS</code> response reports a <code>wal.dir_sync_supported</code> field, so a replica or a monitoring script can see which guarantee it's actually getting instead of assuming.</p>
<h2>What I'd carry into the next project</h2>
<p>Treat durability as a testing problem, not just an implementation one. It's easy to write a WAL that looks correct and never find out it isn't until a real crash loses real data — SIGKILLing actual processes, not mocking the failure, is what turns "should be durable" into something you can actually defend. The other habit worth keeping: <code>protocol</code>, <code>store</code> and <code>wal</code> have no knowledge of each other, so recovery logic can't accidentally depend on something the protocol layer assumed. One package composes them; nothing else is allowed to.</p>
<p>Source: <a href="https://github.com/raqueeeb/kvstore">github.com/raqueeeb/kvstore</a> · Case study: <a href="https://raqueeb.dev/projects/kvstore">raqueeb.dev/projects/kvstore</a> · Portfolio: <a href="https://raqueeb.dev">raqueeb.dev</a> · GitHub: <a href="https://github.com/raqueeeb">@raqueeeb</a> · LinkedIn: <a href="https://linkedin.com/in/raqueeeb">in/raqueeeb</a></p>
]]></content:encoded></item></channel></rss>