<?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 supplies the final boundary. 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 refresh or write observes the newer term and stops. Any SST files it uploaded in the meantime remain unreferenced and are later reclaimed by garbage collection. Only one writer epoch can commit durable state.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;A second mechanism controls who may serve. A conditional marker object beside the database moves through &lt;code&gt;Active&lt;&#x2F;code&gt;, &lt;code&gt;Claiming&lt;&#x2F;code&gt;, and &lt;code&gt;Opening&lt;&#x2F;code&gt;. Every transition names one generation and uses the exact object-store version it read. A candidate must win the marker claim, wait out the previous leader&#x27;s bounded serving authority, open the database to publish a higher writer epoch, reconcile its tail, and become &lt;code&gt;Active&lt;&#x2F;code&gt; before accepting clients.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout rv&quot;&gt;
  &lt;p&gt;Fencing and marker ownership use conditional writes, supported directly by S3, Azure Blob, and Google Cloud Storage. For S3-compatible stores without that primitive, ZeroFS can use 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: exact conditional updates in the object store order marker ownership and writer epochs.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;The supported HA topology is exactly two participants. One node serves and the sole standby supplies the heartbeat acknowledgement and complete-tail proof used by the leader&#x27;s fast path. A third participant would not add a vote; it would violate that sole-standby assumption. Independent read-only nodes remain available for read scaling.&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. While the pair is Connected, the leader sends each mutation to the standby, waits for an acknowledgement, applies it locally, and then answers the client. The receiver accepts sequence &lt;em&gt;N&lt;&#x2F;em&gt; only when durable storage and its retained tail account for every earlier sequence. A restarted receiver that lost its in-memory base rejects a torn suffix, causing the leader to flush the missing base and retry the same sequence.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;File contents need more than metadata replication. Until an immutable segment reaches object storage, a replicated write carries its sealed, compressed and encrypted frame bytes with the extent pointer. On takeover the standby materializes any missing segment at the original offsets before serving.&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. Before acknowledging its first Solo write, the leader marks the current durability lineage non-inheritable. Solo writes then have single-node durability: anything not yet flushed has no second copy. Fencing still prevents another writer from committing durable state.&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 coverage heartbeat every 100 ms. It includes the exact writer epoch and the applied and durable replication frontiers; the standby acknowledges only when shared storage plus its retained tail cover the complete applied frontier. After roughly two seconds without coverage, the standby may attempt a handoff. Silence is a suspicion signal, not permission to serve.&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: Connected
    C-&gt;&gt;L: mutation
    L-&gt;&gt;S: sequence N and any sealed frame bytes
    S-&gt;&gt;S: validate complete prefix, retain tail
    S--&gt;&gt;L: accepted
    L-&gt;&gt;L: apply sequence N
    L--&gt;&gt;C: result
    L-&gt;&gt;S: coverage heartbeat
    S--&gt;&gt;L: exact-epoch coverage ACK
    Note over L: Leader crashes or is partitioned
    L--xS: heartbeats stop
    Note over S: no coverage for ~2 s
    S-&gt;&gt;S: quiesce heartbeat ACKs
    S-&gt;&gt;O: exact CAS Active to Claiming
    Note over S,O: wait the full 9 s claim grace
    S-&gt;&gt;O: exact CAS Claiming to Opening
    S-&gt;&gt;O: open writer, publish a higher epoch
    S-&gt;&gt;S: validate and replay retained tail
    S-&gt;&gt;O: exact CAS Opening to Active
    S-&gt;&gt;O: validate exact Active identity
    C-&gt;&gt;S: reconnect, replay session, resend same 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: commit-before-apply replication, phased takeover, and session replay.&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p class=&quot;rv&quot;&gt;The claim waits nine seconds before entering &lt;code&gt;Opening&lt;&#x2F;code&gt;. That interval is derived from the previous leader&#x27;s last three-second authority grant, a three-second final marker-validation opportunity, a two-second response drain, and one second of scheduling margin. These values belong to one static policy rather than independent runtime knobs. Opening the writer then publishes the higher epoch that independently fences durable commits. Only after tail reconciliation and an exact &lt;code&gt;Active&lt;&#x2F;code&gt; validation does the successor serve.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;The old leader must stop reads as well as writes. During healthy replication, an exact-epoch heartbeat ACK no more than 300 ms old renews serving authority without polling the marker. Once ACKs are stale, the leader immediately validates its exact &lt;code&gt;Active&lt;&#x2F;code&gt; marker on a one-second cadence. Each read has a separate three-second allowance, while authority remains anchored to the request start. An expired grant suspends successful responses while one final validation runs; a changed marker, final error or timeout, or writer fence retires the process.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;The bundled mount and the Python, TypeScript, and Go clients accept both node addresses as one comma-separated target set; Rust also exposes &lt;code&gt;connect_multi&lt;&#x2F;code&gt;. Targets are probed concurrently and re-probed after a disconnect. Session replay rebinds linked open handles by inode id and re-acquires recorded byte-range locks before publishing the new connection. An open file whose final link was removed cannot be replayed, so its loss makes that logical session terminal with &lt;code&gt;ESTALE&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;A request in flight during failure may already have committed. Every replay-sensitive mutation therefore carries a stable operation id, attempt state, and origin epoch, while the replicated batch carries its exact result. A successor with complete coverage can answer a retried &lt;code&gt;mkdir&lt;&#x2F;code&gt; or &lt;code&gt;rename&lt;&#x2F;code&gt; without applying it twice.&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 follows the descriptor POSIX makes responsible. An &lt;code&gt;fsync&lt;&#x2F;code&gt; on a file covers that file&#x27;s data and metadata across every open handle; an &lt;code&gt;fsync&lt;&#x2F;code&gt; on a directory covers its directory-entry changes, and a cross-directory &lt;code&gt;rename&lt;&#x2F;code&gt; marks both directories. On a lineage mismatch, the next responsible &lt;code&gt;fsync&lt;&#x2F;code&gt; returns &lt;code&gt;ESTALE&lt;&#x2F;code&gt;: work since its last successful durability boundary is gone. Client libraries expose this as a distinct stale 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 the leader, the standby, or both; partitions the replication link; pauses the object store; and freezes then thaws a leader after a successor promotes. The workloads cover concurrent add and remove operations, file contents, filesystem statistics, and lineage-aware &lt;code&gt;fsync&lt;&#x2F;code&gt; for file data, metadata, multiple handles, directory entries, and cross-directory rename. The checkers reject lost, resurrected, or corrupted acknowledged state and any successful &lt;code&gt;fsync&lt;&#x2F;code&gt; that approved a lineage recovery which dropped its writes. 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, reads are not distributed across the pair, and exactly two HA participants are supported.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;Crash failover is not a two-second event. The two-second suspicion window is followed by the nine-second claim grace, writer open, tail reconciliation, activation, and client reconnect. Clients block and retry during that interval. The shared object store and its conditional-update path also remain availability dependencies.&lt;&#x2F;p&gt;
&lt;p class=&quot;rv&quot;&gt;Advisory byte-range locks live in server memory and are not replicated. The bundled client records and re-acquires them before completing session replay, but ownership is not continuous: another session may acquire a lock during the gap. Applications that need fencing across failover cannot use an advisory file lock as a distributed mutex.&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;The phased marker retires the old serving term before the next one opens, while the writer epoch remains the independent storage fence. Replication covers ordinary Connected 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>
