Skip to content

Cache Git LFS objects - #595

Merged
andrew merged 4 commits into
git-pkgs:mainfrom
simonchrz:feat/git-lfs-cache
Oct 25, 2026
Merged

andrew merged 4 commits into
git-pkgs:mainfrom
simonchrz:feat/git-lfs-cache

Conversation

@simonchrz

@simonchrz simonchrz commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Closes #517.

git-lfs finds the LFS API under the rewritten remote, at /git/{name}/{repo}.git/info/lfs, so clones through the proxy already asked it for LFS objects and got 404s. The proxy now serves:

  • POST .../info/lfs/objects/batch for downloads. The batch goes to the upstream, which decides which objects the repository has, and each download is pointed at .../info/lfs/objects/{oid}/{size} with authenticated: true and none of the upstream's headers. Upstream errors per object are passed on, and objects the client didn't ask for are dropped.
  • GET .../info/lfs/objects/{oid}/{size} from the artifact cache (ecosystem git, name {name}/{repo}, version the OID), so the S3/GCS/R2 backends, DirectServe and retention apply. On a miss the proxy uses the download URL from the client's batch, kept in memory until shortly before it expires, so the GET needs no second batch request. Without one, or when a kept URL fails, it asks the upstream batch API for a fresh one. It fetches with the headers the upstream gave, cuts off a download that isn't the object's size, and checks the content against the OID before caching it. A HEAD on a miss is answered from the batch response without downloading.
  • If the object's content is already stored under another repository, such as a fork, it is copied from there instead of downloaded, after the upstream has confirmed this repository has the object. This saves the download, not the space. artifacts.content_hash gets an index (migration 017) for the lookup.
  • When the upstream can't be reached or answers 5xx, a batch is answered with the objects cached for the repository, so cached objects stay available during an outage. A refusal from the upstream is passed on.

Download hrefs are usually signed. The proxy fetches the signed URL but logs and records only the URL without its query, also in fetch errors.

For the open question in the issue, the proxy fetches from any href the batch response returns:

  • every fetch still goes through the address checks;
  • content that doesn't match its OID is never cached;
  • upstream.auth credentials only go to the hosts they're configured for.

A per-upstream allowlist of object hosts could follow as an option if someone needs it.

Objects are cached per repository, so having an object cached for one repository doesn't serve it for another. Uploads, locking, other transfers and hash algorithms are refused with LFS error documents.

I also looked at saracen/lfscache. It avoids the second batch request on a miss by handing the original href and headers to the client in signed headers. I didn't do that, because those headers can carry tokens the upstream issued for the proxy's upstream.auth credentials, and the hrefs expire anyway.

Tests are in internal/handler/git_lfs_test.go: a batch API whose hrefs point at a separate object store that needs the batch's header; served from the cache with both down; a batch answered from the cache when the upstream fails; no signature in logs or the artifact record; HEAD without a download; no second batch after the client's, and a fresh URL when the kept one fails; kept URLs expiring; a download of the wrong size cut off; a copy for a fork, and none for a repository the upstream says lacks the object; a tampered object is refused and not cached; refusals.

I also ran it end to end: git-lfs 3 in an Alpine container cloned hf-internal-testing/tiny-random-gpt2 from huggingface.co twice through the proxy. Its LFS hrefs point at Hugging Face's CDN. The first clone fetched three objects from the CDN, the second got them from the cache, and git lfs fsck passed both times. A third clone with the upstream unreachable got Git and LFS from the cache.

git-lfs finds the LFS API under the rewritten remote, so clones through
/git/ already asked the proxy for LFS objects and got 404s. The proxy now
serves the batch API for downloads: it forwards the batch to the upstream,
which decides which objects the repository has, and points each download
at the proxy. Objects are served from the artifact cache. A miss asks the
upstream batch API for a fresh download URL, fetches it and checks it
against its sha256 object ID before caching it.

Download URLs often name another host than the Git server, such as object
storage. The proxy fetches from whatever host the upstream names, with the
usual address checks; upstream.auth credentials stay scoped to their
hosts. Objects are cached per repository. Uploads and locking are refused.

Closes git-pkgs#517.
- Keep signed download URLs out of logs and the database: the fetch uses
  the signed URL, everything else the URL without its query.
- Answer a batch from the cache when the upstream can't be reached or
  fails, so git-lfs can use cached objects during an outage. Refusals
  are still passed on.
- Answer a HEAD for an uncached object from the batch response instead
  of downloading it.
- Keep the download URLs of a batch response in memory until shortly
  before they expire, so the GET that follows needs no second batch
  request. A kept URL that fails is replaced by a fresh one.
- Cut off a download that isn't the object's size.
- Copy an object whose content is already stored, such as for a fork,
  instead of downloading it, once the upstream has confirmed the
  repository has it. Artifacts are indexed by content hash for this.
@andrew
andrew force-pushed the feat/git-lfs-cache branch from d3865c7 to bcda24e Compare October 25, 2026 18:20
@andrew
andrew merged commit 05ad702 into git-pkgs:main Oct 25, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache Git LFS objects

2 participants