Direct Upload vs Git Integration on Cloudflare Pages
Cloudflare’s documentation presents Direct Upload and Git integration as two ways to do the same thing. They are not. They differ in who runs the build, and that one difference decides your build environment, your deploy trigger, and what you are looking at when a deployment goes wrong.
This is the comparison I wanted when I set up the site you are reading.
Who runs the build
With Git integration, you hand Cloudflare two things: a repository and a build command. Cloudflare clones the repo, installs dependencies, runs the build on its own machine, and publishes whatever appears in the output directory. You never see that machine, and you cannot inspect it when it disagrees with your laptop.
With Direct Upload, you build locally — or on your own CI — and wrangler pushes the finished directory to Pages. No npm install runs on Cloudflare’s side. No build command runs. Cloudflare receives files.
Everything below follows from that one inversion.
The full Direct Upload path
Two commands create the project and ship it:
npx wrangler pages project create your-project --production-branch=main
npm run build
npx wrangler pages deploy dist --project-name=your-project --branch=main
For scale, on a ten-page Astro site with two content collections plus RSS and sitemap endpoints:
| Step | Measured |
|---|---|
astro build — 10 pages | 8.1s |
| Upload to Pages — 17 files | 2.4s |
| Live on the edge | a few seconds |
Seventeen files is the entire payload of a text-first content site before images: the HTML pages, one stylesheet, robots.txt, ads.txt, llms.txt, and the XML endpoints. There is not much to upload, and the timings show it.
Deployments are also incremental. Wrangler compares content and uploads only what changed. Three consecutive deploys of this site moved 17 files, then 5, then 1 — the last of those after editing a single paragraph, which changed that article’s page and nothing else. The upload step stays cheap as the archive grows.
Where the two paths actually differ
| Git integration | Direct Upload | |
|---|---|---|
| Build machine | Cloudflare’s build image | Yours |
| Deploy trigger | git push | You run wrangler |
| Server-side build | Yes | None |
| Node and dependency versions | Whatever the build image carries | Exactly what you have |
| Preview URLs | One per pull request, automatic | Only with a non-production --branch |
| Rollback | Dashboard, one click | Dashboard, one click |
| Build secrets | Stored in the dashboard | Stay in your local environment |
| Project works without a repo | No | Yes |
The version row is the one people underestimate. “It builds on my machine” is not a promise that it builds on Cloudflare’s, and the failure mode there is an email about a broken deployment rather than a red line in your terminal.
Direct Upload removes that entire class of problem: you ship the exact bytes you tested. The price is that nothing deploys unless you run the command. There is no push-and-forget.
The API makes the difference visible. Ask for a Direct Upload project and there is no source block in the response at all — nothing describes a repository, because there is not one:
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/pages/projects/$PROJECT" \
-H "Authorization: Bearer $API_TOKEN"
The flag that catches people out
--branch does not choose an environment. It labels the deployment with a branch name, and Cloudflare maps that name to an environment. Wrangler’s own help text says it plainly: the name of the branch you want to deploy to.
--branch=main, wheremainis your production branch → production--branch=staging→ preview, served atstaging.your-project.pages.dev- omitted entirely → production
So it is not a production switch. It is an identity tag. Get the name wrong and you will watch a clean, successful upload land on a preview URL while the live site keeps serving the previous build — with a green success message in your terminal the whole way through.
The setting the dashboard will not let you change
Create a project through Direct Upload and the production branch field becomes read-only. Run npx wrangler pages project create without --production-branch and you can end up locked to a value you did not intend.
Cloudflare’s documentation is explicit that the fix is an API call:
curl --request PATCH \
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/pages/projects/$PROJECT" \
--header "Authorization: Bearer $API_TOKEN" \
--header "Content-Type: application/json" \
--data '{"production_branch": "main"}'
Set the branch name at creation time. Changing it afterwards is a different kind of task than the dashboard implies.
Limits worth knowing before you hit them
| Upload method | File count | Per-file size |
|---|---|---|
| Wrangler | 20,000 | 25 MiB |
| Drag and drop | 1,000 | 25 MiB |
A text-first site will never approach either number. A site with a large image library can. If you are near the ceiling, move images to a separate asset host rather than reshaping your build to dodge a limit.
One more: drag-and-drop deployments cannot compile a functions/ directory. If you need Pages Functions, the upload has to go through Wrangler.
Which to pick
Choose Git integration when:
- Several people deploy, and you want shipping to be a side effect of merging
- You want preview URLs on every pull request without thinking about it
- The build is slow and you would rather not run it on your laptop
Choose Direct Upload when:
- You are on a machine that already holds the build output, and a round trip through a repository adds nothing
- You want the deployed artifact to be byte-identical to the one you tested
- Your build needs credentials you would rather not paste into a dashboard
- What you are deploying is not in a repository at all
The hybrid is common and worth considering: keep the repository, and let GitHub Actions run wrangler pages deploy as the final step. You keep version history and a CI record, but the build environment becomes one you defined in a workflow file rather than one you inherited.
Where Pages stops being the right answer
Pages is a static host with an optional edge runtime. It is the wrong tool when you need a database on the same origin, server-side sessions that outlive a single request, or a scheduled job.
You can add those with Workers and D1, and for plenty of projects that is the right call. But at that point you are operating an application, and the hosting decision deserves its own evaluation rather than being inherited from “there is already a Pages project here”.
For a content site, static hosting remains the cheapest and fastest option available. That is the case Direct Upload was built for, and for a one-person publication it is usually the shorter path.
Written by TestedHost. Every recommendation on this site comes from running the setup described, on a live deployment — not from a vendor spec sheet. Spotted something out of date? Tell us.