> ## Documentation Index
> Fetch the complete documentation index at: https://docs.archil.com/llms.txt
> Use this file to discover all available pages before exploring further.

# File systems on one host

> How we compared Archil with Amazon EFS, JuiceFS, Amazon S3 Files, and Mountpoint for Amazon S3 from one EC2 host, and the commands to rerun each test.

Each benchmark on this page mounts every file system on the same EC2 host, so the compute and the network are the same
and only the file system differs. Run the same commands from your own host to check our numbers.

## Archil vs Amazon EFS

The results are in [Benchmark against Amazon EFS](https://docs.archil.com/details/comparison.md#benchmark-against-amazon-efs) and on
[archil.com/compare/amazon-efs](https://archil.com/compare/amazon-efs).

### Method

* **Host:** AWS c7gn.2xlarge (Graviton3, 8 vCPU, 15 GiB), us-east-1, Amazon Linux 2023. Both file systems were in the
  same region and tested from the same host on 2026-10-15.
* **Archil:** a new Archil disk with no data source, mounted with Archil client v0.9.2 and default mount options.
* **Amazon EFS:** a new file system with General Purpose performance mode, Elastic throughput and Standard storage,
  mounted with amazon-efs-utils 3.0.1 (TLS, NFSv4.1, rsize/wsize 1 MiB). Amazon S3 Files is built on the same service.
* **No network fetches:** git clones came from local bare mirrors, and npm used `npm ci --prefer-offline` with a
  prefilled cache. Only the file system under test was measured.
* **Timing:** times are wall-clock time until the command returns.
* **fio:** fio 3.32, buffered I/O, 4 GiB file. 4 KiB random I/O used libaio, iodepth 16, 4 jobs. Cold reads were taken
  after unmount, `drop_caches` and remount.
* **Tool versions:** git 2.50.1, Node 24.18.0 / npm 11.16.0, SQLite 3.40.0.

### Run it

Mount both file systems, then run every test below 3 times on each mount and take the median. Set `MNT` to the mount
under test. Inputs and caches stay on local disk.

```bash theme={null}
archil mount dsk-... /mnt/archil --region aws-us-east-1
mount -t efs -o tls <file-system-id>:/ /mnt/efs
```

Between steps, except between a clone and its `git status`, we flushed the mount (`archil sync /mnt/archil` on Archil,
`sync -f` on EFS). The flush isn't part of the times in the table.

**git.** Mirror both repositories once, pinned to the commits we used:

```bash theme={null}
git clone --bare https://github.com/expressjs/express /data/mirrors/express.git
git -C /data/mirrors/express.git update-ref refs/heads/master a419aa7350389a275ab45fde2e849ed1f02449ef
git clone --bare https://github.com/facebook/react /data/mirrors/react.git
git -C /data/mirrors/react.git update-ref refs/heads/main 99c106c43439ad7af05d42f66582d8d05190e9ff
```

Then, for each repository (old tag `3.21.2` for express, `v16.0.0` for react):

```bash theme={null}
mkdir -p "$MNT/git" && cd "$MNT/git"
time git clone -q --no-hardlinks /data/mirrors/react.git react
time git -C react status
time git -C react checkout -q v16.0.0
time rm -rf react
```

**npm.** Create the project and fill the cache once:

```bash theme={null}
export npm_config_cache=/data/npm-cache
cd /data && npx -y create-next-app@15.5.4 app --ts --eslint --tailwind --app --src-dir --no-turbopack \
  --import-alias '@/*' --use-npm --skip-install --disable-git --yes
(cd /data/app && npm install --package-lock-only --no-audit --no-fund)
mkdir /data/prime && cp /data/app/package*.json /data/prime && (cd /data/prime && npm ci --no-audit --no-fund) && rm -rf /data/prime
```

Our lockfile resolved 598 packages. Yours resolves the newest matching versions on the day you create it, so expect
the count to differ a little. Then:

```bash theme={null}
rm -rf "$MNT/npm" && mkdir "$MNT/npm" && cp /data/app/package*.json "$MNT/npm" && cd "$MNT/npm"
time npm ci --prefer-offline --no-audit --no-fund
time rm -rf node_modules
```

**SQLite.** `python3 sqlite_bench.py "$MNT/sqlite"` prints the time in seconds for each of the four tests. The script
has three parts, `full`, `normal`, and `bulk`; `bulk` runs the 1M-row insert and then the point reads against that table
in the same process, with no flush between them. On EFS we ran each part on its own with a 10-minute limit, for example
`timeout 600 python3 sqlite_bench.py "$MNT/sqlite" bulk`, and the point reads hit that limit.

> [accordion] sqlite\_bench.py
>
> ```python sqlite_bench.py theme={null}
> import os, random, sqlite3, sys, time
>
> d = sys.argv[1]
> os.makedirs(d, exist_ok=True)
> PAYLOAD = "x" * 100
> rng = random.Random(42)
>
>
> def fresh(name, sync):
>     p = os.path.join(d, name)
>     for suf in ("", "-wal", "-shm"):
>         try:
>             os.remove(p + suf)
>         except FileNotFoundError:
>             pass
>     c = sqlite3.connect(p, isolation_level=None)
>     c.execute("PRAGMA journal_mode=WAL")
>     c.execute(f"PRAGMA synchronous={sync}")
>     c.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, a INTEGER, b TEXT)")
>     return c, p
>
>
> def single_txn_inserts(sync, n=10_000):
>     c, _ = fresh(f"single_{sync}.db", sync)
>     s = time.perf_counter()
>     for i in range(n):
>         c.execute("INSERT INTO t(a, b) VALUES (?, ?)", (i, PAYLOAD))
>     e = time.perf_counter() - s
>     c.close()
>     return e
>
>
> def bulk(n=1_000_000):
>     c, p = fresh("bulk.db", "FULL")
>     s = time.perf_counter()
>     c.execute("BEGIN")
>     c.executemany("INSERT INTO t(id, a, b) VALUES (?, ?, ?)",
>                   ((i, i, PAYLOAD) for i in range(1, n + 1)))
>     c.execute("COMMIT")
>     e = time.perf_counter() - s
>     c.execute("PRAGMA wal_checkpoint(TRUNCATE)")
>     c.close()
>     return e, p, n
>
>
> def point_reads(p, n_rows, n=100_000):
>     keys = [rng.randint(1, n_rows) for _ in range(n)]
>     s = time.perf_counter()
>     c = sqlite3.connect(p, isolation_level=None)
>     cur = c.cursor()
>     for k in keys:
>         cur.execute("SELECT a, b FROM t WHERE id = ?", (k,))
>         if cur.fetchone() is None:
>             raise SystemExit(f"missing row {k}")
>     e = time.perf_counter() - s
>     c.close()
>     return e
>
>
> which = sys.argv[2:] or ["full", "normal", "bulk"]
> if "full" in which:
>     print(f"sqlite_10k_txn_full {single_txn_inserts('FULL'):.3f}", flush=True)
> if "normal" in which:
>     print(f"sqlite_10k_txn_normal {single_txn_inserts('NORMAL'):.3f}", flush=True)
> if "bulk" in which:
>     e, p, n = bulk()
>     print(f"sqlite_bulk_1m {e:.3f}", flush=True)
>     print(f"sqlite_100k_point_reads {point_reads(p, n):.3f}", flush=True)
> ```

**fio.** The four jobs in the table, in the order they ran, all on the same 4 GiB file. The MB/s and IOPS are fio's
`bw_bytes` and `iops`. Warm reads also ran after the sequential write; they aren't in the table.

```bash theme={null}
F="--filename=$MNT/fio/file --size=4g --direct=0 --group_reporting --output-format=json"
mkdir -p "$MNT/fio"
fio --name=seqwrite_1m $F --rw=write --bs=1m --ioengine=psync --end_fsync=1
fio --name=randwrite_4k $F --rw=randwrite --bs=4k --ioengine=libaio --iodepth=16 --numjobs=4 --time_based --runtime=30 --end_fsync=1
# Unmount, run `sync; echo 3 > /proc/sys/vm/drop_caches`, and mount again before each cold read
fio --name=seqread_1m_cold $F --rw=read --bs=1m --ioengine=psync
fio --name=randread_4k_cold $F --rw=randread --bs=4k --ioengine=libaio --iodepth=16 --numjobs=4 --time_based --runtime=30
```

## Archil vs JuiceFS vs Amazon S3 Files

The developer-workload numbers on [archil.com/compare/juicefs](https://archil.com/compare/juicefs) and
[archil.com/compare/amazon-s3-files](https://archil.com/compare/amazon-s3-files) come from one AWS c7gn.2xlarge (arm64)
in us-east-1 with every file system mounted on the same host:

```bash theme={null}
archil mount dsk-... /mnt/archil --region aws-us-east-1
juicefs mount -d redis://<elasticache-endpoint>:6379/1 /mnt/jfs              # JuiceFS defaults
juicefs mount -d --writeback redis://<elasticache-endpoint>:6379/1 /mnt/jfs  # JuiceFS writeback
mount -t s3files <file-system-id>:/ /mnt/s3files                             # amazon-efs-utils
```

JuiceFS Community Edition kept data in Amazon S3 and metadata in ElastiCache for Redis in the same Availability Zone.
Inputs sit on local disk, so GitHub and the npm registry aren't timed, and each run is timed until its writes are
durable. Run each workload three times per file system, delete its output between runs, and take the median:

```bash theme={null}
# Untimed, once: a local Django mirror, a Next.js 15 app, and a filled npm cache
git clone --mirror https://github.com/django/django.git /data/django.git
npx create-next-app@15 /data/next --yes --skip-install
(cd /data/next && npm install --cache /data/npm-cache --ignore-scripts && rm -rf node_modules)

# Timed, with MNT set to the mount under test
time (git clone -q file:///data/django.git "$MNT/django" && sync -f "$MNT/django")
mkdir "$MNT/app" && cp /data/next/package.json /data/next/package-lock.json "$MNT/app"
time (cd "$MNT/app" && npm ci --cache /data/npm-cache --prefer-offline --ignore-scripts --no-audit --no-fund && sync -f .)
```

On Archil, replace `sync -f <dir>` with `archil sync /mnt/archil`. With `--writeback`, JuiceFS acknowledges writes once
they are staged on local disk, so also wait until every staged block has uploaded:

```bash theme={null}
until [ "$(awk '/^juicefs_staging_(writing_)?blocks /{s+=$2} END{print s+0}' /mnt/jfs/.stats)" = 0 ]; do sleep 0.05; done
```

Our app installed 18,602 files. The count depends on the Next.js release `create-next-app@15` resolves, so expect yours
to differ a little.

## Archil vs Amazon S3 Files vs Mountpoint for Amazon S3

The latency, visibility, and new-file-system numbers on
[archil.com/compare/amazon-s3-files](https://archil.com/compare/amazon-s3-files) and
[archil.com/compare/mountpoint-for-amazon-s3](https://archil.com/compare/mountpoint-for-amazon-s3) come from one AWS
c7gn.2xlarge (arm64) in us-east-1. Each file system had two independent mounts on that host, a writer A and a reader
B, each with its own cache:

```bash theme={null}
archil mount dsk-... /mnt/archil-a --region aws-us-east-1             # Archil client v0.9.0, A
archil mount --shared dsk-... /mnt/archil-b --region aws-us-east-1    # B
mount -t s3files -o nosharecache <file-system-id>:/ /mnt/s3files-a     # amazon-efs-utils 3.0.1, NFS 4.2
mount-s3 --region us-east-1 --allow-overwrite --allow-delete <bucket> /mnt/mountpoint-a   # Mountpoint 1.22.3
```

A default Archil mount is exclusive, so B used `--shared`. The S3 Files mount target was in the host's Availability
Zone, on a versioned bucket. Mountpoint ran without `--cache`.

| Result | How |
| - | - |
| Mount an existing file system | Wall time of the mount command; median of 10 |
| Create a file, write 4 KiB, fsync, close | On A, 300 new files; median |
| Second client opens and reads that file | On B, the first open, 4 KiB read and close of 300 files A wrote; median |
| Second client sees a newly created file | B lists the directory first, so it is cached. A creates a file and fsyncs it; B checks for it every 10 ms. Time from the start of A's write; median of 8 |
| Read 1 GiB sequentially | On B, 1 MiB reads of a 1 GiB file A wrote |
| New file system to first durable write | Create the file system, mount it, then write 4 KiB and fsync. Archil: create the disk with the API, median of 5. S3 Files: create a versioned bucket, the file system and its mount target, median of 3. Mountpoint: create a bucket, median of 3 |

AWS API calls for the new-file-system test ran from a separate machine, which adds about 0.1 to 0.5 s per call.
Archil's API call and every mount and write ran on the host. The measurement script will follow.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.