# Cloudflare or S3 and CloudFront for a website?

> Where should a website, SPA or PWA live, Cloudflare static hosting or S3 behind CloudFront, and where do its heavy files go? Cloudflare static hosting by default: push to main and it's live, with static requests free and unlimited. S3 and CloudFront when the client's policy keeps every component inside their AWS accounts, or the origins must stay private in AWS. Often both: the site on Cloudflare and its API on AWS, or S3 in Sydney as the system of record with R2 serving the public copy.

- A choice between tools from The Stack by Nicolas Dao (Cloudless Labs, Sydney): when to pick each, and when to run both
- Web page: https://cloudlesslabs.com/stack/guides/static-hosting/. The whole stack in one file: https://cloudlesslabs.com/stack/brief.md
- Tools: [Cloudflare static hosting](https://cloudlesslabs.com/stack/cloudflare-static-hosting/index.md) (My default), [Amazon CloudFront](https://cloudlesslabs.com/stack/aws-cloudfront/index.md) (Use when it fits), [Amazon S3](https://cloudlesslabs.com/stack/aws-s3/index.md) (My default), [Cloudflare R2](https://cloudlesslabs.com/stack/cloudflare-r2/index.md) (Use when it fits), [Astro on Cloudflare Workers](https://cloudlesslabs.com/stack/astro-on-workers/index.md) (Use when it fits), [Next.js on Cloudflare Workers (OpenNext)](https://cloudlesslabs.com/stack/nextjs-on-workers/index.md) (Use when it fits), [Vercel](https://cloudlesslabs.com/stack/vercel/index.md) (Skip), [Netlify](https://cloudlesslabs.com/stack/netlify/index.md) (Skip), [Route 53 and Cloudflare DNS](https://cloudlesslabs.com/stack/dns/index.md) (My default)

## When to pick which

Pick Cloudflare static hosting when:

- Marketing sites, docs, landing pages, SPAs and PWAs that should never need a server.
- A static front end whose API lives elsewhere: a Lambda function URL in Sydney, or a Worker on the same domain.
- Sites that get sudden spikes: static requests aren't metered, so a viral day costs the same as a quiet one.
- Some pages must render on request: Astro or Next.js on Workers keep the static part free and run code only for those routes.

Pick S3 and CloudFront when:

- The client's policy says every component runs inside their AWS accounts.
- Origins that must stay private inside AWS: a private bucket through origin access control, an internal load balancer through VPC origins.
- Paid or private files from S3: signed URLs or signed cookies, checked at the edge.
- A public site under 50 TB and 125M requests a month where the client wants one AWS bill: a flat-rate plan caps it.

Pick R2 for the files when:

- Media, downloads and exports served publicly at volume: R2 charges $0 egress where S3 charges $0.114/GB from Sydney.
- Files over 25 MiB, which static hosting won't take.

Both, when:

- The site on Cloudflare static hosting, its API and data on AWS: Cloudflare DNS for the front door, Route 53 for the API and internal names, a subdomain delegated with NS records.
- S3 in Sydney as the private, versioned system of record and R2 as the public delivery copy, filled lazily by Sippy so AWS egress is paid once per object.
- An all-AWS site on pay-as-you-go CloudFront with Cloudflare in front, when a viral page would otherwise become the bill.

Questions that change the answer:

- Must every component run inside the client's AWS accounts? Then it's S3 and CloudFront, whatever the site is.
- Must the files stay in Australia? R2 has no Australian jurisdiction, only a best-effort Oceania hint: use S3 in Sydney.
- How much public traffic, in TB and requests a month? Above 50 TB or 125M requests, a CloudFront flat-rate plan no longer covers it.
- Does any page render per request from live data? Then it's Astro or Next.js on Workers, not plain static hosting.
- Will anyone technical run it? If not, Vercel or Netlify beat a complicated AWS setup.

## The short version

| | Cloudflare static hosting | S3 + CloudFront |
|---|---|---|
| Verdict | My default for websites | S3 my default for storage, CloudFront use when it fits |
| Deploy | Push to main, live worldwide | Bucket, origin access control, cache policies, certificate, invalidations |
| Static requests | Free and unlimited | $0.0125 per 10,000 HTTPS requests after 10M free (pay-as-you-go) |
| Egress to Australia | $0 | First 1 TB free, then $0.114/GB |
| Flat-rate option | Not needed | Pro $15 (10M requests, 50 TB), Business $200 (125M requests, 50 TB) |
| Certificate | None to assemble | ACM in us-east-1, even for a Sydney app |
| Limits per site | 25 MiB per file; 20,000 files Free, 100,000 Paid | Large public files at volume: egress dominates |
| Heavy media | R2 | R2, rather than inside a flat-rate plan |
| Private AWS origins | Not its job: a Worker in front of AWS | Origin access control, VPC origins |
| Paid or private files | A Worker signs the link to R2 | Signed URLs or signed cookies |
| DNS | Cloudflare DNS, proxied | Route 53 alias at the apex, free queries |
| IaC | `@pulumi/cloudflare`, Wrangler does the upload | `@pulumi/aws`, plus aws-native for the flat-rate plan |

Price only decides this at volume. On pay-as-you-go, 20 TB a month from Sydney is about $2,158 on S3 and CloudFront against about $44 on R2: that's prohibitive. Below that, what decides it is assembly and where the client says the site must live.

## Why Cloudflare static hosting is my default

Push to main, and it's live worldwide. I've had nothing to fix. There's no bucket, certificate or cache invalidation to assemble, and a launch, a viral post or a bot crawl costs nothing, because static requests aren't metered.

"Static" goes further than people think. A React or Vue SPA, a PWA, a docs site and a marketing site are all files. Put the API somewhere else, a Lambda function URL in Sydney or a Worker on the same domain, and the front end never needs a server. When some pages need rendering on request, I'd reach for [Astro on Workers](https://cloudlesslabs.com/stack/astro-on-workers/index.md) for a content site and [Next.js on Workers](https://cloudlesslabs.com/stack/nextjs-on-workers/index.md) (OpenNext) for a Next.js app. Both keep the static files free.

For a new site I'd start on Workers with static assets, not Pages: in April 2025 Cloudflare said new projects should start on Workers, and Pages gets no new investment. Working Pages projects can stay where they are.

## When S3 and CloudFront win

[CloudFront](https://cloudlesslabs.com/stack/aws-cloudfront/index.md) is the front door of an app that lives in AWS. It reaches private buckets through origin access control and internal load balancers through VPC origins, and it signs URLs for paid content. Transfer from S3 to CloudFront is free, so files served from [S3](https://cloudlesslabs.com/stack/aws-s3/index.md) go through CloudFront, never straight out of the bucket.

My complaint was always egress. The flat-rate plans changed that: Business is $200 a month per distribution for 125M requests and 50 TB, with the WAF and DDoS protection included and no overage charge. For a client who wants one AWS bill, that's now a fair deal. Two catches: sustained use above the allowance gets your traffic served from fewer, more distant edge locations, and real-time logs, staging distributions and multi-tenant distributions keep you on pay-as-you-go.

This site is one of those: it runs on S3 and CloudFront in our own AWS account. It shows what "more assembly" means: an S3 origin behind origin access control, a certificate in us-east-1, 403 and 404 mapped to a real 404 page (S3 answers 403 for a missing file behind origin access control), DNS records in a Route 53 zone in a shared account, and a GitHub Actions deploy that exchanges an OIDC token for a role allowed to sync one bucket and invalidate one distribution. All of it in Pulumi. On Cloudflare, most of that list doesn't exist.

## R2 or S3 for the files

| | Cloudflare R2 | Amazon S3 (Sydney) |
|---|---|---|
| Storage | $0.015/GB-month | $0.025/GB-month (Standard) |
| Writes | $4.50 per million | $5.50 per million PUT/LIST |
| Reads | $0.36 per million | $0.44 per million GET |
| Internet egress | $0 | $0.114/GB (first 10 TB) |
| 20 TB served a month, 2 TB stored | About $44 | About $2,158 with CloudFront, about $228 on the Business flat-rate plan |
| Versioning, replication, Object Lock | No | Yes |
| Archive classes | Infrequent Access only | Standard-IA, Glacier Instant Retrieval, Intelligent-Tiering |
| Access control | API tokens scoped to buckets | IAM roles, bucket policies |
| Australian residency | No: a best-effort Oceania hint | Yes: the region is a guarantee |
| Read by a Worker | A binding, no keys | Signed requests |

[R2](https://cloudlesslabs.com/stack/cloudflare-r2/index.md) is for public files at volume: video, downloads, installers, model weights, exports that outside parties pull. It doesn't replace S3, and I wouldn't try. Data lakes stay in S3: Athena and Glue expect it, and compute in a private subnet reaches R2 through a NAT gateway, so every byte written to R2 is AWS egress.

The pattern is side by side. S3 in Sydney stays the system of record: private, versioned, in the lake. R2 holds the public delivery copy on a custom domain behind Cloudflare's cache, filled from S3 by Sippy as requests arrive. Turn off the r2.dev address as soon as the custom domain is live: it's rate-limited, has no cache, and leaves the files public around your WAF rules.

## Vercel and Netlify: skip, unless nobody technical runs it

[Vercel](https://cloudlesslabs.com/stack/vercel/index.md) and [Netlify](https://cloudlesslabs.com/stack/netlify/index.md) are excellent. Their real product is one workflow: push, preview, ship, roll back, on one dashboard. My stack has no such thing; I wire previews, rollbacks and environments together with Pulumi and GitHub Actions, and that takes real engineering. I skip them because my clients are enterprises: most of their work is in the backend, and I need all of AWS behind it. If you aren't technical, pick one of them over a complicated AWS setup.

## Traps

- **One zone, one provider.** Cloudflare's proxy with Route 53 kept authoritative is only on Business and Enterprise; on Free and Pro, Cloudflare must hold the nameservers. Delegate subdomains with NS records rather than splitting records by hand ([DNS](https://cloudlesslabs.com/stack/dns/index.md)).
- **Previews are public.** Cloudflare Pages preview deployments have guessable URLs by default. Turn on the access policy, or Cloudflare Access on the preview hostnames.
- **SPA deep links.** On Cloudflare, set `not_found_handling = "single-page-application"`. On CloudFront with origin access control, map 403 and 404 to `/index.html` with a 200 for an SPA, or grant `s3:ListBucket` so a normal site returns real 404s.
- **Invalidations.** The first 1,000 paths a month are free, then $0.005 per path. Hash asset names and invalidate only the HTML, or send one `/*`.
- **Moving DNS with DNSSEC on** breaks the domain. Remove the DS record at the registrar first, wait, switch nameservers, then re-enable it.

Sources, checked on 25 October 2026: [Workers static assets: billing and limitations](https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/), [frontend, backend and database in one Worker (Cloudflare, April 2025)](https://blog.cloudflare.com/full-stack-development-on-cloudflare-workers/), [single-page application routing](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/), [Pages limits](https://developers.cloudflare.com/pages/platform/limits/), [CloudFront flat-rate plans](https://aws.amazon.com/cloudfront/pricing/), [CloudFront pay-as-you-go pricing](https://aws.amazon.com/cloudfront/pricing/pay-as-you-go/), [flat-rate allowances and unsupported features](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/flat-rate-pricing-plan.html), [S3 pricing](https://aws.amazon.com/s3/pricing/), [R2 pricing](https://developers.cloudflare.com/r2/pricing/), [R2 limits](https://developers.cloudflare.com/r2/platform/limits/), [R2 data location](https://developers.cloudflare.com/r2/reference/data-location/), [OpenNext Cloudflare adapter](https://opennext.js.org/cloudflare), [Astro Cloudflare adapter](https://docs.astro.build/en/guides/integrations-guide/cloudflare/), [Route 53 pricing](https://aws.amazon.com/route53/pricing/), [Cloudflare DNS proxy status](https://developers.cloudflare.com/dns/proxy-status/).

## Related

- [Cloudflare static hosting](https://cloudlesslabs.com/stack/cloudflare-static-hosting/index.md): the full entry (verdict, pricing, gotchas)
- [Amazon CloudFront](https://cloudlesslabs.com/stack/aws-cloudfront/index.md): the full entry (verdict, pricing, gotchas)
- [Amazon S3](https://cloudlesslabs.com/stack/aws-s3/index.md): the full entry (verdict, pricing, gotchas)
- [Cloudflare R2](https://cloudlesslabs.com/stack/cloudflare-r2/index.md): the full entry (verdict, pricing, gotchas)
- [Astro on Cloudflare Workers](https://cloudlesslabs.com/stack/astro-on-workers/index.md): the full entry (verdict, pricing, gotchas)
- [Next.js on Cloudflare Workers (OpenNext)](https://cloudlesslabs.com/stack/nextjs-on-workers/index.md): the full entry (verdict, pricing, gotchas)
- [Vercel](https://cloudlesslabs.com/stack/vercel/index.md): the full entry (verdict, pricing, gotchas)
- [Netlify](https://cloudlesslabs.com/stack/netlify/index.md): the full entry (verdict, pricing, gotchas)
- [Route 53 and Cloudflare DNS](https://cloudlesslabs.com/stack/dns/index.md): the full entry (verdict, pricing, gotchas)
- [Every choice between tools](https://cloudlesslabs.com/stack/choices/index.md)
- [Map of every Markdown file on cloudlesslabs.com, with what each one answers](https://ask.cloudlesslabs.com)
