There's nothing wrong with S3 and CloudFront. They're the workhorses of the internet. But "store an image, serve it fast" turns into a surprising amount of setup, and — more quietly — an ongoing egress bill that scales with how often your images are viewed. If images are a core part of what you ship, that math matters.
What "just use S3" actually involves
The one-liner hides the parts list. To go from zero to a public, CDN-backed image URL you typically wire up:
- An S3 bucket plus a public-access configuration (bucket policy, or an Origin Access Control so only CloudFront can read it).
- A CloudFront distribution with an origin, cache behaviors, and a deploy wait of several minutes before it's live.
- An IAM user or role with scoped permissions and access keys, so your app can upload.
- Your own upload path and validation — content-type checks, size limits, SVG safety checks — because S3 will happily store whatever you send it.
- Cache invalidations when you replace an image, which are themselves metered past the free monthly allotment.
Compare that with the purpose-built path: register, then upload.
# (Omits the OAC, cache-behavior, and IAM-policy JSON.)
aws s3 mb s3://my-images
# (New buckets block all public access by default — you must
# disable that first, or use an Origin Access Control instead.)
aws s3api put-bucket-policy --bucket my-images \
--policy file://public-read.json
aws cloudfront create-distribution \
--origin-domain-name my-images.s3.amazonaws.com
# ...wait ~15 min for the distribution to deploy...
# Then, for every upload:
aws s3 cp chart.png s3://my-images/chart.png
# → https://d111111abcdef8.cloudfront.net/chart.png
# (+ egress metered per GB past CloudFront's free 1 TB/mo)
Side by side
The egress trap
This is the part that doesn't show up in a proof-of-concept. CloudFront includes 1 TB of data-out free every month — generous for a small site, and invisible at first. Past that, it charges roughly $0.085 per GB in North America and Europe (varying by region and volume), plus per-request fees. Then a popular page or a hotlinked asset ships thousands of GB beyond the allowance and the line item is suddenly real. Because you're billed on views, your cost scales with your success.
PixelVault stores on Cloudflare R2, which has no egress fees — you pay for storage and operations, not for people looking at your images. For view-heavy workloads that single difference often dominates the whole comparison.
When S3 + CloudFront is still the right call
Plenty of times, honestly. Reach for the DIY stack when:
- You're already all-in on AWS and want everything under one bill and one IAM boundary.
- You need edge compute — Lambda@Edge or CloudFront Functions for on-the-fly transforms, signed cookies, header rewrites, or WAF integration.
- You depend on deep S3 features: storage-class lifecycle tiers, cross-region replication, event triggers, or Athena/analytics over your objects.
- You're at a scale with committed-use or private pricing that changes the egress math in your favor.
If that's you, the control is worth the setup. Don't switch just to save a few config steps.
When a hosting API wins
For a large middle of projects, the tradeoff runs the other way:
- You just need upload → URL, not a storage platform to administer.
- Your images are viewed a lot, so metered egress is the silent bill you'd rather not have.
- You're a small team or a side project and don't want to babysit buckets, policies, and distributions.
- Agents or automation upload images on their own — a single Bearer-auth endpoint (or an MCP tool) beats provisioning IAM.
- You'd rather have a fixed, always-free allotment than a usage-metered bill to keep an eye on.
Free to start
PixelVault's free tier includes 200 MB storage, 500 uploads/month, and 1 GB bandwidth — no credit card, no trial clock, zero egress. Enough to replace an S3 + CloudFront setup for a lot of projects before you pay anything. Paid plans start at $9/month.