Skip to content

Start here

lith — read-only data access for S3: mount locally, serve a cluster, read only what you touch

Five minutes from nothing to a real answer, against a public Registry of Open Data bucket. You need a Linux box (lith uses FUSE) and samtools for the last step. Nothing is written to the bucket; nothing but an index file is written locally.

Why "lith"? As in lithic / lithology — rock strata. lith serves a bucket's objects as read-only strata, exactly as they were laid down; the bucket's native key layout is the stratum, and lith never rewrites it.

1. Install

Download the static binary for your architecture from the releases page:

# arm64 (Graviton, Apple-on-Linux VMs, …)
curl -fsSL https://github.com/scttfrdmn/lith/releases/latest/download/lith_linux_arm64 -o lith
# or x86-64
curl -fsSL https://github.com/scttfrdmn/lith/releases/latest/download/lith_linux_amd64 -o lith
chmod +x lith && sudo mv lith /usr/local/bin/
lith version

Or, with Go installed: go install github.com/scttfrdmn/lith/cmd/lith@latest. The releases page also has .deb/.rpm packages (sudo dpkg -i lith_*_linux_amd64.deb) and a container image (ghcr.io/scttfrdmn/lith). Each release is checksummed, cosign-signed, and carries an SBOM + build provenance — see Verifying a release.

1.5. Check your setup (lith doctor)

One command tells you whether lith will work here before you mount anything — credentials, bucket access and region, LIST permission, FUSE, and the mountpoint. Each check prints PASS/FAIL/N/A with a one-line fix; it exits non-zero if anything fails.

lith doctor s3://1000genomes/phase3/data/HG00100/alignment --no-sign-request

It catches the things that otherwise surface as a cryptic mount error: a region-mismatched client, a bucket that denies ListObjectsV2 (it points you at --keys), missing FUSE, or a sudo-created mountpoint that's root-owned. Run it first on a new box.

2. Build the index

s3://1000genomes is public, so pass --no-sign-request. Index one sample's alignment folder — a handful of keys, so the build is instant:

lith index build s3://1000genomes/phase3/data/HG00100/alignment \
  --no-sign-request --index-file /tmp/hg00100.lithidx
lith index inspect /tmp/hg00100.lithidx

inspect prints the key count and a few stats. This listing is the only time lith calls S3 for metadata; from here ls and stat are answered locally.

3. Mount it

sudo mkdir -p /mnt/hg00100 && sudo chown "$USER" /mnt/hg00100  # you must own the mountpoint
lith mount s3://1000genomes/phase3/data/HG00100/alignment /mnt/hg00100 \
  --index-file /tmp/hg00100.lithidx --no-sign-request --daemon
ls -lh /mnt/hg00100

(fusermount3 requires write access to the mountpoint, so the user running lith must own it — hence the chown after the sudo mkdir.)

Scripting a readiness check: run it as the user that created the mount

A FUSE mount created by root cannot be stat'd by another user, so mountpoint -q /mnt/… reports not mounted and a readiness loop times out while the mount is in fact serving. Reported from a benchmark harness where this stacked three mounts behind one failed check.

lith mounts does not rescue you, for a different reason: it reads a per-user record directory and deliberately ignores records from a directory it does not own, so a root-created mount is equally invisible to a non-root lith mounts.

It is symmetric, and the other direction bites too: sudo mountpoint -q cannot see a user-owned FUSE mount either, so a root-run check against a user-created mount also reports not mounted. That one cost a benchmark harness 60 seconds of idle wait per cell before anyone noticed.

The rule, both ways: run the readiness check as the same user that created the mount. mountpoint -q or lith mounts, as that user. Better still, avoid the asymmetry entirely — mount as the user that will read, which is what the chown above is for.

ls returns immediately and shows a 14 GiB …low_coverage…bam.cram and its .crai index — real sizes and mtimes, served from the local index without touching S3.

4. Ask a question

Count the reads in a 1 Mb slice of chromosome 20. samtools seeks into the CRAM through the mount; lith fetches only the bytes those reads occupy (it fetches the CRAM reference from the public EBI registry automatically on first use, so this step needs outbound internet):

samtools view -c /mnt/hg00100/HG00100.*.low_coverage*.cram 20:1000000-2000000

You get a count in a second or two, having moved on the order of tens of MB — not the whole 14 GiB file.

5. Measure the read path

lith bench reads an object through a mount and reports throughput, S3 requests, and an estimated cost, cold then warm:

lith bench s3://1000genomes/phase3/data/HG00100/alignment/HG00100.mapped.ILLUMINA.bwa.GBR.low_coverage.20130415.bam.cram \
  --pattern seq --no-sign-request

6. Unmount

lith umount /mnt/hg00100

lith umount signals the mount process and waits for it to leave the mount table (falling back to fusermount3 -u if needed); lith mounts lists every live lith mount. You can still fusermount3 -u by hand.

Mount at any prefix

The s3://bucket/prefix you pass to mount becomes the root of the filesystem — above, /mnt/hg00100 is rooted at phase3/data/HG00100/alignment/, so its top-level entries are that folder's contents, not the whole bucket. One prebuilt index can back many prefix mounts at once: build a wide index once and mount any prefix at or below its root, concurrently, with no rebuild —

lith index build s3://1000genomes/phase3/data --no-sign-request --index-file /tmp/phase3.lithidx
lith mount s3://1000genomes/phase3/data/HG00096 /mnt/hg00096 --index-file /tmp/phase3.lithidx --no-sign-request --daemon
lith mount s3://1000genomes/phase3/data/HG00100 /mnt/hg00100b --index-file /tmp/phase3.lithidx --no-sign-request --daemon
lith umount --all

What just happened

  • You listed once. The index build was the single metadata pass; every ls, stat, and open after it was served locally with zero S3 calls.
  • You read only what you touched. The region query answered a question about a 14 GiB object by pulling a small fraction of it — no staging, no copy, no volume to size or delete.
  • You wrote nothing to the bucket. lith is read-only by construction; the only local artifact is the index file you named.

Whether that trade — no staging, bytes-you-touch — actually beats copying the file first depends on your access shape and your box. That is the next page: Copy or mount?