<?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: Anapeksha Mukherjee</title>
    <description>The latest articles on DEV Community by Anapeksha Mukherjee (@anapeksha).</description>
    <link>https://dev.to/anapeksha</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%2F3970396%2F73aabc94-ef46-4605-984e-09bc3ddfec18.png</url>
      <title>DEV Community: Anapeksha Mukherjee</title>
      <link>https://dev.to/anapeksha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anapeksha"/>
    <language>en</language>
    <item>
      <title>What Designing a Binary Protocol Actually Taught Me</title>
      <dc:creator>Anapeksha Mukherjee</dc:creator>
      <pubDate>Thu, 11 Jun 2026 06:10:07 +0000</pubDate>
      <link>https://dev.to/anapeksha/what-designing-a-binary-protocol-actually-taught-me-lo0</link>
      <guid>https://dev.to/anapeksha/what-designing-a-binary-protocol-actually-taught-me-lo0</guid>
      <description>&lt;p&gt;Most developers never have to design a network protocol from scratch. You use HTTP, gRPC, WebSockets, or something else that already exists and has been debugged by thousands of people over many years. That is the right call for most situations.&lt;/p&gt;

&lt;p&gt;I did not take that path when building Vaylix, a key-value database engine. I designed a custom binary protocol called VTP2, and the process taught me things about networking that I would not have picked up any other way.&lt;/p&gt;

&lt;p&gt;This is not an argument that you should also build a custom protocol. For most things, you should not. This is an honest account of what I ran into.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not HTTP
&lt;/h2&gt;

&lt;p&gt;The first question anyone reasonably asks is: why not just use HTTP?&lt;/p&gt;

&lt;p&gt;HTTP is everywhere. The tooling is excellent. Every language has a client. Debugging with curl is trivial. If I had used HTTP, I would have had working client libraries in a dozen languages before writing a single line of server code.&lt;/p&gt;

&lt;p&gt;The problem is that HTTP is stateless by design. Every request is independent. Every request carries headers. Every response carries headers. The model assumes that each round trip is a fresh conversation with no memory of what came before.&lt;/p&gt;

&lt;p&gt;A database session is the opposite of that. A client connects, authenticates, and then issues many commands over the same connection. The authentication should happen once. The session should carry state. Pipelining requests without waiting for each response to return should be natural, not something you fight the protocol to achieve.&lt;/p&gt;

&lt;p&gt;HTTP/2 closes some of this gap. But using HTTP/2 correctly for a stateful session model involves working against the grain of what HTTP was designed for. I would have been spending a lot of time on infrastructure that exists to make HTTP behave less like HTTP.&lt;/p&gt;

&lt;p&gt;The other issue is overhead. HTTP headers are verbose. For small key-value operations, the headers can easily exceed the payload. That felt wrong for something designed to be a tight operational data store.&lt;/p&gt;

&lt;p&gt;So I went with TCP directly, with a custom framing layer on top.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first thing TCP teaches you
&lt;/h2&gt;

&lt;p&gt;TCP is a stream. Not a sequence of messages. A stream.&lt;/p&gt;

&lt;p&gt;When a client sends two requests back to back, the server cannot assume they arrive as two separate chunks of bytes. They might arrive together. They might arrive in three pieces. One might arrive before the other but the other might split across two read calls.&lt;/p&gt;

&lt;p&gt;The first real problem a custom protocol has to solve is: where does one message end and the next begin?&lt;/p&gt;

&lt;p&gt;The standard answer is a length-prefixed frame. Every message starts with a fixed-size header that includes the length of the payload that follows. The receiver reads the header, learns how long the body is, reads exactly that many bytes, and now has one complete message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+--------+-------+---------+------ ... ------+
| magic  |  ver  |  flags  |    payload      |
| 4 bytes| 1 byte| 2 bytes | length bytes    |
+--------+-------+---------+------ ... ------+
         |                 |
         header (fixed)    body (variable)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Simple in theory. The implementation detail that catches you is the partial read. If you ask for 16 bytes and only 10 arrive, you wait. If you ask for 10,000 bytes and the connection closes after 9,000, you have a truncated frame. Both of these are normal TCP behavior and your parser needs to handle them without panicking or blocking forever.&lt;/p&gt;

&lt;p&gt;In Rust with Tokio this is manageable, but it requires explicit handling. You cannot just call &lt;code&gt;read()&lt;/code&gt; and assume the full frame arrives.&lt;/p&gt;


&lt;h2&gt;
  
  
  Versioning is a commitment you make to every future client
&lt;/h2&gt;

&lt;p&gt;Once you have framing, the next thing you want is versioning. Not because you plan to break anything, but because you will, and you want a way to handle it gracefully when you do.&lt;/p&gt;

&lt;p&gt;VTP2 includes the protocol version in every frame header. This sounds straightforward until you think about what compatibility actually means across versions.&lt;/p&gt;

&lt;p&gt;A client built against protocol version 2 connects to a server running version 3. What should happen? There are two reasonable answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server accepts the connection and negotiates down to the common version.&lt;/li&gt;
&lt;li&gt;The server rejects the connection with a structured error explaining what versions it supports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;VTP2 uses a startup negotiation step. Before any command frames are exchanged, the client sends a hello frame with its protocol version, its client name and version, and the capabilities it wants to use. The server responds with what it accepts.&lt;/p&gt;

&lt;p&gt;This means adding a new capability in a future version is safe because older clients simply do not request it. They get a connection without the new capability and everything works as before.&lt;/p&gt;

&lt;p&gt;What you cannot do without breaking things is change the meaning of an existing opcode or restructure an existing response format. Those are wire-breaking changes. The version number is the signal that something fundamentally changed.&lt;/p&gt;

&lt;p&gt;I learned this the hard way. In an early version of VTP2, I changed the response format for &lt;code&gt;EXEC&lt;/code&gt; to return structured typed results instead of a string list. That was a correctness improvement. It was also a silent breaking change for any client that had already parsed the old response format. Now that is a protocol version boundary: &lt;code&gt;0.2.x&lt;/code&gt; clients are not transaction-wire-compatible with &lt;code&gt;0.3.0&lt;/code&gt; servers, and the changelog says so explicitly.&lt;/p&gt;


&lt;h2&gt;
  
  
  Request IDs are not optional when you pipeline
&lt;/h2&gt;

&lt;p&gt;Early in the design, requests used a local counter for identification. It was simple. It was wrong.&lt;/p&gt;

&lt;p&gt;When you pipeline requests over a single connection, you might have dozens of in-flight requests at the same time with responses arriving in any order depending on how long each operation takes. If two connections both generate request IDs from a local counter, they can collide. If one connection's counter resets, it can collide with itself.&lt;/p&gt;

&lt;p&gt;VTP2 switched to UUIDs for request IDs. Every request carries a UUID. Every response echoes back the same UUID. The client correlates responses to requests using the UUID, not position.&lt;/p&gt;

&lt;p&gt;This removes the ordering assumption entirely. Responses can arrive in any order. The client matches them correctly regardless.&lt;/p&gt;

&lt;p&gt;The cost is 16 bytes per request and per response for the UUID. For the workloads Vaylix targets, that is irrelevant. For a high-throughput system doing millions of tiny operations per second, it might be worth revisiting. For coordination state, it is the right tradeoff.&lt;/p&gt;


&lt;h2&gt;
  
  
  Checksums are the difference between silent corruption and a caught error
&lt;/h2&gt;

&lt;p&gt;A frame travels from the client through the OS, the network stack, maybe some middleware, and into the server. Bytes can be flipped. Not often. Not reliably reproducibly. But it happens.&lt;/p&gt;

&lt;p&gt;Without a checksum, a corrupted frame is processed as if it were valid. The server executes a command with wrong arguments, or writes garbage to the store, or produces a result nobody asked for. The error is silent and the consequences are unpredictable.&lt;/p&gt;

&lt;p&gt;VTP2 includes a checksum in the frame header that covers the payload. If the checksum does not match, the frame is rejected before any processing happens. The client gets a structured error with the expected and actual checksum values. The server logs it. Nothing gets executed.&lt;/p&gt;

&lt;p&gt;One subtlety: Vaylix uses zstd compression on outbound frames above a size threshold. The checksum validates the compressed payload, not the decompressed payload. This means a compression bug that produces different bytes would be caught by the checksum, but a decompression bug that produces different bytes would not. That asymmetry is deliberate and documented, but it is the kind of thing that is easy to get backwards if you do not think it through.&lt;/p&gt;


&lt;h2&gt;
  
  
  Error codes need to be stable forever
&lt;/h2&gt;

&lt;p&gt;Error handling is the part of protocol design that is easiest to under-invest in early and hardest to fix later.&lt;/p&gt;

&lt;p&gt;The naive approach is to return error strings. A server returns "key not found" and the client parses the string. This works until you change the error message for any reason, which you will, and every client that pattern-matched the string silently breaks.&lt;/p&gt;

&lt;p&gt;VTP2 uses structured errors with three fields: a stable numeric code, a stable string name, and a human-readable message that can change freely.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;code:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KEY_NOT_FOUND"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;message:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"the key 'config:env' does not exist"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Client code matches on the numeric code or the name. The message is for humans debugging the problem. You can change the message text without breaking anything. You cannot change the code or the name without a versioned protocol change.&lt;/p&gt;

&lt;p&gt;The error codes are now documented in &lt;code&gt;ERROR_CODES.md&lt;/code&gt; and treated as a stability contract. Any code that shipped in a release will not be reused for a different failure class. Adding new codes is fine. Changing old ones is a breaking change.&lt;/p&gt;

&lt;p&gt;This seems like a lot of discipline for a small project. It is. But the alternative is telling users that their error handling broke because I rewrote a string.&lt;/p&gt;


&lt;h2&gt;
  
  
  Capability negotiation solves the feature drift problem
&lt;/h2&gt;

&lt;p&gt;As a protocol evolves, new features get added. Compression. Request deadlines. Trace context propagation. Metrics. Each of these is useful in some contexts and irrelevant or undesirable in others.&lt;/p&gt;

&lt;p&gt;Hard-coding every feature into every connection creates two problems. Clients that do not need compression still pay the negotiation cost. Servers that add a new feature have no way to know which connected clients support it.&lt;/p&gt;

&lt;p&gt;VTP2 uses capability negotiation in the startup hello. The client lists the capabilities it wants. The server lists what it accepts. The intersection is what the connection uses.&lt;/p&gt;

&lt;p&gt;Current capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;zstd&lt;/code&gt; — frame-level compression&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;request_deadline&lt;/code&gt; — per-request timeout propagation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;server_metrics&lt;/code&gt; — server-side metric events&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pipelining&lt;/code&gt; — explicit pipeline mode&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;trace_context&lt;/code&gt; — distributed trace ID propagation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a new capability in a future release is safe because existing clients just do not request it. The server enables it only for clients that ask. There is no flag day where all clients must be updated simultaneously.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;p&gt;The startup negotiation adds latency to connection establishment. One extra round trip before any commands can be sent. For long-lived connections this is a one-time cost and irrelevant. For workloads with short-lived connections, it adds up.&lt;/p&gt;

&lt;p&gt;If I were starting over, I would think harder about whether the hello/server-hello round trip could be combined with the first command frame or at least pipelined without waiting for the server hello response before sending the first request.&lt;/p&gt;

&lt;p&gt;I also underestimated how much work the per-language SDK burden would be. Every language binding starts from scratch with VTP2. There is no existing tooling, no existing parser, no existing test suite. A first-class TypeScript SDK exists now and a Go SDK is in progress, but each one is weeks of work that would have been avoided with RESP or gRPC.&lt;/p&gt;

&lt;p&gt;Whether that tradeoff was worth it depends on what the protocol enables. For a system where the protocol needs to carry replication metadata, structured error codes, versioned CAS operations, and request deadlines on the same transport, building something that was designed for all of that from the start made the implementation cleaner than it would have been if those features had been retrofitted onto RESP.&lt;/p&gt;

&lt;p&gt;But that is a judgment call that only makes sense in hindsight.&lt;/p&gt;


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

&lt;p&gt;TCP gives you a stream, not messages. Framing is your problem.&lt;/p&gt;

&lt;p&gt;Versioning is a commitment you make to every client that ever connects. Get it wrong early and you pay later.&lt;/p&gt;

&lt;p&gt;Request IDs need to be globally unique if you pipeline.&lt;/p&gt;

&lt;p&gt;Checksums catch corruption before it becomes a silent bug.&lt;/p&gt;

&lt;p&gt;Error codes are forever. Treat them that way from the start.&lt;/p&gt;

&lt;p&gt;Capability negotiation is the only sane way to evolve a protocol without breaking existing clients.&lt;/p&gt;

&lt;p&gt;None of these are surprising in retrospect. Building VTP2 was the only way I was going to understand them properly.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;VTP2 is the transport protocol powering Vaylix, an open source key-value database engine built for operational state that must survive crashes.&lt;/em&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/vaylix" rel="noopener noreferrer"&gt;
        vaylix
      &lt;/a&gt; / &lt;a href="https://github.com/vaylix/vaylix" rel="noopener noreferrer"&gt;
        vaylix
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A key-value database engine in Rust. Custom binary protocol, RBAC, encrypted WAL persistence, Raft-style replication, TLS/mTLS, and binary-safe values with versioned compare-and-set.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Vaylix&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Vaylix is a Rust key/value database built around a strict transport boundary:&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;client -&amp;gt; transport -&amp;gt; TCP/TLS -&amp;gt; transport -&amp;gt; server -&amp;gt; engine
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The current server stores UTF-8 keys with opaque byte values using segmented WAL plus encrypted snapshot persistence. It includes a shared framed binary transport, a Tokio multi-client server, authentication with RBAC, optional TLS/mTLS, default-on frame compression, logical backup/restore commands, offline PITR-oriented storage subcommands, maintenance mode, hash-chained audit logging, and Raft-style HA replication with automatic leader election and quorum-backed writes.&lt;/p&gt;
&lt;p&gt;Detailed architecture context lives in &lt;a href="https://github.com/vaylix/vaylix/LLM.md" rel="noopener noreferrer"&gt;LLM.md&lt;/a&gt;
Benchmark guidance lives in &lt;a href="https://github.com/vaylix/vaylix/BENCHMARKING.md" rel="noopener noreferrer"&gt;BENCHMARKING.md&lt;/a&gt;
Stability and compatibility contracts live in &lt;a href="https://github.com/vaylix/vaylix/STABILITY.md" rel="noopener noreferrer"&gt;STABILITY.md&lt;/a&gt;, &lt;a href="https://github.com/vaylix/vaylix/COMPATIBILITY_1_0.md" rel="noopener noreferrer"&gt;COMPATIBILITY_1_0.md&lt;/a&gt;, &lt;a href="https://github.com/vaylix/vaylix/ERROR_CODES.md" rel="noopener noreferrer"&gt;ERROR_CODES.md&lt;/a&gt;, &lt;a href="https://github.com/vaylix/vaylix/NON_GOALS.md" rel="noopener noreferrer"&gt;NON_GOALS.md&lt;/a&gt;, and &lt;a href="https://github.com/vaylix/vaylix/DEPLOYMENT.md" rel="noopener noreferrer"&gt;DEPLOYMENT.md&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Downloads&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;Release binaries are published from tagged releases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server and client archives: &lt;a href="https://github.com/vaylix/vaylix/releases" rel="noopener noreferrer"&gt;https://github.com/vaylix/vaylix/releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Server image: &lt;code&gt;ghcr.io/vaylix/vaylix:latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Versioned server image example: &lt;code&gt;ghcr.io/vaylix/vaylix:0.9.0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Release builds also publish SBOMs and keyless Sigstore/cosign attestations.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run with Docker&lt;/h2&gt;

&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;docker pull ghcr.io/vaylix/vaylix:latest
docker&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/vaylix/vaylix" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



</description>
      <category>rust</category>
      <category>distributedsystems</category>
      <category>architecture</category>
      <category>database</category>
    </item>
    <item>
      <title>Write-ahead logs: what fsync actually means and why it matters</title>
      <dc:creator>Anapeksha Mukherjee</dc:creator>
      <pubDate>Tue, 09 Jun 2026 08:15:32 +0000</pubDate>
      <link>https://dev.to/anapeksha/write-ahead-logs-what-fsync-actually-means-and-why-it-matters-1e6d</link>
      <guid>https://dev.to/anapeksha/write-ahead-logs-what-fsync-actually-means-and-why-it-matters-1e6d</guid>
      <description>&lt;h2&gt;
  
  
  write() returned OK. Your data did not make it to disk.
&lt;/h2&gt;

&lt;p&gt;There is a line of code that almost every developer has written and trusted completely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nb"&gt;file&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="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It returns. No error. You move on.&lt;/p&gt;

&lt;p&gt;What actually happened is that the operating system accepted your data into a buffer in memory, marked some pages as dirty, and returned control to your program. The data has not touched the disk yet. The kernel will flush it eventually, in batches, when it decides the time is right.&lt;/p&gt;

&lt;p&gt;If the process crashes, or the machine loses power, or the kernel panics between your write and that eventual flush, your data is gone. The write call succeeded. The data did not survive.&lt;/p&gt;

&lt;p&gt;This is not a bug. It is how operating systems work. Buffered writes are one of the most significant performance optimizations in the entire I/O stack. The kernel batches small writes into larger sequential flushes, coalesces writes to the same blocks, and avoids saturating the disk with every individual write call. For most workloads, this is exactly what you want.&lt;/p&gt;

&lt;p&gt;For a database, it is a disaster waiting to happen.&lt;/p&gt;


&lt;h2&gt;
  
  
  The lie at the heart of write()
&lt;/h2&gt;

&lt;p&gt;When you issue a write command on a file descriptor, the data is mainly copied from user space to kernel space into the operating system's buffers. The kernel does not write the data directly to storage. It marks the pages as dirty and returns success to the user. The kernel periodically detects dirty data in its page buffers and writes it lazily in batches, trying to optimize write throughput.&lt;/p&gt;

&lt;p&gt;So when your code does this:&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="k"&gt;mut&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;File&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"data.bin"&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;file&lt;/span&gt;&lt;span class="nf"&gt;.write_all&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;payload&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;// payload is in kernel buffer, not on disk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The write succeeded in the kernel's accounting. It has not reached persistent storage.&lt;/p&gt;

&lt;p&gt;The gap between "write returned OK" and "data is on disk" is the window where a crash causes data loss. For an application that just saved a user's document, this might mean a few seconds of lost work. For a database that just told a client their write succeeded, it means a durability lie.&lt;/p&gt;


&lt;h2&gt;
  
  
  What fsync actually does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fsync()&lt;/code&gt; transfers all modified in-core data of the file referred to by the file descriptor to the disk device so that all changed information can be retrieved even if the system crashes or is rebooted. This includes writing through or flushing a disk cache if present. The call blocks until the device reports that the transfer has completed.&lt;/p&gt;

&lt;p&gt;That blocking is the important part. &lt;code&gt;fsync&lt;/code&gt; does not return until the hardware confirms the data is on non-volatile storage. Your program waits. The disk works. When &lt;code&gt;fsync&lt;/code&gt; returns, you have a guarantee.&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="k"&gt;mut&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;File&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"data.bin"&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;file&lt;/span&gt;&lt;span class="nf"&gt;.write_all&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;payload&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;file&lt;/span&gt;&lt;span class="nf"&gt;.sync_all&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;// blocks until disk confirms&lt;/span&gt;
&lt;span class="c1"&gt;// now you can tell the client OK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The cost is real. Compared to a buffered write, &lt;code&gt;fsync&lt;/code&gt; is slow. A modern NVMe SSD can handle tens of thousands of &lt;code&gt;fsync&lt;/code&gt; operations per second under ideal conditions, but random synchronous writes at scale add up fast. Every database that takes durability seriously has to decide where to put this cost and how to amortize it.&lt;/p&gt;

&lt;p&gt;There is also one subtlety worth knowing: &lt;code&gt;fsync()&lt;/code&gt; does not necessarily ensure that the entry in the directory containing the file has also reached disk. For that, an explicit &lt;code&gt;fsync()&lt;/code&gt; on a file descriptor for the directory is also needed. This matters for atomic file replacement. If you write to a temp file and rename it over the old one, you need to fsync the directory too, or the rename may not survive a crash even if the file contents did.&lt;/p&gt;


&lt;h2&gt;
  
  
  Enter the write-ahead log
&lt;/h2&gt;

&lt;p&gt;The naive solution to the durability problem is to fsync every write. Write the data, fsync, return OK to the client. That is correct but painful because every client write now pays the full disk latency cost synchronously.&lt;/p&gt;

&lt;p&gt;The write-ahead log is the solution that every serious database converged on independently.&lt;/p&gt;

&lt;p&gt;The name Write-Ahead Log says it all. It is a log that gets written before any risky change actually touches the database files.&lt;/p&gt;

&lt;p&gt;Instead of writing changed data directly to its final location on disk, the database first appends a record of the change to a sequential log file, fsyncs that log entry, and only then acknowledges the write to the client. The actual data structures are updated separately, in the background, without being on the critical path for the client's write.&lt;/p&gt;

&lt;p&gt;The write path looks like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client: SET config:env production

1. append entry to WAL:
   [seq:001][term:1][SET config:env production][checksum]

2. fsync WAL segment

3. update in-memory state:
   map.insert("config:env", "production")

4. return OK to client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The disk write is to the WAL, not to the primary data structure. And WAL writes are sequential appends, which are significantly faster than random writes to arbitrary locations in a data file.&lt;/p&gt;

&lt;p&gt;If we follow this procedure, we do not need to flush data pages to disk on every transaction commit, because we know that in the event of a crash we will be able to recover the database using the log: any changes that have not been applied to the data pages can be redone from the WAL records.&lt;/p&gt;


&lt;h2&gt;
  
  
  What crash recovery actually looks like
&lt;/h2&gt;

&lt;p&gt;When the process starts after a crash, it cannot trust its in-memory state because that state is gone. It cannot trust its primary data structures entirely because they may reflect partial writes. What it can trust is the WAL, because every entry in the WAL was fsynced before the client was told OK.&lt;/p&gt;

&lt;p&gt;Recovery is replay:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;startup:
  1. load last known good snapshot (if any)
  2. find WAL segments that postdate the snapshot
  3. replay each entry in strict sequence order
  4. skip entries past the last committed sequence
  5. in-memory state is now consistent with last committed write
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is deterministic. Given the same WAL, recovery always produces the same state. There is no ambiguity about what happened before the crash.&lt;/p&gt;

&lt;p&gt;The sequence numbers matter. If a WAL segment has a gap, the entries after the gap are suspect. If entries are out of order, replay cannot be trusted. If a checksum fails on a WAL entry, that entry is corrupt and recovery should fail closed rather than apply a corrupted change.&lt;/p&gt;

&lt;p&gt;In Vaylix, startup recovery fails closed on any of these conditions:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gap in sequence numbers → startup error, actionable message
out of order entries    → startup error
checksum mismatch       → startup error
unsupported format      → startup error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Failing closed is the correct choice. A database that recovers silently from a corrupt WAL and presents a subtly wrong state is more dangerous than one that refuses to start and tells you exactly what is wrong.&lt;/p&gt;


&lt;h2&gt;
  
  
  The snapshot problem
&lt;/h2&gt;

&lt;p&gt;WAL segments cannot grow forever. If a process has run for months and handled millions of writes, replaying the entire WAL history on every restart is not practical.&lt;/p&gt;

&lt;p&gt;The solution is snapshots. Periodically, the database serializes its entire current state to disk as a point-in-time snapshot. After a successful snapshot, WAL segments that predate it can be discarded. Recovery becomes: load the snapshot, then replay only the WAL entries that came after it.&lt;/p&gt;

&lt;p&gt;But snapshots introduce their own failure modes.&lt;/p&gt;

&lt;p&gt;What if the process crashes while writing the snapshot? A half-written snapshot file is worse than no snapshot at all, because you might load it and think you have valid state when you have garbage.&lt;/p&gt;

&lt;p&gt;The solution is the atomic rename pattern:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. serialize current state
2. write to a temporary file: snapshot.tmp
3. fsync the temporary file
4. fsync the parent directory
5. atomically rename snapshot.tmp → snapshot
6. fsync the parent directory again
7. write manifest pointing to new snapshot
8. fsync manifest
9. prune old WAL segments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The rename is atomic on all major filesystems. Either the old snapshot is there or the new one is. There is no state where a half-written file is the active snapshot.&lt;/p&gt;

&lt;p&gt;The directory fsyncs around the rename are easy to forget and important. Without them, the rename itself may not survive a crash even if the file contents are fine.&lt;/p&gt;


&lt;h2&gt;
  
  
  WAL segments and retention
&lt;/h2&gt;

&lt;p&gt;A single WAL file that grows indefinitely would be inefficient to manage. Most implementations split the WAL into fixed-size segments.&lt;/p&gt;

&lt;p&gt;In Vaylix, the active segment is named to reflect its starting sequence:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;wal/&lt;/span&gt;
  &lt;span class="s"&gt;active-000001.wal       ← current segment, still being written&lt;/span&gt;
  &lt;span class="s"&gt;000001-000500.wal       ← sealed segment&lt;/span&gt;
  &lt;span class="s"&gt;000501-001000.wal       ← sealed segment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When the active segment reaches the configured size limit, it is sealed: renamed to include its sequence range, and a new active segment is opened. Segments older than the configured retention window are pruned after a successful snapshot.&lt;/p&gt;

&lt;p&gt;The sealed segment names encode their sequence range deliberately. During recovery, the system can determine the correct replay order from the filenames alone, without needing to open every segment to find its position.&lt;/p&gt;


&lt;h2&gt;
  
  
  Point-in-time recovery
&lt;/h2&gt;

&lt;p&gt;A WAL that records every change is also a time machine.&lt;/p&gt;

&lt;p&gt;If you have a snapshot from Monday night and all WAL segments since then, you can replay the WAL to any point in time: to just before a bad deploy at 2pm Tuesday, to exactly the state the database was in when a bug was first reported, or to any sequence number you choose.&lt;/p&gt;

&lt;p&gt;This is what PITR (point-in-time recovery) means in practice. It is not magic. It is just WAL replay stopped at a specific boundary.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vaylix pitr restore &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-dir&lt;/span&gt; /var/lib/vaylix &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-dir&lt;/span&gt; /var/lib/vaylix-restored &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--to-timestamp-ms&lt;/span&gt; 1749200000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The restore writes to a new target directory and never touches the source. If the restore produces the wrong state, you still have the original data intact to try again.&lt;/p&gt;


&lt;h2&gt;
  
  
  What this cost in practice
&lt;/h2&gt;

&lt;p&gt;Building the WAL in Vaylix surfaced a few things that are easy to get wrong.&lt;/p&gt;

&lt;p&gt;The WAL I/O worker runs on a dedicated thread, separate from the engine coordinator that assigns sequence numbers. Sequence assignment is fast and in-memory. The disk I/O is pushed off the hot path. But those two things have to stay in sync: the engine worker must not acknowledge a write to a client until the WAL I/O worker confirms the corresponding entry has been fsynced.&lt;/p&gt;

&lt;p&gt;Getting that handoff wrong in either direction is a bug. Too early and you have the durability problem again. Too late and you serialize unnecessarily and hurt throughput.&lt;/p&gt;

&lt;p&gt;The checksum on each WAL entry is also not optional. Without it, a partial write to the WAL is indistinguishable from a complete one. The checksum is what lets recovery distinguish a truncated entry, which happens when the process is killed mid-append, from a valid entry that happens to contain unusual bytes.&lt;/p&gt;

&lt;p&gt;Directory fsyncs, as mentioned above, are easy to forget. They are not obviously required by the code. They show up as flaky data loss that only reproduces under specific crash conditions during specific filesystem operations. They are the kind of thing that only gets discovered through careful testing or production incidents.&lt;/p&gt;


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

&lt;p&gt;&lt;code&gt;write()&lt;/code&gt; buffers data in the kernel. The data is not on disk until something forces it there.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fsync()&lt;/code&gt; forces it there and blocks until the hardware confirms.&lt;/p&gt;

&lt;p&gt;A write-ahead log puts &lt;code&gt;fsync&lt;/code&gt; on the path of every acknowledged write, but makes that cost manageable by writing to a sequential append-only log rather than to arbitrary locations in the primary data structure.&lt;/p&gt;

&lt;p&gt;On crash, replay the WAL to reconstruct state. On growth, take snapshots and prune old segments.&lt;/p&gt;

&lt;p&gt;The details are subtle: sequence gaps, checksums, directory fsyncs, the atomic rename pattern, the ordering of snapshot and WAL pruning steps. Each one is a failure mode that shows up in the wrong conditions at the wrong time.&lt;/p&gt;

&lt;p&gt;But the core idea is simple. Write first, confirm later, replay if needed.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;Vaylix is an open source key-value database engine built around these principles. The WAL implementation, crash recovery path, and snapshot logic are all in the public repository.&lt;/em&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/vaylix" rel="noopener noreferrer"&gt;
        vaylix
      &lt;/a&gt; / &lt;a href="https://github.com/vaylix/vaylix" rel="noopener noreferrer"&gt;
        vaylix
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A key-value database engine in Rust. Custom binary protocol, RBAC, encrypted WAL persistence, Raft-style replication, TLS/mTLS, and binary-safe values with versioned compare-and-set.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Vaylix&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Vaylix is a Rust key/value database built around a strict transport boundary:&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;client -&amp;gt; transport -&amp;gt; TCP/TLS -&amp;gt; transport -&amp;gt; server -&amp;gt; engine
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The current server stores UTF-8 keys with opaque byte values using segmented WAL plus encrypted snapshot persistence. It includes a shared framed binary transport, a Tokio multi-client server, authentication with RBAC, optional TLS/mTLS, default-on frame compression, logical backup/restore commands, offline PITR-oriented storage subcommands, maintenance mode, hash-chained audit logging, and Raft-style HA replication with automatic leader election and quorum-backed writes.&lt;/p&gt;
&lt;p&gt;Detailed architecture context lives in &lt;a href="https://github.com/vaylix/vaylix/LLM.md" rel="noopener noreferrer"&gt;LLM.md&lt;/a&gt;
Benchmark guidance lives in &lt;a href="https://github.com/vaylix/vaylix/BENCHMARKING.md" rel="noopener noreferrer"&gt;BENCHMARKING.md&lt;/a&gt;
Stability and compatibility contracts live in &lt;a href="https://github.com/vaylix/vaylix/STABILITY.md" rel="noopener noreferrer"&gt;STABILITY.md&lt;/a&gt;, &lt;a href="https://github.com/vaylix/vaylix/COMPATIBILITY_1_0.md" rel="noopener noreferrer"&gt;COMPATIBILITY_1_0.md&lt;/a&gt;, &lt;a href="https://github.com/vaylix/vaylix/ERROR_CODES.md" rel="noopener noreferrer"&gt;ERROR_CODES.md&lt;/a&gt;, &lt;a href="https://github.com/vaylix/vaylix/NON_GOALS.md" rel="noopener noreferrer"&gt;NON_GOALS.md&lt;/a&gt;, and &lt;a href="https://github.com/vaylix/vaylix/DEPLOYMENT.md" rel="noopener noreferrer"&gt;DEPLOYMENT.md&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Downloads&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;Release binaries are published from tagged releases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server and client archives: &lt;a href="https://github.com/vaylix/vaylix/releases" rel="noopener noreferrer"&gt;https://github.com/vaylix/vaylix/releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Server image: &lt;code&gt;ghcr.io/vaylix/vaylix:latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Versioned server image example: &lt;code&gt;ghcr.io/vaylix/vaylix:0.9.0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Release builds also publish SBOMs and keyless Sigstore/cosign attestations.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run with Docker&lt;/h2&gt;

&lt;/div&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;docker pull ghcr.io/vaylix/vaylix:latest
docker&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/vaylix/vaylix" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



</description>
      <category>database</category>
      <category>rust</category>
      <category>distributedsystems</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Raft, Quorum, and High Availability</title>
      <dc:creator>Anapeksha Mukherjee</dc:creator>
      <pubDate>Sun, 07 Jun 2026 13:01:59 +0000</pubDate>
      <link>https://dev.to/anapeksha/raft-quorum-and-high-availability-5684</link>
      <guid>https://dev.to/anapeksha/raft-quorum-and-high-availability-5684</guid>
      <description>&lt;p&gt;If you run one database node, life is simple. There is one copy of the data, one process accepting writes, and one place to recover from.&lt;/p&gt;

&lt;p&gt;The moment you run three nodes, you get a harder question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If these machines disagree, which one is right?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the problem Raft is built to solve.&lt;/p&gt;

&lt;p&gt;Raft is a consensus algorithm. More specifically, it is a way for a group of machines to agree on a single ordered log of operations, even when some machines crash, restart, lag behind, or lose network connectivity.&lt;/p&gt;

&lt;p&gt;It shows up in databases, metadata stores, schedulers, service discovery systems, and control planes because it gives those systems a practical answer to a dangerous question: how do we stay available without letting different nodes invent different versions of reality?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;Raft turns a cluster of nodes into one logical state machine.&lt;/p&gt;

&lt;p&gt;Instead of letting every node mutate state independently, Raft makes nodes agree on a log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. SET user:1 "Ada"
2. SET user:2 "Grace"
3. DELETE session:abc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each node applies the same committed log entries in the same order. If the state machine is deterministic, the nodes end up with the same state.&lt;/p&gt;

&lt;p&gt;That is the whole trick.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;same log + same order + deterministic apply = same state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Raft is not really about key-value stores or SQL or config files. Those are just things you can build on top. Raft is about agreeing on the order of changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Roles
&lt;/h2&gt;

&lt;p&gt;Each Raft node is in one of three roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Follower&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Candidate&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leader&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A follower is passive. It accepts messages from the leader and votes in elections.&lt;/p&gt;

&lt;p&gt;A candidate is trying to become leader.&lt;/p&gt;

&lt;p&gt;The leader is the node currently responsible for handling writes and replicating log entries to the rest of the cluster.&lt;/p&gt;

&lt;p&gt;In a healthy cluster, there is one leader for the current term.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node-a: leader
node-b: follower
node-c: follower
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clients usually send writes to the leader. Some systems let clients connect to any node, but under the hood the write still has to reach the leader or go through an equivalent consensus path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terms: Raft's Logical Clock
&lt;/h2&gt;

&lt;p&gt;Raft uses numbered terms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;term 1
term 2
term 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A term is not wall-clock time. It is a logical epoch.&lt;/p&gt;

&lt;p&gt;Every election happens in a term. If a candidate wins, it becomes leader for that term.&lt;/p&gt;

&lt;p&gt;Terms help nodes detect stale information. If a node receives a message from an older term, it knows the sender is behind. If it sees a newer term, it updates itself and steps down if needed.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node-a thinks it is leader in term 4
node-b has already seen term 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;node-a&lt;/code&gt; sends a leader message with term &lt;code&gt;4&lt;/code&gt;, &lt;code&gt;node-b&lt;/code&gt; rejects it. That prevents old leaders from continuing to act authoritative after the cluster has moved on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Quorum Means
&lt;/h2&gt;

&lt;p&gt;A quorum is the minimum number of voting nodes needed to make a decision.&lt;/p&gt;

&lt;p&gt;In Raft, quorum usually means a majority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;quorum = floor(N / 2) + 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Voting Nodes&lt;/th&gt;
&lt;th&gt;Quorum&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important part is not the formula. The important part is overlap.&lt;/p&gt;

&lt;p&gt;Any two majorities must share at least one node.&lt;/p&gt;

&lt;p&gt;In a 3-node cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;majority 1: node-a + node-b
majority 2: node-b + node-c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both include &lt;code&gt;node-b&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In a 5-node cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;majority 1: node-a + node-b + node-c
majority 2: node-c + node-d + node-e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both include &lt;code&gt;node-c&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That overlap is what stops two isolated groups from making conflicting committed decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Quorum Matters
&lt;/h2&gt;

&lt;p&gt;Suppose we have three nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node-a
node-b
node-c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The leader receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SET color "blue"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the leader writes locally and immediately returns success, the write is fragile. The leader could crash before anyone else receives the entry.&lt;/p&gt;

&lt;p&gt;Raft waits for the entry to be replicated to a quorum.&lt;/p&gt;

&lt;p&gt;For three nodes, quorum is two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node-a: SET color "blue"
node-b: SET color "blue"
node-c: not yet replicated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once two nodes have the entry, the leader can mark it committed.&lt;/p&gt;

&lt;p&gt;Why is that safe?&lt;/p&gt;

&lt;p&gt;Because any future leader also needs a quorum to win an election. Since quorums overlap, a future leader election must involve at least one node that knows about the committed entry.&lt;/p&gt;

&lt;p&gt;That is the safety property. A committed entry cannot simply vanish because one machine died.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leader Election
&lt;/h2&gt;

&lt;p&gt;Followers expect regular heartbeats from the leader.&lt;/p&gt;

&lt;p&gt;If a follower does not hear from the leader before its election timeout, it assumes the leader may be gone and starts an election:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It becomes a candidate.&lt;/li&gt;
&lt;li&gt;It increments its term.&lt;/li&gt;
&lt;li&gt;It votes for itself.&lt;/li&gt;
&lt;li&gt;It asks the other nodes for votes.&lt;/li&gt;
&lt;li&gt;If it receives votes from a quorum, it becomes leader.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a 3-node cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node-a: candidate, votes for node-a
node-b: votes for node-a
node-c: no response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;node-a&lt;/code&gt; has two votes. That is a majority, so it becomes leader.&lt;/p&gt;

&lt;p&gt;Raft uses randomized election timeouts to reduce split votes. If every follower started an election at exactly the same time, each might vote for itself and nobody would win. Randomized timeouts make it likely that one node starts first and collects votes before the others become candidates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log Replication
&lt;/h2&gt;

&lt;p&gt;Once there is a leader, writes go through the leader.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client -&amp;gt; leader: SET user:1 "Ada"
leader: append entry to local log
leader -&amp;gt; followers: replicate entry
followers -&amp;gt; leader: acknowledged
leader: commit after quorum
leader -&amp;gt; client: success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The leader does not need every follower to acknowledge the write. It needs a quorum.&lt;/p&gt;

&lt;p&gt;In a 5-node cluster, quorum is 3. The leader plus two followers is enough to commit.&lt;/p&gt;

&lt;p&gt;That is why a Raft cluster can keep working even when some nodes are down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Committed vs Applied
&lt;/h2&gt;

&lt;p&gt;Two words matter a lot in Raft:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;committed&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;applied&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An entry is committed when Raft has made it durable according to the quorum rule.&lt;/p&gt;

&lt;p&gt;An entry is applied when a node has actually executed it against its local state machine.&lt;/p&gt;

&lt;p&gt;A follower can know that entry &lt;code&gt;100&lt;/code&gt; is committed but only have applied through entry &lt;code&gt;98&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That distinction matters for reads. A node that has not applied the latest committed entries may return stale data if it serves reads directly from local state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reads Are Subtle
&lt;/h2&gt;

&lt;p&gt;Writes naturally go through the log. Reads are easier to get wrong.&lt;/p&gt;

&lt;p&gt;Imagine an old leader gets separated from the rest of the cluster. It still has data. It still has a process running. It may even still believe it is leader for a short time.&lt;/p&gt;

&lt;p&gt;Meanwhile, the majority side elects a new leader and commits newer writes.&lt;/p&gt;

&lt;p&gt;If the old leader serves reads without proving it still has authority, it can return stale answers.&lt;/p&gt;

&lt;p&gt;There are a few common ways to handle this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Route Reads To The Leader
&lt;/h3&gt;

&lt;p&gt;This is simple and common.&lt;/p&gt;

&lt;p&gt;The read goes to the leader, and the leader confirms it still has quorum authority before serving the read.&lt;/p&gt;

&lt;p&gt;This is correct, but all linearizable read traffic goes through the leader.&lt;/p&gt;

&lt;h3&gt;
  
  
  ReadIndex
&lt;/h3&gt;

&lt;p&gt;Raft ReadIndex is an optimization.&lt;/p&gt;

&lt;p&gt;The leader establishes a safe read point, usually by confirming authority with a quorum. A follower can then wait until it has applied through that index and serve the read locally.&lt;/p&gt;

&lt;p&gt;The important part is the wait:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;safe read index = 120
follower applied index = 118
wait
follower applied index = 120
serve read
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without that wait, the follower may be stale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lease Reads
&lt;/h3&gt;

&lt;p&gt;Lease reads rely on timing assumptions. The leader assumes it remains valid for a lease window after contacting quorum.&lt;/p&gt;

&lt;p&gt;They can be fast, but they require careful timeout and clock assumptions. Getting them wrong can break consistency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stale Reads
&lt;/h3&gt;

&lt;p&gt;Some systems intentionally allow local follower reads because they are fast and distribute load.&lt;/p&gt;

&lt;p&gt;That is fine when the API is honest about it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;linearizable read: latest committed state
stale read: local replica state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trouble starts when stale reads are presented as if they are strongly consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  High Availability: What Raft Actually Gives You
&lt;/h2&gt;

&lt;p&gt;High availability does not mean every node can always accept writes.&lt;/p&gt;

&lt;p&gt;In a strongly consistent Raft system, availability means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The cluster can continue accepting safe writes as long as a quorum is alive and connected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For three nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;quorum = 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cluster can tolerate one failed node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node-a: alive
node-b: alive
node-c: down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For five nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;quorum = 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cluster can tolerate two failed nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node-a: alive
node-b: alive
node-c: alive
node-d: down
node-e: down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In general, a cluster of &lt;code&gt;2f + 1&lt;/code&gt; voting nodes can tolerate &lt;code&gt;f&lt;/code&gt; failures.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Nodes&lt;/th&gt;
&lt;th&gt;Quorum&lt;/th&gt;
&lt;th&gt;Failures Tolerated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is why odd-sized clusters are common.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Two Nodes Are Usually Not Enough
&lt;/h2&gt;

&lt;p&gt;A 2-node cluster has quorum 2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node-a
node-b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If either node fails, only one node remains. One node is not a quorum, so the cluster cannot safely elect a leader or commit writes.&lt;/p&gt;

&lt;p&gt;That means a 2-node Raft cluster often gives you redundancy without useful write availability.&lt;/p&gt;

&lt;p&gt;Three nodes is usually the smallest practical production setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network Partitions
&lt;/h2&gt;

&lt;p&gt;Network partitions are where quorum really earns its keep.&lt;/p&gt;

&lt;p&gt;Take a 5-node cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node-a
node-b
node-c
node-d
node-e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now split the network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;partition 1: node-a, node-b
partition 2: node-c, node-d, node-e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quorum is 3.&lt;/p&gt;

&lt;p&gt;Partition 1 has only 2 nodes, so it cannot elect a leader or commit writes.&lt;/p&gt;

&lt;p&gt;Partition 2 has 3 nodes, so it can continue.&lt;/p&gt;

&lt;p&gt;This prevents split brain. The minority side may be alive, but it cannot make authoritative decisions.&lt;/p&gt;

&lt;p&gt;That is the tradeoff: Raft sacrifices availability on the minority side to protect consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens During Failover?
&lt;/h2&gt;

&lt;p&gt;Suppose the leader dies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node-a: leader, down
node-b: follower
node-c: follower
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The followers stop receiving heartbeats. After an election timeout, one follower becomes candidate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node-b: candidate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It requests votes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node-b votes for node-b
node-c votes for node-b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;node-b&lt;/code&gt; has quorum and becomes leader.&lt;/p&gt;

&lt;p&gt;There is usually a short window where writes are unavailable. Once a new leader is elected, the cluster can accept writes again.&lt;/p&gt;

&lt;p&gt;This is high availability with a consistency boundary. The system pauses rather than allowing two leaders to accept conflicting writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repairing Divergent Logs
&lt;/h2&gt;

&lt;p&gt;Followers can fall behind. They can also contain entries that were written by an old leader but never committed.&lt;/p&gt;

&lt;p&gt;Raft repairs this through log matching.&lt;/p&gt;

&lt;p&gt;Append requests include information about the entry immediately before the new entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;previous log index
previous log term
new entries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the follower does not have the expected previous entry, it rejects the append. The leader backs up and tries again from an earlier point.&lt;/p&gt;

&lt;p&gt;Once the leader finds a matching prefix, it overwrites the follower's conflicting suffix.&lt;/p&gt;

&lt;p&gt;That sounds aggressive, but it is correct. Uncommitted entries are not guaranteed to survive. Committed entries are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Snapshots And Compaction
&lt;/h2&gt;

&lt;p&gt;Logs cannot grow forever.&lt;/p&gt;

&lt;p&gt;If a system has applied millions of entries, it should not need to keep every old entry around just to recover.&lt;/p&gt;

&lt;p&gt;A snapshot captures the state machine at a specific log index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;snapshot at index 1,000,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, older log entries can be compacted.&lt;/p&gt;

&lt;p&gt;If a follower is far behind, the leader may send a snapshot instead of trying to stream a huge log history.&lt;/p&gt;

&lt;p&gt;Snapshots are not an optional polish feature in real systems. They are part of making Raft operationally practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Membership Changes
&lt;/h2&gt;

&lt;p&gt;Adding or removing voting nodes is harder than it looks because membership changes alter quorum.&lt;/p&gt;

&lt;p&gt;If different parts of the cluster disagree about who can vote, you can accidentally create two groups that both think they have authority.&lt;/p&gt;

&lt;p&gt;That is why membership changes must go through the replicated log too.&lt;/p&gt;

&lt;p&gt;The cluster has to agree on configuration changes with the same care it uses for data changes.&lt;/p&gt;

&lt;p&gt;Different Raft implementations handle this in different ways, often with joint consensus or carefully sequenced configuration transitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Raft Guarantees
&lt;/h2&gt;

&lt;p&gt;Raft is designed around a few key safety properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Election safety:&lt;/strong&gt; at most one leader is elected in a term.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leader append-only:&lt;/strong&gt; a leader does not rewrite its own log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log matching:&lt;/strong&gt; matching entries imply matching prior history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leader completeness:&lt;/strong&gt; committed entries appear in future leaders' logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State machine safety:&lt;/strong&gt; two nodes do not apply different commands at the same log index.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These properties let a group of machines behave like one ordered state machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Raft Does Not Solve
&lt;/h2&gt;

&lt;p&gt;Raft is not a complete database.&lt;/p&gt;

&lt;p&gt;It does not automatically fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bad disk durability settings&lt;/li&gt;
&lt;li&gt;overloaded nodes&lt;/li&gt;
&lt;li&gt;slow replication links&lt;/li&gt;
&lt;li&gt;unsafe read paths&lt;/li&gt;
&lt;li&gt;poor timeout tuning&lt;/li&gt;
&lt;li&gt;multi-shard transactions&lt;/li&gt;
&lt;li&gt;application-level conflicts&lt;/li&gt;
&lt;li&gt;bad operational procedures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Raft gives you a way to agree on ordered changes. The system around it still has to use that agreement correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Acknowledging Writes Too Early
&lt;/h3&gt;

&lt;p&gt;If a leader returns success before quorum replication, a successful write can disappear during failover.&lt;/p&gt;

&lt;p&gt;The safe flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;append locally
replicate to quorum
mark committed
respond success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Treating Follower Reads As Strong Reads
&lt;/h3&gt;

&lt;p&gt;Follower reads are not automatically linearizable.&lt;/p&gt;

&lt;p&gt;They are fine if they are documented as stale reads. They are dangerous if callers expect read-after-write consistency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Letting Old Leaders Serve Reads
&lt;/h3&gt;

&lt;p&gt;A partitioned leader can still be alive.&lt;/p&gt;

&lt;p&gt;Before serving a strong read, a leader needs proof that it still has authority. Otherwise it may return stale state after a new leader has already been elected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making Timeouts Too Aggressive
&lt;/h3&gt;

&lt;p&gt;Short election timeouts can make a healthy cluster flap during latency spikes.&lt;/p&gt;

&lt;p&gt;Long election timeouts make failover slow.&lt;/p&gt;

&lt;p&gt;There is no universal perfect value. Timeouts have to match the environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Good Mental Model
&lt;/h2&gt;

&lt;p&gt;Raft is not "replication" in the casual sense of copying bytes to other machines.&lt;/p&gt;

&lt;p&gt;It is stricter than that.&lt;/p&gt;

&lt;p&gt;Raft is agreement over history.&lt;/p&gt;

&lt;p&gt;The leader proposes the next entries. A quorum decides which entries are durable enough to become part of the cluster's history. Every node applies that history in order.&lt;/p&gt;

&lt;p&gt;The shortest version is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raft = one agreed log, replicated by majority decisions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is why quorum matters. It makes sure every committed decision intersects with future decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;High availability is not just keeping processes alive. A system that stays online but returns conflicting answers is not healthy.&lt;/p&gt;

&lt;p&gt;Raft gives distributed systems a way to remain available on the majority side of failures while protecting the integrity of committed state.&lt;/p&gt;

&lt;p&gt;That is the tradeoff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep serving when a quorum is available&lt;/li&gt;
&lt;li&gt;stop serving authoritative writes when a quorum is not available&lt;/li&gt;
&lt;li&gt;never let two disconnected groups commit conflicting histories&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once that clicks, Raft becomes less mysterious. It is a disciplined way to decide which changes become real.&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>database</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why I Stopped Using Redis for Coordination State and Built Something Else</title>
      <dc:creator>Anapeksha Mukherjee</dc:creator>
      <pubDate>Fri, 05 Jun 2026 20:42:46 +0000</pubDate>
      <link>https://dev.to/anapeksha/why-i-stopped-using-redis-for-coordination-state-and-built-something-else-2ab7</link>
      <guid>https://dev.to/anapeksha/why-i-stopped-using-redis-for-coordination-state-and-built-something-else-2ab7</guid>
      <description>&lt;p&gt;I have been building AuthSafe, a developer auth platform, for three years. Auth infrastructure is unforgiving about correctness. A stale session state, a dropped rate limit counter, a coordination write that silently vanished, these are not edge cases you can wave away. They are production bugs with real consequences.&lt;/p&gt;

&lt;p&gt;For most of those three years, I used Redis for coordination state. Rate limiting counters. Session metadata. Configuration that had to survive a restart. It worked well enough that I did not question it seriously until I started digging into what well enough actually meant on the durability side.&lt;/p&gt;

&lt;p&gt;What I found made me uncomfortable enough to build something else. That project is Vaylix.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Default Redis Durability Story Is Worse Than Most People Think
&lt;/h2&gt;

&lt;p&gt;Here is the part that surprised me: AOF persistence, the mechanism that gives Redis its best durability story, is &lt;strong&gt;disabled by default&lt;/strong&gt; in open source Redis. What runs out of the box is RDB snapshotting, which takes periodic point-in-time snapshots of the dataset. The default RDB configuration triggers a snapshot after 3600 seconds if at least one key changed, after 300 seconds if at least 10 keys changed, and after 60 seconds if at least 10,000 keys changed.&lt;/p&gt;

&lt;p&gt;Which means under default configuration, a Redis crash can lose anywhere from one minute to one hour of writes depending on write volume. The client got &lt;code&gt;OK&lt;/code&gt; back. The data is gone.&lt;/p&gt;

&lt;p&gt;If you enable AOF and set &lt;code&gt;appendfsync everysec&lt;/code&gt;, that window shrinks to approximately one second. That is the configuration most production guides recommend and what Redis's own documentation describes as "fast enough and relatively safe." But one second of acknowledged writes disappearing is still a meaningful data loss window for coordination state, and &lt;code&gt;appendfsync always&lt;/code&gt;, which fsyncs on every write, drops throughput by over 500 times compared to the default — from tens of thousands of operations per second down to a few thousand at best on SSDs.&lt;/p&gt;

&lt;p&gt;None of this is a criticism of Redis. These are deliberate tradeoffs, documented openly, that make total sense for a system designed primarily as a cache. Redis is fast because it does not pay the full durability cost on every write. That is the right design for the workload it was built for.&lt;/p&gt;

&lt;p&gt;For coordination state in an auth platform, it was the wrong design for my workload.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Did Not Just Switch to etcd
&lt;/h2&gt;

&lt;p&gt;etcd is the standard answer for strongly consistent key-value storage. It is battle-tested, runs at significant scale inside Kubernetes clusters worldwide, and its durability guarantees are genuine. Writes are not acknowledged until they are committed through Raft consensus and fsynced.&lt;/p&gt;

&lt;p&gt;The problem is not etcd's correctness. The problem is that etcd's entire operational identity is Kubernetes infrastructure. Its documentation, its deployment patterns, its client API, its watch semantics — all shaped by that context. I spent two days trying to set up a simple three-node etcd cluster for a non-Kubernetes workload and kept hitting documentation that assumed I was configuring cluster state for container orchestration.&lt;/p&gt;

&lt;p&gt;The operational weight was not justified for what I needed. I did not need etcd. I needed what etcd guarantees, without etcd's context.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Actually Needed
&lt;/h2&gt;

&lt;p&gt;Stripped down to specifics:&lt;/p&gt;

&lt;p&gt;Every acknowledged write had to survive a process crash. Not probably. Not within one second. Every write.&lt;/p&gt;

&lt;p&gt;Reads had to be consistent with the latest committed write on the same connection. No stale replica reads for session-critical paths.&lt;/p&gt;

&lt;p&gt;The security model had to be granular enough that different internal services could access different key namespaces without sharing a single global credential.&lt;/p&gt;

&lt;p&gt;The deployment had to be operationally simple for a two or three node setup without a dedicated infrastructure team.&lt;/p&gt;

&lt;p&gt;That is a narrow requirements list. No complex queries, no document storage, no pub/sub. Just a key-value store with correct durability semantics and a sensible auth model.&lt;/p&gt;




&lt;h2&gt;
  
  
  So I Built It
&lt;/h2&gt;

&lt;p&gt;I had been learning Rust seriously and wanted a project with real systems constraints rather than toy complexity. The requirements above were specific enough to guide every architectural decision.&lt;/p&gt;

&lt;p&gt;The durability foundation is a write-ahead log with fsync. Every write goes to the WAL and is fsynced before the client receives acknowledgement. On restart, the WAL replays to reconstruct state. No acknowledged write is lost.&lt;/p&gt;

&lt;p&gt;On top of that is Raft-style replication. Writes are not acknowledged until a quorum of nodes confirms receipt. Which means even a leader crash immediately after acknowledgement leaves the data on a majority of nodes.&lt;/p&gt;

&lt;p&gt;The wire protocol is a custom framed binary format rather than HTTP. Persistent connections with capability negotiation at startup, low per-request overhead, pipelined requests with UUID-based correlation. More work to build than wrapping HTTP, but the right design for stateful sessions.&lt;/p&gt;

&lt;p&gt;Authentication and RBAC are on by default. Permissions are pattern-scoped at the key level, so a rate limiting service can read and write &lt;code&gt;ratelimit:*&lt;/code&gt; without touching &lt;code&gt;config:*&lt;/code&gt; or &lt;code&gt;session:*&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Honest Tradeoffs
&lt;/h2&gt;

&lt;p&gt;Vaylix is slower than Redis on raw throughput. Significantly slower under &lt;code&gt;appendfsync always&lt;/code&gt; comparison. That gap is structural, not an optimization problem. Vaylix fsyncs every write, replicates to a quorum before acknowledging, and runs a serialized engine worker. Redis with default or &lt;code&gt;everysec&lt;/code&gt; configuration skips most of that work. The latency difference reflects different guarantees, not different levels of engineering effort.&lt;/p&gt;

&lt;p&gt;If you need a cache, a leaderboard, a job queue where occasional loss is tolerable, or a pub/sub bus, Redis is the right tool. Vaylix is not.&lt;/p&gt;

&lt;p&gt;If you need acknowledged writes to survive crashes without configuration gymnastics, and you want a security model that does not require every internal service to share a root credential, Vaylix is the gap it was built to fill.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where It Sits Now
&lt;/h2&gt;

&lt;p&gt;Vaylix is three months old and running inside AuthSafe in production for rate limiting and coordination state. It is the first real test of whether the design holds under actual operational conditions. So far the failure modes have been explicit rather than silent, which is what you want from infrastructure you are trusting with correctness.&lt;/p&gt;

&lt;p&gt;It is pre-1.0. The roadmap has richer transaction semantics, better cluster tooling, and more client SDKs. Sharding and MVCC are explicitly deferred until the core model is proven by real usage.&lt;/p&gt;

&lt;p&gt;The project is open source under MIT.&lt;/p&gt;

&lt;p&gt;If you have been running Redis for coordination workloads and quietly uncomfortable about the durability configuration, or if you have looked at etcd and decided the operational overhead is not worth it for a non-Kubernetes context, I would genuinely value your feedback on whether Vaylix fits the gap you have been working around.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Engine:&lt;/strong&gt; &lt;a href="https://github.com/vaylix/vaylix" rel="noopener noreferrer"&gt;https://github.com/vaylix/vaylix&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;TypeScript SDK:&lt;/strong&gt; &lt;a href="https://github.com/vaylix/vaylix-ts" rel="noopener noreferrer"&gt;https://github.com/vaylix/vaylix-ts&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://vaylix.github.io" rel="noopener noreferrer"&gt;https://vaylix.github.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>distributedsystems</category>
      <category>database</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
