Cloudflare Pages is an excellent free hosting platform for static sites. But there's a catch: the free tier limits you to 500 builds per month. If you're actively developing and pushing frequently, you can hit that limit fast.
The solution? Build on GitHub Actions (unlimited and free) and deploy the pre-built files directly to Cloudflare Pages.
Cloudflare Pages Free Tier Limits
Before we dive in, here's what you get for free:
| Feature | Free Tier |
|---|---|
| Static hosting/bandwidth | Unlimited |
| Builds per month | 500 |
| Max deploy size | 1 GB |
| Max file size | 25 MB |
| Pages Functions | 100k requests/day |
The key insight: only Cloudflare-initiated builds count against the 500 limit. Direct uploads via Wrangler CLI are unlimited.
The Strategy
Instead of:
GitHub Push → Cloudflare Builds → Cloudflare Deploys
(counts toward 500)We do:
GitHub Push → GitHub Actions Builds → Wrangler Deploys
(unlimited free) (doesn't count)Step 1: Create the GitHub Actions Workflow
Create .github/workflows/deploy.yml in your repository:
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=YOUR_PROJECT_NAMEReplace YOUR_PROJECT_NAME with your Cloudflare Pages project name.
If you use npm instead of pnpm, adjust the workflow:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run buildStep 2: Create a Cloudflare API Token
- Go to Cloudflare Dashboard
- Click your profile icon → My Profile
- Navigate to API Tokens → Create Token
- Use the "Edit Cloudflare Workers" template, or create a custom token with:
- Permission:
Account→Cloudflare Pages→Edit
- Permission:
- Copy the token (you won't see it again)
Step 3: Get Your Account ID
- Go to Cloudflare Dashboard → Workers & Pages
- Look at the right sidebar for Account ID
- Copy it
Step 4: Add Secrets to GitHub
- Go to your GitHub repository
- Navigate to Settings → Secrets and variables → Actions
- Click New repository secret
- Add two secrets:
- Name:
CLOUDFLARE_API_TOKEN→ Value: your API token - Name:
CLOUDFLARE_ACCOUNT_ID→ Value: your account ID
- Name:
Step 5: Disable Cloudflare Auto-Build
To prevent Cloudflare from also building (and wasting your 500 limit):
- Go to Cloudflare Dashboard → Workers & Pages
- Select your Pages project
- Go to Settings → Builds & deployments
- Find Automatic deployments and set it to Disabled
Alternatively, you can completely disconnect the Git repository from the same settings page.
How It Works
The cloudflare/wrangler-action does the heavy lifting. Under the hood, it runs:
npx wrangler pages deploy dist --project-name=YOUR_PROJECT_NAMEThis uploads your pre-built dist folder directly to Cloudflare Pages. Since Cloudflare doesn't run the build, it doesn't count against your quota.
Local Deploys
You can also deploy manually from your machine:
# First time only - authenticate with Cloudflare
npx wrangler login
# Build your site
pnpm build
# Deploy
npx wrangler pages deploy dist --project-name=YOUR_PROJECT_NAMEBonus: Preview Deployments for PRs
Want preview URLs for pull requests? Extend the workflow:
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Deploy to Cloudflare Pages
id: deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=YOUR_PROJECT_NAME
- name: Comment preview URL on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '🚀 Preview deployed to: ${{ steps.deploy.outputs.deployment-url }}'
})Cost Comparison
| Approach | Monthly Cost | Build Limit |
|---|---|---|
| Cloudflare Free | $0 | 500 builds |
| Cloudflare Pro | $20 | 5,000 builds |
| GitHub Actions + Wrangler | $0 | Unlimited |
For most personal sites and small projects, the GitHub Actions approach is the clear winner.
Conclusion
By moving the build step to GitHub Actions, you get:
- Unlimited builds for free
- Faster builds (GitHub runners are often faster than Cloudflare's build system)
- More control over your CI/CD pipeline
- Preview deployments for pull requests
The only trade-off is a few minutes of initial setup. Worth it.