<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>ZeroFS</title>
    <subtitle>A log-structured filesystem for S3-compatible object storage.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://www.zerofs.net/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://www.zerofs.net"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-07-11T00:00:00+00:00</updated>
    <id>https://www.zerofs.net/atom.xml</id>
    <entry xml:lang="en">
        <title>ZeroFS vs. Amazon S3 Files</title>
        <published>2026-07-11T00:00:00+00:00</published>
        <updated>2026-07-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            Pierre Barre
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.zerofs.net/blog/zerofs-vs-aws-s3-files/"/>
        <id>https://www.zerofs.net/blog/zerofs-vs-aws-s3-files/</id>
        
        <content type="html" xml:base="https://www.zerofs.net/blog/zerofs-vs-aws-s3-files/">&lt;p class=&quot;lede rv&quot;&gt;Amazon S3 Files and ZeroFS expose POSIX filesystems backed by object storage, but the shared interface hides opposite bucket layouts. The choice turns on the role of the bucket: if files must remain ordinary S3 objects, S3 Files preserves that identity; if the bucket can be an internal persistence layer, ZeroFS trades direct S3 access for packing, compression, and client-side encryption.&lt;&#x2F;p&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;storage-layout&quot;&gt;Storage layout&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;The defining property of &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonS3&#x2F;latest&#x2F;userguide&#x2F;s3-files.html&quot;&gt;S3 Files, which is built using Amazon EFS&lt;&#x2F;a&gt;, is that &lt;code&gt;images&#x2F;cat.jpg&lt;&#x2F;code&gt; on the mount corresponds to the same key in the bucket, with changes flowing in both directions. Active data and metadata reside in a low-latency tier that AWS calls “high-performance storage.”&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;That one-file, one-object identity is deliberately absent from the &lt;a href=&quot;https:&#x2F;&#x2F;www.zerofs.net&#x2F;docs&#x2F;architecture&quot;&gt;ZeroFS storage format&lt;&#x2F;a&gt;. Metadata lives in an LSM tree; file contents are split into extents, compressed and encrypted, then packed into immutable segment objects. An S3 client sees an opaque internal layout rather than the mounted files.&lt;&#x2F;p&gt;
&lt;figure class=&quot;rv&quot;&gt;
  &lt;div class=&quot;diagram&quot;&gt;&lt;pre class=&quot;mermaid&quot;&gt;
flowchart TB
    subgraph CLIENTS[&quot;Client Layer&quot;]
        NFS[&quot;NFS Client&quot;]
        P9[&quot;9P Client&quot;]
        NBD[&quot;NBD Client&quot;]
        WEB[&quot;Web Browser&quot;]
    end
    subgraph CORE[&quot;ZeroFS Core&quot;]
        NFSD[&quot;NFS Server&quot;]
        P9D[&quot;9P Server&quot;]
        NBDD[&quot;NBD Server&quot;]
        WEBUI[&quot;Web UI&quot;]
        VFS[&quot;Virtual Filesystem&quot;]
        SEG[&quot;Segment store: compressed, encrypted file-data frames&quot;]
        SLATE[&quot;LSM tree: metadata and 32-byte extent pointers&quot;]
        CACHE[&quot;Local Cache&quot;]
        NFSD --&gt; VFS
        P9D --&gt; VFS
        NBDD --&gt; VFS
        WEBUI --&gt; VFS
        VFS --&gt; SEG
        VFS --&gt; SLATE
        SEG --&gt; CACHE
        SLATE --&gt; CACHE
    end
    subgraph BACKEND[&quot;Storage Backend&quot;]
        SEGOBJ[&quot;Immutable segment objects&quot;]
        SSTS[&quot;Metadata SSTs and manifest&quot;]
        S3[&quot;S3 Object Store&quot;]
        CACHE --&gt; SEGOBJ
        CACHE --&gt; SSTS
        SEGOBJ --&gt; S3
        SSTS --&gt; S3
    end
    NFS --&gt; NFSD
    P9 --&gt; P9D
    NBD --&gt; NBDD
    WEB --&gt; WEBUI
  &lt;&#x2F;pre&gt;&lt;&#x2F;div&gt;
  &lt;figcaption&gt;ZeroFS keeps file data and filesystem metadata on separate paths until both reach the object store.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p class=&quot;rv&quot;&gt;Both mounts use the client page cache. The write-path row below starts after the client sends the write to the server.&lt;&#x2F;p&gt;
&lt;div class=&quot;table-wrap rv&quot;&gt;
  &lt;table&gt;
    &lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;Amazon S3 Files&lt;&#x2F;th&gt;&lt;th&gt;ZeroFS&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td&gt;Storage model&lt;&#x2F;td&gt;&lt;td&gt;AWS-managed high-performance storage synchronized with a bucket; internal layout not documented&lt;&#x2F;td&gt;&lt;td&gt;An LSM tree and immutable data segments on object storage&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;Object layout&lt;&#x2F;td&gt;&lt;td&gt;One file maps to one S3 object&lt;&#x2F;td&gt;&lt;td&gt;Metadata in an LSM tree; file-data frames packed into segments&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;Write path after client cache&lt;&#x2F;td&gt;&lt;td&gt;NFS write to high-performance storage, durable immediately; asynchronous S3 export after write inactivity&lt;&#x2F;td&gt;&lt;td&gt;Over 9P, &lt;code&gt;fsync&lt;&#x2F;code&gt; uploads data segments and flushes LSM metadata to object storage before returning success&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;Cold read and read-ahead&lt;&#x2F;td&gt;&lt;td&gt;Linux NFS read-ahead, directory metadata import, optional small-file import, or direct S3 read&lt;&#x2F;td&gt;&lt;td&gt;LSM lookup, then adaptive object and cross-segment frame prefetch&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;S3 API access to files&lt;&#x2F;td&gt;&lt;td&gt;Yes; filesystem changes appear after asynchronous export&lt;&#x2F;td&gt;&lt;td&gt;No; reading files requires ZeroFS and the encryption password&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;Client interface&lt;&#x2F;td&gt;&lt;td&gt;NFS 4.1&#x2F;4.2&lt;&#x2F;td&gt;&lt;td&gt;9P or NFS for files; NBD for blocks&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;Object-store choice&lt;&#x2F;td&gt;&lt;td&gt;Amazon S3&lt;&#x2F;td&gt;&lt;td&gt;Amazon S3, S3-compatible stores, Azure Blob, or Google Cloud Storage&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;Cost model&lt;&#x2F;td&gt;&lt;td&gt;S3 plus high-performance storage, file access, and synchronization charges&lt;&#x2F;td&gt;&lt;td&gt;Object storage and requests, plus the compute and cache running ZeroFS&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
    &lt;&#x2F;tbody&gt;
  &lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;object-interoperability&quot;&gt;Object interoperability&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;Keeping that one-to-one mapping means a file written through the mount &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonS3&#x2F;latest&#x2F;userguide&#x2F;s3-files-synchronization.html&quot;&gt;eventually becomes a normal S3 object&lt;&#x2F;a&gt;. Export begins after 60 seconds without a write, so continued writes postpone S3 visibility. Once export completes, existing tools can read the object with &lt;code&gt;GetObject&lt;&#x2F;code&gt;, and S3-side changes flow back into the filesystem. If both sides modify the same file before synchronization, S3 wins and the file-side version moves to &lt;code&gt;lost+found&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;A ZeroFS pathname cannot be fetched with the S3 API, and a segment cannot be scanned as Parquet. In exchange, small files need neither one data object nor one PUT each: their extents are compressed, encrypted, and packed together. The bucket and raw local cache contain ciphertext, so mounting or recovery requires ZeroFS and its encryption password. The &lt;a href=&quot;https:&#x2F;&#x2F;www.zerofs.net&#x2F;docs&#x2F;encryption&quot;&gt;encryption documentation&lt;&#x2F;a&gt; lists the structural metadata that remains visible.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;If other applications need immediate S3 visibility after a filesystem write, neither model provides it: S3 Files exports asynchronously, while ZeroFS never exposes mounted files through the S3 API.&lt;&#x2F;p&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;cold-access&quot;&gt;Cold access&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;The first S3 Files access can trigger an import: listing a directory loads every object&#x27;s metadata and asynchronously copies files below the import threshold, 128 KiB by default, into high-performance storage. &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonS3&#x2F;latest&#x2F;userguide&#x2F;s3-files-synchronization.html&quot;&gt;AWS says a first listing of 1,000 objects may take several seconds&lt;&#x2F;a&gt;. Larger files stay in S3, and &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonS3&#x2F;latest&#x2F;userguide&#x2F;s3-files-performance.html&quot;&gt;reads of at least 1 MiB go directly to S3&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;ZeroFS instead populates a &lt;a href=&quot;https:&#x2F;&#x2F;www.zerofs.net&#x2F;docs&#x2F;caching&quot;&gt;local RAM and disk cache&lt;&#x2F;a&gt; from reads and uploads. Newly sealed segments enter the cache from bytes already in hand, so read-after-write needs no GET; the cache is not write-back, and writes still reach object storage when segments are sealed and metadata is flushed. A cold miss resolves the requested extents through the LSM tree and coalesces adjacent frames into ranged GETs. It pays the first object-store round trip without importing the rest of the file or every small file in the directory; once the working set fits locally, data reads generate few S3 requests.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;Both paths add read-ahead for sequential traffic: ZeroFS prefetches within and across segment objects, while S3 Files relies on Linux NFS read-ahead alongside its direct-S3 routing.&lt;&#x2F;p&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;cost&quot;&gt;Cost&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;S3 storage and request charges apply in either case. S3 Files also charges for its high-performance storage tier: resident storage per GB-month and reads and writes per GB. The &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;s3&#x2F;pricing&#x2F;&quot;&gt;AWS pricing example&lt;&#x2F;a&gt; current at publication uses these rates:&lt;&#x2F;p&gt;
&lt;div class=&quot;table-wrap rv&quot;&gt;
  &lt;table&gt;
    &lt;thead&gt;&lt;tr&gt;&lt;th&gt;S3 Files charge&lt;&#x2F;th&gt;&lt;th&gt;Rate used in AWS&#x27;s example&lt;&#x2F;th&gt;&lt;th&gt;When it applies&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td&gt;High-performance storage&lt;&#x2F;td&gt;&lt;td&gt;$0.30 per GB-month&lt;&#x2F;td&gt;&lt;td&gt;Files have a 10 KiB minimum billable size&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;File reads&lt;&#x2F;td&gt;&lt;td&gt;$0.03 per GB&lt;&#x2F;td&gt;&lt;td&gt;Reads from high-performance storage, including exports to S3&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;File writes&lt;&#x2F;td&gt;&lt;td&gt;$0.06 per GB&lt;&#x2F;td&gt;&lt;td&gt;Writes to high-performance storage, including imports from S3&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
    &lt;&#x2F;tbody&gt;
  &lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p class=&quot;rv&quot;&gt;The amount resident in high-performance storage depends on the configured &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonS3&#x2F;latest&#x2F;userguide&#x2F;s3-files-synchronization-customizing.html&quot;&gt;import threshold and expiration window&lt;&#x2F;a&gt;. Listing imports metadata and eligible small files, reads can load more data, and &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonS3&#x2F;latest&#x2F;userguide&#x2F;s3-files-synchronization.html&quot;&gt;file metadata does not expire&lt;&#x2F;a&gt;. Imports are metered as filesystem writes and exports as reads; the &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonS3&#x2F;latest&#x2F;userguide&#x2F;s3-files-metering.html&quot;&gt;minimum operation sizes&lt;&#x2F;a&gt; are 32 KiB for file data and 4 KiB for metadata. AWS reports resident bytes and inodes in &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonS3&#x2F;latest&#x2F;userguide&#x2F;s3-files-monitoring-cloudwatch.html&quot;&gt;CloudWatch&lt;&#x2F;a&gt;, but total bucket size alone cannot forecast the bill.&lt;&#x2F;p&gt;
&lt;h3 class=&quot;rv&quot; id=&quot;cost-example-10-tb&quot;&gt;Illustrative model: store 10,000 GiB and read it once&lt;&#x2F;h3&gt;
&lt;p class=&quot;rv&quot;&gt;&lt;strong&gt;This is an illustrative scenario, not a general cost comparison.&lt;&#x2F;strong&gt; It uses 10,000 AWS billable GB (GiB) of logical data in S3 Standard in &lt;code&gt;us-east-1&lt;&#x2F;code&gt;, at &lt;a href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;s3&#x2F;pricing&#x2F;&quot;&gt;$0.023 per GB-month&lt;&#x2F;a&gt;. The direct case assumes 1 MiB reads from data already in S3; the resident case assumes reads below 1 MiB with all data also in high-performance storage. Internet egress is excluded.&lt;&#x2F;p&gt;
&lt;div class=&quot;table-wrap wide-table rv&quot;&gt;
  &lt;table&gt;
    &lt;thead&gt;&lt;tr&gt;&lt;th&gt;Case&lt;&#x2F;th&gt;&lt;th&gt;Storage&#x2F;month&lt;&#x2F;th&gt;&lt;th&gt;Read once&lt;&#x2F;th&gt;&lt;th&gt;Illustrative subtotal&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td&gt;ZeroFS (2:1)&lt;&#x2F;td&gt;&lt;td&gt;~$115 + overhead&lt;&#x2F;td&gt;&lt;td&gt;S3 GETs&lt;&#x2F;td&gt;&lt;td&gt;~$115 + infrastructure&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;ZeroFS (1:1)&lt;&#x2F;td&gt;&lt;td&gt;~$230 + overhead&lt;&#x2F;td&gt;&lt;td&gt;S3 GETs&lt;&#x2F;td&gt;&lt;td&gt;~$230 + infrastructure&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;S3 Files (1 MiB direct reads)&lt;&#x2F;td&gt;&lt;td&gt;~$230 + metadata&lt;&#x2F;td&gt;&lt;td&gt;~$1.17 + S3 GETs&lt;&#x2F;td&gt;&lt;td&gt;~$231 + metadata and requests&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;S3 Files (small resident reads)&lt;&#x2F;td&gt;&lt;td&gt;$3,230&lt;&#x2F;td&gt;&lt;td&gt;$300&lt;&#x2F;td&gt;&lt;td&gt;$3,530&lt;br&gt;+$600 if imported&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
    &lt;&#x2F;tbody&gt;
  &lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p class=&quot;rv&quot;&gt;The ZeroFS figures are estimates: compression reduces payload bytes, while metadata, temporary compaction data, and requests add cost. At an ideal 8 MiB GET size, reading the file data costs about $0.26 at 2:1 compression or $0.51 at 1:1, before metadata and short or random reads. ZeroFS also requires one server, or two for high availability, plus local disk cache; each node needs about 2 GB of RAM beyond its configured memory cache. S3 Files may therefore cost less for large-file streaming. The difference grows when data is imported into, or written through, high-performance storage: writing 10,000 GiB through S3 Files adds $600 in filesystem writes and $300 for export, while a month of residency adds $3,000.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;Actual costs depend on file count, compression, I&#x2F;O size, residency, &lt;code&gt;fsync&lt;&#x2F;code&gt; frequency, and infrastructure. S3 Files also &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonS3&#x2F;latest&#x2F;userguide&#x2F;s3-files-synchronization.html&quot;&gt;requires bucket versioning&lt;&#x2F;a&gt;, so noncurrent versions need an appropriate lifecycle rule.&lt;&#x2F;p&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;fsync-and-s3-visibility&quot;&gt;&lt;code&gt;fsync&lt;&#x2F;code&gt; and S3 visibility&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;Durability and S3 visibility are separate events in S3 Files. &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonS3&#x2F;latest&#x2F;userguide&#x2F;s3-files-performance.html&quot;&gt;Writes to high-performance storage are durable immediately&lt;&#x2F;a&gt;, but export starts only after &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonS3&#x2F;latest&#x2F;userguide&#x2F;s3-files-synchronization.html&quot;&gt;approximately 60 seconds without a write&lt;&#x2F;a&gt;; calling &lt;code&gt;fsync&lt;&#x2F;code&gt; does not make the object immediately visible through the S3 API. ZeroFS has no second visibility event because there is no export step. Over 9P, a &lt;a href=&quot;https:&#x2F;&#x2F;www.zerofs.net&#x2F;docs&#x2F;durability&quot;&gt;successful &lt;code&gt;fsync&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; returns after object storage acknowledges the data segment and its LSM metadata, leaving the file recoverable after a cold restart.&lt;&#x2F;p&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;rename&quot;&gt;Rename&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;&lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonS3&#x2F;latest&#x2F;userguide&#x2F;s3-files-synchronization.html&quot;&gt;S3 has no directories or atomic rename for general-purpose buckets&lt;&#x2F;a&gt;. S3 Files renames on the mount, then copies each affected object to a new key and deletes the old one; during a directory rename, both prefixes can be visible. AWS says &lt;a href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;AmazonS3&#x2F;latest&#x2F;userguide&#x2F;s3-files-performance.html&quot;&gt;synchronizing 100,000 renamed files takes a few minutes&lt;&#x2F;a&gt;. ZeroFS changes only its LSM directory entries, with no copy per descendant and no S3 prefix exposed to object clients.&lt;&#x2F;p&gt;
&lt;hr class=&quot;rule&quot;&gt;
&lt;p class=&quot;rv&quot;&gt;The mount points may look similar, but the buckets make different promises: S3 Files preserves ordinary objects for the surrounding S3 ecosystem; ZeroFS treats object storage as the private substrate of the filesystem.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Making POSIX filesystems replicated and highly available</title>
        <published>2026-06-28T00:00:00+00:00</published>
        <updated>2026-06-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            Pierre Barre
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://www.zerofs.net/blog/highly-available-posix-on-s3/"/>
        <id>https://www.zerofs.net/blog/highly-available-posix-on-s3/</id>
        
        <content type="html" xml:base="https://www.zerofs.net/blog/highly-available-posix-on-s3/">&lt;p class=&quot;lede rv&quot;&gt;ZeroFS stores a POSIX filesystem in a log-structured database in S3. On one node, acknowledged but unflushed writes live only in memory, and the filesystem is unavailable whenever that node is down. A standby must cover both gaps without weakening the durability promised by &lt;code&gt;fsync&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;where-a-single-node-runs-out&quot;&gt;Where a single node runs out&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;Object storage is durable, but each request is slow and billable. Committing every filesystem operation directly would add tens of milliseconds and a PUT charge to each write. ZeroFS instead buffers writes in an in-memory memtable and flushes batches on &lt;code&gt;fsync&lt;&#x2F;code&gt;, when the memtable fills, and on a periodic timer. A &lt;code&gt;write()&lt;&#x2F;code&gt; returns once the data reaches the memtable; &lt;code&gt;fsync()&lt;&#x2F;code&gt; pushes it to S3.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;This creates two failure modes. A crash before a flush loses acknowledged writes, which POSIX permits because &lt;code&gt;fsync&lt;&#x2F;code&gt;, not &lt;code&gt;write&lt;&#x2F;code&gt;, is the durability boundary. Data already flushed remains in S3, but no server exposes the filesystem while the node is down. A standby can retain the in-memory tail and take over serving, provided it never becomes a second writer.&lt;&#x2F;p&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;only-one-writer-ever&quot;&gt;Only one writer, ever&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;Two nodes over one bucket can split-brain: both believe they are the leader and write concurrently. Once their updates interleave, the filesystem is corrupt. Preventing that cannot depend on timing, network reachability, or synchronized clocks.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;The storage layer chooses the single writer. Opening a database for writing conditionally updates its manifest, increments a &lt;code&gt;writer_epoch&lt;&#x2F;code&gt;, and fences the previous holder. The old leader&#x27;s next manifest write fails and it stops. Any SST files it uploaded in the meantime remain unreferenced and are later reclaimed by garbage collection. During a race, both nodes may briefly claim leadership, but only one epoch can commit.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout rv&quot;&gt;
  &lt;p&gt;Fencing uses conditional writes, supported directly by S3, Azure Blob, and Google Cloud Storage. For S3-compatible stores without that primitive, ZeroFS uses Redis. At startup, each node also asks its peer which one is active instead of trusting its configured role.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;why-two-nodes-not-three&quot;&gt;Why two nodes, not three&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;Raft and Paxos clusters use an odd number of members because the nodes vote on leadership and committed state. ZeroFS has no node quorum. A conditional update to the manifest in the bucket decides which node owns the writer epoch.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;The object store&#x27;s linearizable compare-and-swap can choose one winner among any number of contenders. One ZeroFS node serves; the second holds the un-&lt;code&gt;fsync&lt;&#x2F;code&gt;&#x27;d tail and can take over. A third would add no voting safety because the vote already happens in the bucket.&lt;&#x2F;p&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;keeping-a-standby-in-step&quot;&gt;Keeping a standby in step&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;Replication is semi-synchronous and ordered. For each write, the leader sends it to the standby, waits for an acknowledgement, applies it locally, and then answers the client. Anything visible to a client has therefore reached the standby. Unapplied writes remain in a tail buffer for replay during takeover.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;If the standby falls behind or disconnects, the leader enters Solo instead of blocking the filesystem. Replication resumes when the standby returns. Solo writes have single-node durability: writes not yet flushed have no second copy. Fencing still prevents another writer from committing.&lt;&#x2F;p&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;when-the-leader-dies&quot;&gt;When the leader dies&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;The leader sends a heartbeat every 100 ms with its writer epoch. After roughly two seconds without one, the standby opens the database for writing, fences the old leader, replays its buffered tail, and starts serving. Heartbeats from the old epoch are ignored. If a standby with an unreplayed tail hears from a stale leader, it promotes immediately rather than waiting for the timer.&lt;&#x2F;p&gt;
&lt;figure class=&quot;rv&quot;&gt;
  &lt;div class=&quot;diagram&quot;&gt;&lt;pre class=&quot;mermaid&quot;&gt;
sequenceDiagram
    participant C as Client
    participant L as Leader
    participant S as Standby
    participant O as Object storage
    Note over C,O: Steady state
    C-&gt;&gt;L: write
    L-&gt;&gt;S: ship (semi-sync)
    S--&gt;&gt;L: replicated
    L--&gt;&gt;C: ack (after local apply)
    L-&gt;&gt;O: flush on fsync
    L-&gt;&gt;S: heartbeats (writer epoch)
    Note over L: Leader crashes or is partitioned
    L--xS: heartbeats stop
    Note over S: no heartbeat for ~2s (takeover TTL)
    S-&gt;&gt;O: open as writer, fence old leader
    S-&gt;&gt;S: replay buffered tail
    Note over S: now serving as the new leader
    C-&gt;&gt;L: in-flight write
    L--xC: fail, timeout, or not-leader
    C-&gt;&gt;S: reroute, resend with op-id
    S--&gt;&gt;C: definitive result (deduplicated)
    Note over L: a returning old leader stays fenced
  &lt;&#x2F;pre&gt;&lt;&#x2F;div&gt;
  &lt;figcaption&gt;A leader failure end to end: steady state, takeover, and the client&#x27;s rerouted retry.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p class=&quot;rv&quot;&gt;Clients know both addresses and reroute when one node stops serving. A request in flight during failure may already have committed, so every mutating call carries a stable operation id. The client can retry a &lt;code&gt;mkdir&lt;&#x2F;code&gt; or &lt;code&gt;rename&lt;&#x2F;code&gt; on the new leader without duplicating the effect.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;The old leader must stop reads as well as writes. A node serves only while its lease is valid, and the lease follows the storage engine&#x27;s view of the database. A manifest poll sees the new epoch and closes the stale database within one interval, even if the old leader is idle.&lt;&#x2F;p&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;when-fsync-cant-prove-durability&quot;&gt;When fsync can&#x27;t prove durability&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;Replication reduces but cannot eliminate the un-&lt;code&gt;fsync&lt;&#x2F;code&gt;&#x27;d window. Losing both nodes, or losing a leader in Solo, discards acknowledged writes that never reached S3. A successful &lt;code&gt;fsync&lt;&#x2F;code&gt; means every write acknowledged on that descriptor is in object storage and, under HA, survives failover. If recovery did not carry those writes, &lt;code&gt;fsync&lt;&#x2F;code&gt; returns &lt;code&gt;ESTALE&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;A &lt;strong&gt;lineage token&lt;&#x2F;strong&gt; identifies an uninterrupted durable history. A connected standby that replays the leader&#x27;s tail retains the token. A cold restart or takeover after Solo gets a new token because it cannot prove that it inherited every write. Before acknowledging its first Solo write, the leader marks the lineage un-inheritable in object storage. The client tags un-&lt;code&gt;fsync&lt;&#x2F;code&gt;&#x27;d changes with the current token; on &lt;code&gt;fsync&lt;&#x2F;code&gt;, the leader flushes and compares the oldest outstanding token with the live one. A mismatch fails the call.&lt;&#x2F;p&gt;
&lt;div class=&quot;table-wrap rv&quot;&gt;
  &lt;table&gt;
    &lt;thead&gt;&lt;tr&gt;&lt;th&gt;What happened&lt;&#x2F;th&gt;&lt;th&gt;Token&lt;&#x2F;th&gt;&lt;th&gt;fsync result&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td&gt;Connected leader fails&lt;&#x2F;td&gt;&lt;td&gt;Kept: the standby held the writes and replayed the tail&lt;&#x2F;td&gt;&lt;td&gt;Succeeds after failover&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;Both nodes lost before a flush&lt;&#x2F;td&gt;&lt;td&gt;Regenerated: a cold restart can&#x27;t prove it inherited anything&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ESTALE&lt;&#x2F;code&gt;: the writes were only in memory&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
      &lt;tr&gt;&lt;td&gt;Takeover after Solo&lt;&#x2F;td&gt;&lt;td&gt;Regenerated: the Solo leader tainted the lineage first&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;ESTALE&lt;&#x2F;code&gt;: the new leader never got the writes&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
    &lt;&#x2F;tbody&gt;
  &lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p class=&quot;rv&quot;&gt;The token check is per descriptor but covers the whole file across open handles, plus metadata, directory entries, and both sides of a &lt;code&gt;rename&lt;&#x2F;code&gt;. On a mismatch, the next &lt;code&gt;fsync&lt;&#x2F;code&gt; returns &lt;code&gt;ESTALE&lt;&#x2F;code&gt;: work since the last successful &lt;code&gt;fsync&lt;&#x2F;code&gt; is gone. Client libraries expose this as a distinct stale-handle error.&lt;&#x2F;p&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;trying-to-break-it&quot;&gt;Trying to break it&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;ZeroFS tests these guarantees with &lt;a href=&quot;https:&#x2F;&#x2F;jepsen.io&quot;&gt;Jepsen&lt;&#x2F;a&gt;. The single-node suite generates filesystem operations over a 9P mount and checks them against a reference model. Its crash mode kills the server, drops the memtable, recovers from object storage, and verifies the result against the last &lt;code&gt;fsync&lt;&#x2F;code&gt;. This suite runs in CI on every change.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;The HA suite runs a leader and standby over MinIO while a nemesis kills either node or both, partitions the pair, pauses the object store, and freezes the leader beyond its lease. Meanwhile, a client continues to write and call &lt;code&gt;fsync&lt;&#x2F;code&gt;. The checker looks for overlapping commit epochs, lost durable writes, duplicates, stale reads, and a thawed leader that fails to recognize its fencing. The &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Barre&#x2F;ZeroFS&#x2F;tree&#x2F;main&#x2F;jepsen&#x2F;ha&quot;&gt;suite is in the repository&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 class=&quot;rv&quot; id=&quot;what-it-doesnt-do&quot;&gt;What it doesn&#x27;t do&lt;&#x2F;h2&gt;
&lt;p class=&quot;rv&quot;&gt;HA improves availability, not throughput. There is still one writer, and reads are not distributed across nodes.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;Crash failover takes roughly two seconds while the standby waits for the takeover TTL; clients block and retry during that interval. Advisory byte-range locks are not replicated and disappear on failover.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;&lt;code&gt;fsync&lt;&#x2F;code&gt; remains the durability boundary. A connected standby also protects unflushed writes, but applications should not rely on that extra copy. Configuration and a complete failure-case table are in the &lt;a href=&quot;https:&#x2F;&#x2F;www.zerofs.net&#x2F;docs&#x2F;high-availability&quot;&gt;high availability documentation&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;hr class=&quot;rule&quot;&gt;
&lt;p class=&quot;rv&quot;&gt;Fencing makes the writer a storage-layer decision rather than a timing assumption. Replication covers ordinary failover; lineage tokens turn failures outside that coverage into &lt;code&gt;ESTALE&lt;&#x2F;code&gt; instead of false success.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
