GIT LFS · PAGES MIDDLEWARE

Serve large files from Git LFS.

A Cloudflare Pages middleware that serves your repository's Git LFS objects in place of pointer files — past the 25 MiB Pages file-size limit. Stateless, range-aware, no servers to run.

Why git-lfserve

Cloudflare Pages refuses to serve any static file larger than 25 MiB. Video, datasets, disk images and other large assets simply can't ship as ordinary files — Git LFS replaces them in your repository with small text pointers, and Pages serves the pointer, not the file.

git-lfserve runs as Pages middleware. When Pages is about to serve a Git LFS pointer, the worker resolves the object it references and serves the real bytes in its place. Large files stay in LFS; visitors get the file.

A request for a large assetPlain PagesWith git-lfserve
Files under 25 MiBServedServed (passes through)
Files over 25 MiBBuild fails / 404Object served from LFS
What the visitor receivesThe LFS pointer textThe real bytes
Range requests (video seek)206 Partial Content

How it works

The worker installs as functions/_middleware.js — a thin entry point over six modules in src/ (handler, pointer, lfsconfig, batch, objects, http). On every GET and HEAD it lets Pages produce the response, reads the first 256 bytes, and checks whether they are a valid LFS pointer — the spec version line, a 64-hex oid and an integer size. Anything that isn't a pointer passes through untouched.

Two ways to resolve an object

  • Bound R2 bucket. When LFS_BUCKET and LFS_BUCKET_URL are both set, the worker reads the object straight from the bound bucket — no round-trip to an LFS server, and full GETs are cached as immutable.
  • Upstream LFS server. Otherwise it reads the server URL from your .lfsconfig, calls the LFS batch API for a download action, and streams the object from there.

Range requests

HTTP Range works on both paths, so video seeking and partial downloads behave correctly: the bucket path returns 206 Partial Content straight from R2, and the LFS-server path forwards Range to the object store. Every object response carries Content-Length and Accept-Ranges: bytes.

Trust model
The worker is stateless. Credentials embedded in a .lfsconfig URL are forwarded only to the upstream LFS batch endpoint — never stored, never logged. /.lfsconfig is never served, and any resolution failure returns a generic 502 with the cause logged privately. See SECURITY.md.

Install Git LFS

Run every command from your repository root:

cd "$(git rev-parse --show-toplevel)"
    git lfs version

If the command reports git: 'lfs' is not a git command, follow the upstream installation instructions. The binary doesn't always register the smudge and clean filters, so install them for your user account:

git lfs install

Install the worker

The worker must run as middleware, so _middleware.js needs to become functions/_middleware.js in your repository, alongside a symlink functions/.lfsconfig.txt.lfsconfig so it can read your LFS configuration.

If your site has no functions directory, the simplest route is to add this repository as a submodule named functions:

git submodule add https://github.com/khwstolle/git-lfserve functions
    git commit -m "Add Git LFS Client Worker"

Disable LFS fetching by default

The worker resolves pointers on the fly, so the standard client must not also fetch them when Cloudflare Pages clones your repository — if it does, builds fail at the "Cloning git repository" step even though LFS holds every large file. Disable the filters in .lfsconfig (environment variables don't appear to reach that build step):

git config -f .lfsconfig lfs.fetchexclude '*'
    git add .lfsconfig
    git commit -m "Disable LFS fetching by default"

After checking out a commit with this change, Git LFS leaves pointers in place. Restore normal behaviour in your own clones by overriding the setting locally:

git config lfs.fetchexclude ''

Choose an LFS server

GitHub and GitLab both run default LFS servers, but neither was built to serve web content — storage and bandwidth are metered and shared, and returning an object's URL alone takes around 270 ms.

Pair it with git-lfs3
Back your repository with git-lfs3 on R2. Cloudflare's free tier serves up to 10 GB with unlimited bandwidth and the lowest possible latency — your objects sit in the same datacenters as the worker. Beyond 10 GB, storage is $0.015/GB·mo.

Whichever server you use, specify its URL in .lfsconfig. The worker has no other way to find it — a Pages worker doesn't know which repository built its site.

Track large files

At a minimum, route every file above the 25 MiB Pages limit through LFS:

find . -type f '!' -path './.*' -size +25M -exec git lfs track {} +
    find . -type f '!' -path './.*' -size +25M -exec git add --renormalize {} +
    git add .gitattributes
    git commit -m "Add files over 25 MiB to Git LFS"

Track each new large file before committing it, or track a whole type by pattern when it consistently exceeds the limit:

git lfs track bigfile      # one file
    git lfs track '*.mp4'      # a whole type

Create a Pages site

Create a Cloudflare Pages site from your repository, or push to trigger a rebuild. The middleware is picked up automatically from functions/.

git push

Bind an R2 bucket (optional)

If you switched to git-lfs3 backed by R2, binding the bucket to your Pages site lets the worker read objects straight from R2 — skipping the presigned-URL round-trip for the best performance.

LFS_BUCKETR2 bucket binding — the bucket you created for git-lfs3.
LFS_BUCKET_URLYour LFS server URL without the access key: https://<INSTANCE>/<ENDPOINT>/<BUCKET>.
Show dashboard steps

In the Workers & Pages dashboard, open your Pages site:

  • Settings → Functions → R2 bucket bindings → Production — add a binding with variable name LFS_BUCKET pointing at your LFS bucket.
  • Settings → Environment variables → Production — add LFS_BUCKET_URL with the value above.
  • Deployments → Production → Manage deployment → Retry deployment — redeploy so the bindings take effect.

With both variables set, the worker fetches objects directly through the binding instead of asking the LFS server for presigned URLs.

Configuration

The worker reads your repository's .lfsconfig (via the .lfsconfig.txt symlink) plus optional Pages environment bindings.

KeyDefaultPurpose
.lfsconfig urlUpstream LFS server URL. Resolved in order: [lfs] url, then [remote "origin"] lfsurl, then any other [remote] lfsurl.
LFS_BUCKETunsetR2 bucket binding. With LFS_BUCKET_URL, objects are read straight from the bucket.
LFS_BUCKET_URLunsetLFS server URL without credentials, used to key the bucket cache.
KEEP_HEADERSCache-ControlComma-separated headers copied from the original page response onto the served object.

If the worker can't resolve an object, it logs the cause server-side and returns 502 Bad Gateway.