Three Ways to Deploy Nuxt 4 on Cloudflare (And Why I Chose Pages)
A practical comparison of Cloudflare Pages, Workers, and hybrid deployment for Nuxt 4 — with real costs, real bugs, and what actually works for indie developers.
I wanted to run a tool site with near-zero infrastructure costs. After testing three deployment approaches on Cloudflare, I settled on Pages + R2 storage. The total monthly cost: one domain name.
Here is what I tried, what broke, and why Pages won for my use case.
Who This Is For
This article is for:
- Indie developers building tool sites or SaaS products
- Frontend engineers considering Cloudflare as a deployment platform
- Nuxt developers looking for low-cost hosting options
- Anyone tired of paying $20+/month for Vercel or Netlify Pro
If you want a production-ready Nuxt 4 setup with automatic deployments, static asset storage, and zero monthly fees, this should give you a practical starting point.
Project Context
BulkPicTools is a browser-based image processing tool site. Users can resize, compress, and convert images without uploading them to a server. Most processing happens in the browser.
The tech stack:
- Nuxt 4 (full feature usage: SSG, server routes, middleware, auto-imports)
- Cloudflare Pages (hosting and deployment)
- Cloudflare R2 (image storage and ONNX model files)
- Cloudflare Workers (used in other projects, not the main site)
At the time of writing, the site generates approximately US$200 in monthly revenue. Infrastructure cost beyond the domain: $0.
The Three Approaches I Tested
Approach 1: Cloudflare Pages + R2
This is what I use in production.
How it works:
- Nuxt 4 built with SSG (Static Site Generation)
- Static files deployed to Cloudflare Pages
- R2 bucket stores blog images and ONNX models
- API routes handle sitemap.xml generation
- Git push to main branch triggers automatic deployment
Pros:
- Generous free tier (500 builds/month, 100GB bandwidth)
- Automatic deployments from Git
- Built-in CDN and edge caching
- R2 has zero egress fees
- Custom domain support with free SSL
Cons:
- SSG builds can consume significant memory
- Cloudflare build environment differs from local Node.js
- Some npm packages need special handling
Approach 2: Cloudflare Workers
I tested this for a different project.
How it works:
- Nuxt 4 configured for server-side rendering (SSR)
- Deployed as a Worker script
- Handles requests at the edge
Pros:
- True edge rendering
- Lower latency for dynamic content
- Better for API-heavy applications
Cons:
- Worker runtime has strict memory and CPU limits
- Not all Node.js APIs are available
- Requires Nitro preset configuration for Cloudflare
- More complex debugging
Approach 3: Hybrid (Pages + Workers)
This combines both approaches.
How it works:
- Static pages served from Pages
- Dynamic API routes handled by Workers
- R2 for asset storage
Pros:
- Best of both worlds
- Static assets are fast and cheap
- Dynamic logic runs at the edge
Cons:
- More complex architecture
- Need to manage two deployment targets
- Harder to debug across environments
Why I Chose Pages
For a tool site with mostly static content, Pages was the clear winner.
Decision factors:
The key insight: Most tool sites are 90% static content. The pages users visit (tool interfaces, blog posts, landing pages) do not need server-side rendering. They just need fast CDN delivery.
Cloudflare Pages handles this perfectly. The API routes I need (sitemap generation) work within Pages’ limitations.
The Bugs I Hit
Bug 1: Cloudflare Build Environment Differences
Problem: Packages that work locally failed during Cloudflare builds.
Cloudflare’s build environment does not provide a full Node.js runtime. Some npm packages assume access to native modules or system APIs that are not available.
One specific issue: the Nuxt Content module (used for blog posts) failed during SSG builds on Cloudflare. The module tries to query content at build time, but the Cloudflare environment does not support this properly.
Get Genglin’s stories in your inbox
Join Medium for free to get updates from this writer.
Solution: I created a mock file to replace the Content module’s query functions during Cloudflare builds:
// mock-content.ts
// Mock queryCollection for Content v3
export const queryCollection = () => {
return {
all: () => Promise.resolve([]),
where: () => ({
all: () => Promise.resolve([])
})
}
}
// Mock serverQueryContent for Content v2
export const serverQueryContent = () => {
return {
find: () => Promise.resolve([]),
where: () => ({
find: () => Promise.resolve([])
})
}
}
export default {
queryCollection,
serverQueryContent
}This mock returns empty arrays, allowing the Sitemap module and other content-dependent features to complete without errors. The actual content is still loaded client-side from the CMS.
Lesson: Test your build on Cloudflare early. Do not assume local builds will translate directly. When packages fail, creating targeted mocks is often faster than finding alternatives.
Bug 2: SSG Build Memory Usage
Problem: Nuxt 4 SSG builds consumed more memory than Cloudflare’s build environment expected.
Static Site Generation needs to render every page at build time. For sites with many pages or complex components, this can exceed memory limits.
Solution: I modified the build command to allocate more memory:
NODE_OPTIONS=" - max-old-space-size=8192" npm run buildThis sets the maximum heap size to 8192 MB (8 GB). Cloudflare Pages build environment supports this allocation, and it resolved the out-of-memory errors for my SSG builds.
I also optimized the build process by:
- Reducing the number of pages generated at build time
- Simplifying complex components
- Using dynamic imports where possible
- Monitoring build memory usage
Lesson: SSG is great for performance, but the build process has real costs. If your builds fail with memory errors, adjusting `NODE_OPTIONS` is the first thing to try. Plan your page structure accordingly.
Bug 3: R2 Path Configuration
Problem: Serving files from R2 required careful path configuration.
R2 does not serve files like a traditional CDN out of the box. You need to configure routes or use a Worker to handle requests.
Solution: I set up proper path mapping between my Nuxt routes and R2 object keys. Blog images use a predictable path structure that maps directly to R2 storage.
Lesson: Plan your R2 path structure before you start storing files. Changing paths later means updating all references.
R2 Storage: Images and Models
I use R2 for two purposes:
1. Blog images
All images for blog posts are stored in R2. This keeps my Pages deployment lightweight and reduces build times. The images are served through a custom domain with proper caching headers.
2. ONNX models
For browser-based AI features, I store ONNX model files in R2. Users download these models directly from R2 when they need them, without hitting my server.
Why R2 instead of a traditional CDN:
- Zero egress fees (this matters at scale)
- S3-compatible API (easy to integrate)
- Built into Cloudflare ecosystem
- Generous free tier (10GB storage, 10 million Class A operations/month)
The Deployment Workflow
My deployment process is intentionally simple:
git push to main branch
↓
Cloudflare detects changes
↓
Cloudflare runs build command
↓
Static files deployed to Pages
↓
Site liveNo Kubernetes. No CI/CD pipelines. No Docker containers. No server management.
For an indie developer, this simplicity is a feature, not a limitation. I spend my time building products, not maintaining infrastructure.
Build times to expect:
My site has 1000+ pages. With SSG, Cloudflare needs to render every page at build time. Realistic build time: around 15 minutes.
This is not a problem for my workflow. I verify changes locally before pushing. The 15-minute wait is just background processing — I continue working on other things while it builds.
Build configuration:
- Build command: `NODE_OPTIONS=” — max-old-space-size=8192" npm run build`
- Build output directory: `.output/public`
- Node.js version: 22 (or whatever Nuxt 4 requires)
- Memory allocation: 8192 MB (required for SSG builds with many pages)
- Typical build time: ~15 minutes for 1000+ pages
Cost Breakdown
The free tiers cover everything I need:
- Pages: 500 builds/month, 100GB bandwidth
- R2: 10GB storage, 10M Class A / 10M Class B operations
- Workers: 100,000 requests/day
I have never hit these limits. With local verification before deploying, I average around 20–30 builds per month. The 500 build limit is more than enough.
When This Approach Works
Good fit:
- Tool sites with mostly static content
- Blog-driven sites with static pages
- Landing pages and marketing sites
- Small to medium SaaS products
- Projects where cost is a primary concern
Bad fit:
- Applications with heavy server-side logic
- Real-time collaborative features
- Large-scale API services
- Projects requiring database connections at the edge
- Applications with strict Node.js API requirements
What I Would Change
If I started over:
1. Test the build environment earlier. I wasted time fixing build issues that could have been caught on day one.
2. Plan R2 paths from the beginning. My initial path structure needed refactoring later.
3. Use hybrid approach for API-heavy projects. For BulkPicTools, Pages is fine. For projects with more dynamic content, I would consider hybrid from the start.
4. Monitor build performance. SSG builds can degrade silently as the site grows. Set up alerts early.
Key Takeaways
- Cloudflare Pages + R2 can run a profitable tool site for ~$1/month (just the domain)
- Three deployment options exist: Pages, Workers, and Hybrid — pick based on your content type
- SSG builds have real memory costs — plan your page structure accordingly
- Cloudflare’s build environment differs from local Node.js — test early
- R2 is ideal for static assets with zero egress fees
- Simple deployment (git push → auto deploy) saves more time than you think
CTA
If you want to see a live example of this stack, you can check out BulkPicTools: https://bulkpictools.com
The blog runs on Nuxt 4 + Cloudflare Pages, with images stored in R2. The infrastructure cost is zero beyond the domain.








