When building a blog, the first question is where to host it. Most Next.js blogs end up on Vercel — one git push deploys it and preview environments come for free. It's frictionless.
But this blog compiles to fully static files at build time (output: 'export'). No server-side rendering, no edge functions. So is there a reason to pay for a PaaS abstraction I don't use? For a static site, running it yourself on AWS S3 + CloudFront is
- cheap — under $1/mo for a small blog
- fast — cache-served from CloudFront's global edge
- less lock-in — drops the Vercel dependency for direct control over Lambda@Edge, security headers, and cache policy (though it does add AWS dependencies: CloudFront, OAC, Route 53)
This post is the record of that decision — and a direct, modeled answer to the common worry: "won't AWS end up costing more once traffic grows?"
The architecture at a glance
First, how the pieces fit together.
| Component | Role |
|---|---|
| S3 | Static asset storage, public access blocked, OAC-only |
| CloudFront | HTTPS, compression, security headers, global CDN |
| ACM | TLS cert (must live in us-east-1 for CloudFront) |
| Route 53 | DNS, apex (root domain) alias |
| GitHub Actions | Keyless deploys via OIDC |
A visitor request flows DNS → CloudFront edge → OAC-signed → private S3. Only dynamic bits like view counts are split off to a separate Cloudflare Worker. (Workers/KV free limits are 100k req/day and 1k KV writes/day — fine for a small blog, but if you POST a view count per PV, watch the limits/cost at high traffic.)
On a cache hit, assets serve from the AWS edge (cache misses / HTML revalidation reach the S3 origin); only the dynamic view count lives on a Cloudflare Worker + KV — the S3 origin is never exposed directly.
Three decisions are central to the design:
- S3 is never made public. Block Public Access stays on, and the bucket policy only allows requests signed by CloudFront's OAC (Origin Access Control) via SigV4. Even if the bucket URL leaks, it can't be hit directly.
- The TLS cert (ACM) lives in us-east-1. CloudFront is a global service that only reads certs from N. Virginia. The site itself can live in any region.
- The apex domain uses an alias record. A root domain like
develicit.comcan't use a CNAME, so a Route 53 alias (A/AAAA) points straight at the CloudFront distribution.
Deploy: one git push
Static infra doesn't mean manual deploys. A push to main makes GitHub Actions mint short-lived credentials via OIDC, then build → sync to S3 → invalidate CloudFront in one shot.
Triggers the GitHub Actions deploy workflow
GitHub OIDC token → STS AssumeRoleWithWebIdentity → short-lived creds
keyless (no secrets)pnpm install --frozen-lockfile → next export (out/) + Pagefind index
_next/ immutable (1-yr cache), the rest max-age=0 + --delete
two-tier cache policycreate-invalidation --paths "/*" refreshes the edge cache
No long-lived access keys live in GitHub — OIDC mints short-lived STS credentials per run to assume the deploy role.
Two things matter here.
- Keyless deploys. No long-lived AWS access keys sit in GitHub Secrets. Instead, GitHub's OIDC token calls
AssumeRoleWithWebIdentityto obtain expiring, per-run credentials. The leak risk simply disappears. - Two-tier cache policy. Hash-stamped
_next/assets are cachedimmutablefor a year; HTML and the rest usemax-age=0so they revalidate every time. New posts show up instantly while static assets live as long as possible at the edge.
Cost analysis
Fixed monthly cost (low traffic)
For a small personal blog (~10k PV/month ≈ ~5 GB transfer), CloudFront transfer/requests and S3 requests mostly fall inside the always-free tier (CloudFront's 1 TB transfer + 10M requests per month). So the real fixed cost collapses to essentially one line item.
- CloudFront: ~$0 (within free tier)
- S3: ~$0.05 (storage + a few requests)
- Route 53: $0.50 (hosted zone, fixed regardless of traffic)
- ACM: free
About $0.5–1/mo — basically just the Route 53 hosted zone. Against Vercel Pro ($20), that's roughly a 20× gap. (For a personal/non-commercial blog, though, Vercel Hobby is free and includes 100 GB/mo transfer, so the $20 comparison only applies when you actually need Pro.)
About an 18× gap for a low-traffic personal blog. What happens as traffic grows is worked out concretely in Fig 4 below.
Does the break-even flip as traffic grows?
People assume "if traffic spikes, AWS eventually costs more," but for a static site there's effectively no dollar break-even. Two reasons:
- Both include 1 TB/mo egress free. CloudFront is free up to 1 TB/month; Vercel Pro includes 1 TB too. At
0.5 MB average transfer per pageview, 1 TB is about 2M pageviews/month. Up to that point AWS stays essentially flat ($1) while Vercel is flat at $20. - AWS's overage rate is also lower. Past the free tier, CloudFront is $0.085/GB (US/EU) vs Vercel's ~$0.15/GB. So even at high traffic the marginal rate keeps AWS ahead — it never flips.
Free-tier regime — up to ~2M PV/month
Both CloudFront and Vercel include 1 TB egress. Here AWS is essentially flat no matter the traffic.
Beyond the free tier — egress rate comparison
AWS's overage rate is lower ($0.085/GB < $0.15/GB) → it doesn't flip even at high traffic.
For a static site, AWS is cheaper across the board. The $20/mo doesn't buy a lower bill — it buys DX: preview deploys, build minutes, image optimization.
AWS does get expensive fast in two cases — (1) costly edge regions like India/South America/some of APAC (~$0.11–0.12/GB vs $0.085 for US/EU), (2) multi-MB page payloads that spike per-PV transfer. Lowering per-PV egress via caching and image optimization IS cost control.
Assumptions: 0.5 MB average egress per PV, CloudFront US/EU $0.085/GB (1 TB/mo free), Vercel Pro 1 TB included then $0.15/GB, Route 53 $0.5 fixed. Only transfer is modeled; at very high PV, request charges add up (CloudFront beyond 10M requests, Vercel Edge Requests $0.002/1K). Real cost varies with page weight, region, and cache hit ratio.
So for a static blog, $20/mo doesn't buy a lower bill — it buys DX: preview deploys, build minutes, image optimization. The real break-even is "how much of your own time will you spend." The two exceptions where AWS gets expensive fast: (1) costly edge regions like India/South America, and (2) multi-MB page payloads that spike per-PV transfer — so keeping per-PV egress low via caching and image optimization is cost control.
Trade-offs
There's no free lunch, of course. With self-hosting you pay the cost in effort, not dollars.
- More upfront setup. Instead of wiring S3 · CloudFront · OAC · ACM · Route 53 by hand, I codified it in Terraform — but that's a learning curve of its own.
- Preview environments are DIY. There's no per-PR preview URL like Vercel's out of the box; you build your own bucket/distribution if you want one.
- Image optimization is on you. Instead of
next/image's runtime optimization, you do it at build time or bolt on CloudFront + Lambda@Edge yourself.
In return, once it's built, maintenance is near zero. The infra is frozen as Terraform code, so reproducibility is 100% — and I can see exactly how every part works.
Wrapping up
To sum up: when you have a static site · need Vercel Pro (the free Hobby tier isn't enough) · mostly US/EU traffic · and the willingness to operate it yourself, AWS self-hosting wins on cost and speed. Conversely, if the free Hobby tier is enough, or you need SSR/ISR, don't want to spend time on infra, or rely on team-wide preview collaboration, Vercel is a perfectly reasonable call.
I'm in the camp of "I want to understand, line by line, how my blog is served" — so I picked AWS. That the choice never leaves me regretting it on the monthly bill is exactly why I wrote this up.