The production stack
A single R2/S3 read is cheap. A repo page that renders a file tree, a commit log, and a diff is not — it can mean hundreds of object reads. This guide is the stack a production git-hosting service actually runs, in the order it actually runs in, with the reasoning for each layer.
The full stack
Section titled “The full stack”import { createCachedStore, createGitFs, createRetryStore,} from 'git-fs-s3';import { S3ObjectStore } from 'git-fs-s3/s3';
const store = createCachedStore( createRetryStore( new S3ObjectStore({ client, bucket: 'my-git-repos', contentType: (key) => /\/(HEAD|config)$|\/refs\//.test(key) ? 'text/plain' : undefined, }) ), { maxBytes: 256 * 1024 * 1024, ttlMs: 3_600_000, cacheMisses: true, cacheLists: true, });
const fs = createGitFs(store, { looseObjectHints: true, isStructurallyAbsent: (p) => /(^|\/)git\/(packed-refs|shallow)$/.test(p),});Why this exact order
Section titled “Why this exact order”-
S3ObjectStoreat the bottom. The real network call. -
createRetryStorewraps it directly. Retry belongs as close to the network as possible: retrying at this layer means every consumer above (including the cache) sees a request that either eventually succeeds or fails for real — never a request that failed transiently and got treated as a permanent miss. -
createCachedStorewraps the retry store. This is the order that matters most: if the cache were underneath retry, a transient failure during a cached-misses lookup could get memoized as “not found” for the full TTL. With retry underneath, the cache never even sees a transient failure — only a real result or a real (rare, already-retried) error. -
createGitFswraps the cache. The git-aware layer (loose-object hints, structural-absence short-circuits) sits on top so its own bookkeeping benefits from everything below it being fast and resilient.
Loose-object hints
Section titled “Loose-object hints”isomorphic-git probes for a loose object before falling back to searching
packs. On a fully packed repository, every one of those probes is a
guaranteed miss — pure wasted round trips. looseObjectHints: true tracks,
per gitdir, whether any loose object has ever been seen, so a packed repo
skips the probe entirely.
Hints don’t self-populate — call detectLooseObjects(gitdir) once per
gitdir before a full-history walk:
await fs.detectLooseObjects(gitdir); // one bounded LIST per repoA loose write flips the hint back on instantly, so it’s impossible for a hint to go stale mid-push and hide a just-written object.
isStructurallyAbsent
Section titled “isStructurallyAbsent”Some paths git probes constantly but a given backend simply never writes —
packed-refs and shallow, for a backend that never packs refs or does
shallow clones. isStructurallyAbsent answers ENOENT for them with zero
store calls, not even a cached miss.
Pack prefetching
Section titled “Pack prefetching”Before a full-history walk (a commit log, a reachability traversal), warm every pack file in parallel instead of paying one round trip per pack as the walk discovers each one sequentially:
await fs.prefetchPacks(gitdir);Invalidation
Section titled “Invalidation”Anything that writes to the bucket around this fs — a hydrate/sync
pipeline, a bulk upload, another process — must invalidate the affected
state, or reads can stay stale for up to ttlMs:
fs.invalidate('repos/alice/blog');This forwards to the underlying cached store’s own invalidate(prefix) and
also clears any loose-object hints under that prefix.