Skip to content

Benchmarks

Results

  • PutFS serves concurrent small file reads 2-3x faster than MinIO
  • PutFS writes are 1.6-5x faster than MinIO
  • PutFS directory listings are 10-65x faster, the gap growing with directory size
  • PutFS uses 37x less memory
  • PutFS reads a 1 GB file 2x faster
  • MinIO wins on bulk parallel uploads (mc mirror vs putfs sync)

Both backends ran on the same server and the same ext4 SSD, PutFS as the Rust build behind nginx, see Test setup.

Small file random reads (10K files, 1K–100K, wrk, 30s)

Concurrency PutFS MinIO Ratio
1 5,968 req/s 3,150 req/s 1.9x
10 39,876 req/s 18,158 req/s 2.2x
100 73,668 req/s 23,408 req/s 3.1x
1000 77,693 req/s 23,518 req/s 3.3x

nginx serves files via sendfile straight from the page cache; the app server is never involved, so reads are the same for every PutFS build.

Single-file write latency (hyperfine, mean)

File size PutFS MinIO Ratio
1 KB 8.1ms 41.0ms 5.0x
10 KB 9.6ms 33.1ms 3.5x
100 KB 7.7ms 31.4ms 4.1x
1 MB 8.1ms 34.5ms 4.3x
10 MB 38.2ms 110.2ms 2.9x
100 MB 323ms 616ms 1.9x
1 GB 3.1s 5.1s 1.6x
10 GB 38.7s 123.5s 3.2x

Each size re-uploads one key, so every run after the first replaces an existing object. ext4 writes a file's data out when a rename replaces another file (auto_da_alloc, on by default), so from 10 MB on PutFS writes at disk speed. A new key does not pay this:

1 GB upload PutFS MinIO
New key (one run) 1,060 MB/s 81 MB/s
Replacing a key 330 MB/s 200 MB/s

Bulk upload (10K files, 1K–100K)

System Method files/s
MinIO mc mirror 1,365
PutFS putfs sync 1,126

Go's native concurrency in mc mirror beats the threaded Python putfs sync.

Large file sequential read (1 GB)

System Time Throughput
PutFS 224ms 4,562 MB/s
MinIO 469ms 2,183 MB/s

PutFS is read with curl (nginx sendfile), MinIO with mc cat.

Directory listing

Dataset size PutFS MinIO Ratio
1K objects 6.7ms 70ms 10x
10K objects 13ms 460ms 34x
100K objects 64ms 4,180ms 65x

One full listing per run, client process included (curl with QUERY vs mc ls), which is most of the 1K figure. For the server's walk alone see the wrk listing numbers below.

Memory footprint (idle)

System Total
PutFS 66.5 MiB (API: 42.0 MiB + nginx: 24.5 MiB)
MinIO 2,431 MiB

PutFS uses 37x less memory. This is each container's anonymous memory (heap, stacks) from its cgroup, sampled after the run and unchanged under 100 concurrent readers.

Summary

Metric PutFS MinIO Ratio
Reads (c=1000) 77,693 req/s 23,518 req/s 3.3x
Write 1 KB 8.1ms 41.0ms 5.0x
Write 1 GB 3.1s 5.1s 1.6x
Large read 1 GB 4,562 MB/s 2,183 MB/s 2.1x
List 100K 64ms 4,180ms 65x
Bulk upload 1,126 files/s 1,365 files/s 0.8x
Memory idle 66.5 MiB 2,431 MiB 37x

Tuning

A Unix socket between nginx and granian cut write latency ~15–20%. See Granian tuning and Nginx tuning.

access_log

access_log /dev/stdout cut throughput from 104K req/s to 3.8K req/s in our benchmarks. Keep access_log off, see Nginx tuning.


Test setup

Both ran in docker on the same server, with their data on the same local SSD (ext4) and the benchmark clients on the host. The seed and write source files were generated on a tmpfs (TESTDATA_DIR), off that SSD.

Tuning

  • PutFS:
    • the Rust build (docker-compose.rust.yml) on a unix socket
    • nginx, no access logs
  • MinIO:
    • MINIO_API_REQUESTS_MAX: "0"
    • MINIO_SCANNER_SPEED: "fastest"
    • MINIO_BROWSER: "off"
    • GOMAXPROCS: "0"

Hardware

Component Spec
CPU Intel Core i7-8700 @ 3.20 GHz (6 cores, 12 threads)
RAM 64 GB DDR4
Storage local SSD, one ext4 partition for both backends
Test data tmpfs
OS Debian 13 (trixie), kernel 6.12

Running

Scripts live in benchmark/ (README); results are read with analyze.sh, one CSV column per result directory:

The two backends run as separate commands, never in one sequence, so the disks can be wiped or re-laid between them. TESTDATA_DIR (default /tmp, about 12 GB) keeps the generated files off the backend's disk.

docker compose -f docker-compose.yml -f docker-compose.rust.yml up -d --build   # PutFS: the Rust build behind nginx
make -C benchmark putfs
# tear down, wipe or re-lay the disks
docker compose -f contrib/docker-compose.minio.yml up -d
make -C benchmark minio
bash benchmark/analyze.sh benchmark/results/minio/putfs benchmark/results/minio/minio

Implementation comparison (Python / Go / Rust)

The Go and Rust ports share the Python build's HTTP API; only the language differs. This benchmark isolates the app-server hot path (PUT / DELETE / LIST; GET/HEAD are nginx's job in production) by running each server bare on localhost, no nginx, no auth, driven by wrk.

Different setup from the MinIO comparison above

Developer laptop, page cache, 4 KB objects, no fsync: these measure per-request handling overhead, not disk throughput, and are not comparable to the server numbers of the MinIO comparison. Listing uses wrk over a kept-alive connection, the server's real scandir + serialise cost, which is why it grows with object count here.

Parallel PUT throughput (4 KB objects, wrk)

Concurrency Python (granian, 16 workers) Go Rust
1 1,435 req/s 8,350 req/s 8,506 req/s
10 3,945 req/s 31,255 req/s 51,061 req/s
100 6,479 req/s 67,857 req/s 35,656 req/s

Directory listing (wrk, keep-alive)

Objects Python Go Rust
1K 4.2 ms · 240k keys/s 1.3 ms · 755k keys/s 0.83 ms · 1.26M keys/s
10K 37 ms · 268k keys/s 9.6 ms · 1.04M keys/s 5.4 ms · 1.84M keys/s
100K 362 ms · 265k keys/s 73 ms · 1.36M keys/s 37 ms · 2.71M keys/s
  • Listing: Rust wins, ~1.7–2.0× Go and ~5–10× Python, widening with directory size.
  • Writes: Rust leads at c10 (~1.6× Go), Go at c100 (~1.9× Rust), c1 is a tie. Rust offloads each PUT's open and rename to a thread pool (as Python's anyio.to_thread does), which costs it only at high fan-out.
  • Both ports are ~5–13× Python on writes. Python's per-line async listing tops out at ~240–270k keys/s regardless of size.

Intel Core i7-1360P (16 threads), 30 GB RAM, Debian forky/sid (kernel 7.0.10); bare servers on localhost, page cache, mean of three wrk runs. Not the server of the MinIO comparison.

make build-rust build-go     # the native binaries (repo-root Makefile)
cd benchmark
make impls                   # python + go + rust
bash analyze.sh results/impls/*