{"slug": "show-hn-fs-relay-mount-remote-folders-over-http-10x-faster-than-s3fs-go", "title": "Show HN: Fs-relay – Mount remote folders over HTTP 10x faster than s3fs (Go)", "summary": "Muayyad Alsadi released fs-relay v0.2.0, a Go-based tool that mounts remote folders over HTTP and WebSockets and is claimed to run 10x faster than s3fs, with the author contributing upstream fixes to locking, startup safety, shutdown and self-update. The design uses rootless Linux FUSE v3 (fusermount3) on the client, five Go packages with zero CGO, a dedicated raw path for large files that pays constant framing overhead regardless of file size, and kernel zero-copy transfers that bypass the user-space copy loop. The release targets multi-mount SQLite workloads and atomic self-updates, addressing broken POSIX byte-range locks that cause silent database corruption on traditional network mounts.", "body_md": "In 2004, setting up NFS across two beige desktop boxes in my room felt\nlike inviting a thunderstorm indoors. You edited `/etc/exports`, restarted\n`portmap` and `rpcbind`, crossed your fingers, and waited for the whole desktop\nto freeze the moment someone tripped over an Ethernet cable.\n\nAnyone who grew up in [those early Linux days](https://jadmadi.net/posts/open-source-ai-rebellion/)\nknows the specific dread of a dead network mount. A terminal tab running `ls`\nwould enter an unkillable kernel sleep (`D` state). Even `kill -9` bounced off\nit. You either lived with a haunted directory tree or reached for the physical\nreset button.\n\nTwo decades later, our workloads moved to cloud instances and container\nsandboxes, yet the friction of touching files across a wire never really\ndisappeared. We traded manual NFS exports for `rsync` loops, background sync\ndaemons, or `sshfs`. When you work from the Middle East, where\n[latency is a daily tax](https://jadmadi.net/posts/latency-is-political-when-you-live-far-from-the-center/)\non every round trip, chatty remote filesystems punish every command you type.\n\nAt the end of October, [Muayyad Alsadi](https://github.com/muayyad-alsadi/)\n([@muayyadalsadi](https://x.com/muayyadalsadi)) started building a clean-sheet\nanswer to this problem called [fs-relay](https://jadmadi.net/project/fs-relay/). Over the past\nweek, I had the pleasure of contributing upstream fixes across locking, startup\nsafety, shutdown, and self-update for today’s `v0.2.0` release. This is the\nstory of how Muayyad’s design makes `fs-relay` ten times faster than the usual\nsuspects, and what went into making multi-mount SQLite and atomic self-updates\nsolid.\n\nWhat’s in this post:\n\n- [Why traditional remote mounts struggle](#why-traditional-remote-mounts-struggle)\n- [How Muayyad made fs-relay 10x faster](#how-muayyad-made-fs-relay-10x-faster)\n- [Upstream work toward v0.2.0](#upstream-work-toward-v020)\n- [Try fs-relay v0.2.0](#try-fs-relay-v020)\n\n## Why Traditional Remote Mounts Struggle\n\nWhen you want multiple machines or [coding agents](https://jadmadi.net/posts/my-mcp-stack/) to\nshare a live workspace without copying gigabytes back and forth, you hit five\nwalls almost immediately:\n\n- **The classical user-space copy loop** : Moving a file across a network the\ntextbook way repeats the same five steps for every chunk. With a 4 KiB buffer\nand a 1 GiB file, that loop runs over 250,000 times, paying 250,000 syscalls,\n250,000 user/kernel context switches, and a trail of scattered, fragmented\nmemory allocations:\n  1. Allocate memory.\n  2. Receive from the network into memory.\n  3. Write from memory to disk.\n  4. Repeat steps 2 and 3 in either direction (*net -> disk* or*disk -> net* ).\n  5. Free memory.\n- **Round-trip chatty metadata** : A simple`tar -xf` or project build issues\nthousands of`lookup` ,`getattr` ,`open` ,`read` , and`close` calls. Sending\neach call as a separate network round trip turns a fast link into molasses.\n- **Root privileges and firewall ports** : Classic NFS and SMB expect root\ndaemons, kernel modules, and open ports that are painful to expose across\nnetworks.\n- **Broken advisory locks** : Most network mounts either ignore POSIX byte-range\nlocks or fake them locally. Point two processes at the same SQLite database\nand you get silent corruption.\n- **No fast-lane for large payloads** : Other protocols wrap every small chunk\nof a large file inside its own message frame, so framing overhead grows\nlinearly with file size.`fs-relay` has a dedicated`raw` path for large\nfiles that pays a constant overhead regardless of size.\n\n## How Muayyad Made fs-relay 10x Faster\n\nMuayyad designed `fs-relay` around a strict constraint: five Go packages, zero\nCGO, rootless Linux FUSE v3 (`fusermount3`) on the client, and standard HTTP\nand WebSockets on the wire.\n\nInstead of trying to micro-optimize the classical copy loop, `fs-relay` skips\nboth the user-space copy cycle and the per-file round trips:\n\n- **Zero-copy kernel transfers for large files** : Large reads and writes bypass\nframing overhead on a raw socket and use Linux kernel zero-copy primitives\n(`splice(2)` and`copy_file_range(2)` , as Muayyad detailed in[his article on zero-copy single context switching](https://medium.com/@alsadi/secret-sauce-is-zero-copy-single-context-switching-b749bd614dca) ).\nBytes move between socket and disk inside the kernel without user-space\nbuffer allocation, copying, or a context switch on every 4 KiB block.\n- **1 MiB write-behind buffering** : Contiguous client writes coalesce up to\n1 MiB on the mount before flushing across the wire.\n- **Local read-only opens** : Opening a file read-only takes no server file\nhandle. The client resolves`open` ,`flush` , and`release` locally and reads\nby inode.\n- **Batch directory and small-file prefetching** : Directory listings\n(`readdir` ) return entry attributes inline, then batch-prefetch up to 32\nsmall regular files (`<= 64 KiB` , up to 1 MiB total) via`reads` and up to 32\nchild directories via`readdirs` in one trip.\n- **Server-pushed invalidation** : Instead of polling for stale attributes, the\nmount listens on one WebSocket or chunked`join` stream for server-pushed\ncache evictions and 2-second heartbeats.\n\nThe framing overhead of reading a 1 GiB file should not be ten times that of a\n100 MiB file; the goal is constant cost. Once those bottlenecks (the\n250,000-syscall copy loop and the per-file round trips) are out of the way,\n`fs-relay` also keeps tiny files and chatty operations lean by encoding frames\nwith FlatBuffers for in-place reads with no new allocations (a recurring lesson\nfor anyone who has watched\n[serialization overhead dominate Go hot paths](https://jadmadi.net/posts/go-json-v2-explained/)).\n\nThat is why the repository benchmarks test both scenarios, and the numbers\nspeak for themselves. On small files, unpacking `ansible.tar` (27 MiB across\n5,868 files) takes 203.56 seconds on `s3fs` (mounted over `rustfs`) and 107.89\nseconds on `fuse-nfs`, while `fs-relay` finishes in 15.47 seconds. On large\nfiles, unpacking `Antigravity.tar.gz` (500 MiB) takes 36.13 seconds on\n`fuse-nfs` and 21.01 seconds on `s3fs`, compared to 9.78 seconds on `fs-relay`.\nEven more surprising, running DuckDB queries over Parquet files mounted through\n`fs-relay` beat DuckDB reading the exact same files directly from `rustfs` over\nnative S3.\n\n## Upstream Work Toward v0.2.0\n\nMuayyad’s code and `SPEC.md` are clear enough that jumping in was easy. The\nfirst contribution\n([`9071dbd`](https://github.com/muayyad-alsadi/fs-relay/commit/9071dbd))\nproposed dual licensing under MIT and the Waqf Digital Public License\n(`WaqfDPL-1.0`), which Muayyad adopted so the project stays free and open in\nperpetuity.\n\nFrom there, the work covered four practical engineering problems.\n\n### 1. Server-Authoritative POSIX Locks and SQLite Coherence\n\nTo run SQLite across two remote mounts without corruption, both mounts must\nagree on byte-range locks (`getlk`, `setlk`, `setlkw`) and `flock`.\n[PR #3](https://github.com/muayyad-alsadi/fs-relay/pull/3) added the\nserver-side lock table in `internal/local/lock.go`.\n\nA classic POSIX lock belongs to a `(PID, inode)` pair, which breaks across\nmachines when two hosts reuse the same PID. In `fs-relay`, a lock holder is\nthe mount’s HMAC-signed client ID paired with the kernel lock owner. Because\nSQLite’s rollback journal hammers three exact byte offsets at `0x40000000`, the\nlock table tracks dedicated fast-path counters for the pending byte, the\nreserved byte, and the 510-byte shared range, spilling arbitrary ranges onto an\noverflow list only when needed.\n\nGetting the lock table working was only half the battle. Enabling a dedicated\n`go test -race` job in CI\n([PR #25](https://github.com/muayyad-alsadi/fs-relay/pull/25)) and running\nmulti-writer SQLite stress tests exposed subtle distributed races, fixed\nacross [PR #24](https://github.com/muayyad-alsadi/fs-relay/pull/24),\n[PR #30](https://github.com/muayyad-alsadi/fs-relay/pull/30),\n[PR #33](https://github.com/muayyad-alsadi/fs-relay/pull/33), and\n[PR #34](https://github.com/muayyad-alsadi/fs-relay/pull/34):\n\n- **Draining write-behind before unlock** : If mount A writes a transaction and\nunlocks before its buffered bytes reach the server, mount B acquires the lock\nand reads stale bytes.`fs-relay-bind` now drains dirty buffers before\nsending`F_UNLCK` and drops cached attributes and prefetched file bodies the\nmoment a lock grant arrives.\n- **Closing wake-up races** : When a writer downgrades to a shared read lock or\na waiter receives`EAGAIN` , registering the waiter under the lock mutex\nensures coalesced broadcast wake-ups never miss a blocked reader.\n- **Guarding late grants** : If a file handle closes while a lock request is\nstill in flight, the client immediately releases the late grant so orphaned\nlocks never wedge the namespace.\n\nWe verified this on Linux 6.12 with SQLite 3.46.1 in rollback journal mode:\nfour writers split across two mounts committed 200 rows concurrently, and\n`PRAGMA integrity_check` returned `ok` on both mounts. (`WAL` mode stays out of\nscope because it relies on an `-shm` file mapped into shared memory via `mmap`\non a single kernel, which cannot be shared across separate machines.)\n\n### 2. Startup Validation and Single-Server Data Locks\n\nEarly on, a typo on the command line or a second `fs-relay` process pointing at\nthe same data directory could cause confusing state. Across\n[PR #16](https://github.com/muayyad-alsadi/fs-relay/pull/16),\n[PR #17](https://github.com/muayyad-alsadi/fs-relay/pull/17),\n[PR #18](https://github.com/muayyad-alsadi/fs-relay/pull/18),\n[PR #20](https://github.com/muayyad-alsadi/fs-relay/pull/20),\n[PR #29](https://github.com/muayyad-alsadi/fs-relay/pull/29), and\n[PR #35](https://github.com/muayyad-alsadi/fs-relay/pull/35), we added\nnon-blocking `-token-file` secret loading, default `http://` URL schemes,\npre-mount directory validation (`-force-existing-mnt`), and an exclusive\n`.fs-relay.lock` file lock on the data root hardened against symlink\ntraversal.\n\n### 3. Bounded Unmount on Interrupts\n\nAnyone who has pressed `Ctrl+C` on a FUSE daemon while a shell is still sitting\ninside the mounted folder knows how easily shutdown can hang. In\n[PR #19](https://github.com/muayyad-alsadi/fs-relay/pull/19),\n[PR #37](https://github.com/muayyad-alsadi/fs-relay/pull/37),\n[PR #38](https://github.com/muayyad-alsadi/fs-relay/pull/38), and\n[PR #39](https://github.com/muayyad-alsadi/fs-relay/pull/39), we tightened CLI\nargument validation and reworked signal shutdown in `fs-relay-bind`.\n\nOn `SIGINT` or `SIGTERM`, `fs-relay-bind` attempts a clean unmount bounded by a\n2-second timeout. If open files or a stalled network flush keep the mount busy,\nit falls back to an interruptible lazy detach (`fusermount3 -uz` or\n`MNT_DETACH`) and exits non-zero with a clear message instead of hanging the\nterminal.\n\n### 4. Two-Phase Atomic Self-Update in v0.2.0\n\nFor `v0.2.0`\n([PR #40](https://github.com/muayyad-alsadi/fs-relay/pull/40)), we added\n`update`, `version`, and `help` subcommands to both `fs-relay` and\n`fs-relay-bind`, alongside a 12-hour cached background release check\n(`FS_RELAY_NO_UPDATE_CHECK` disables it).\n\nUpdating two companion binaries (`fs-relay` and `fs-relay-bind`) in place has\nsharp edges. If the first binary swaps and the second fails, the machine is\nleft running mismatched versions. Our updater verifies the release tarball\nagainst the GitHub SHA-256 digest (falling back to `checksums.txt`), preserves\nexisting file permissions (such as `0700`), stages both replacement binaries in\nexclusive temporary files in the target directory, and rolls back the primary\nbinary automatically if the companion commit fails.\n\n## Try fs-relay v0.2.0\n\nThere is genuine satisfaction in contributing to software that refuses unnecessary layers. You do not need root daemons, kernel modules, or heavyweight cluster coordinators to mount a directory across machines. Standard HTTP, careful FUSE caching, and server-side POSIX locks get the job done in five small packages.\n\nStarting a share, mounting it on a client, and updating both binaries in place takes three commands:\n\n```\nmkdir -p ./data/demo /mnt/demo\nfs-relay -listen 127.0.0.1:8080 -data ./data -token-file ./token.txt\nfs-relay-bind -url 127.0.0.1:8080 -ns demo -token-file ./token.txt /mnt/demo\nfs-relay update\n```\n\nYou can grab the `v0.2.0` binaries or read the design contract in\n[Muayyad Alsadi’s fs-relay repository](https://github.com/muayyad-alsadi/fs-relay),\nor check the summary card on the [project page](https://jadmadi.net/project/fs-relay/).\n\nHave you tried replacing `sshfs` or `rsync` loops with user-space FUSE over\nHTTP in your own workflows, or are you still nursing old NFS mounts?\n\nRecommended\n\n## Continue reading\n\nSelected from shared topics, related tags, and the recent archive.", "url": "https://wpnews.pro/news/show-hn-fs-relay-mount-remote-folders-over-http-10x-faster-than-s3fs-go", "canonical_source": "https://jadmadi.net/posts/mounting-remote-directories-over-http/", "published_at": "2026-11-09 09:36:19+00:00", "updated_at": "2026-11-09 09:52:20.360723+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure"], "entities": ["fs-relay", "Muayyad Alsadi", "Go", "s3fs", "FUSE", "SQLite", "NFS"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/show-hn-fs-relay-mount-remote-folders-over-http-10x-faster-than-s3fs-go", "markdown": "https://wpnews.pro/news/show-hn-fs-relay-mount-remote-folders-over-http-10x-faster-than-s3fs-go.md", "text": "https://wpnews.pro/news/show-hn-fs-relay-mount-remote-folders-over-http-10x-faster-than-s3fs-go.txt", "jsonld": "https://wpnews.pro/news/show-hn-fs-relay-mount-remote-folders-over-http-10x-faster-than-s3fs-go.jsonld"}}