The Problem: Vercel Blob Is Getting Expensive
Here's the thing about running your own blog—it starts simple, then suddenly you're staring at hosting bills wondering where it all went wrong.
I built injong.com using Next.js on Vercel. For image storage, I went with Vercel Blob because it was right there. Easy integration. One-click setup. No thinking required.
But a few months in, I started noticing something: the free tier was getting tight. More posts meant more images. More images meant more storage. More storage meant... you see where this is going.
So I did what any reasonable person would do: I spent an entire weekend researching alternatives instead of actually writing content.
Enter Cloudflare R2
After falling down a rabbit hole of AWS S3 pricing calculators and Google Cloud documentation that somehow made me more confused, I stumbled upon Cloudflare R2.
The pitch? Zero egress fees.
For the non-technical folks: most cloud storage services charge you when people view your content. It's like paying rent AND paying every time someone looks at your apartment. R2 doesn't do that.
Here's the comparison that sold me:
Provider | Storage Cost | Bandwidth Cost |
|---|---|---|
Vercel Blob | $0.08/GB after 1GB free | $0.15/GB |
Cloudflare R2 | $0.015/GB after 10GB free | FREE |
That's 10x the free storage and literally zero bandwidth costs. For a personal blog with growing traffic, this is kind of a no-brainer.
The Migration Plan (A.K.A. How Not to Break Everything)
Now, switching image storage for a live website isn't like changing your profile picture. Every blog post has image URLs hardcoded in the content. Swap the storage without updating those URLs, and suddenly your beautiful posts are full of broken image icons.
Here's my strategy:
Phase 1: Dual-Mode Architecture
Instead of a hard cutover (which is basically asking for disaster), I'm implementing a toggle system:
javascript
const STORAGE_MODE = process.env.IMAGE_STORAGE_MODE || 'blob';
if (STORAGE_MODE === 'cloudflare') {
return uploadToCloudflare(file);
} else {
return uploadToVercelBlob(file);
}Development stays on Vercel Blob. Production switches to Cloudflare. Simple environment variable flip.
Phase 2: The Actual Migration
This is where it gets tedious but necessary:
Download all images from Vercel Blob
Upload to Cloudflare R2
Update every URL in the database
Update URLs embedded in post content (yes, all of them)
Generate a migration report to verify nothing got lost
Phase 3: Keep the Safety Net
Here's the part my paranoid self appreciates: I'm keeping the original Vercel Blob images for 30 days after migration. If something breaks, I can flip back instantly. Peace of mind is worth a few extra dollars.
The Bonus: LCP Optimization
While I'm in there ripping things apart, might as well optimize for performance.
LCP (Largest Contentful Paint) is that metric Google uses to judge how fast your page "feels" to users. For blogs, it's usually your main post image.
Cloudflare gives me two things here:
Global CDN — Images served from edge servers worldwide, not from a single datacenter
Automatic format conversion — Request a JPEG, get WebP or AVIF if your browser supports it
Combined with a preload hint in the HTML:
html
<link rel="preload" as="image" href="https://images.injong.com/post-image.jpg" />This should noticeably speed up page loads. Will I actually notice? Probably not. Will Lighthouse give me better scores? Definitely. And honestly, that's what matters for my dopamine.
Lessons So Far
Infrastructure decisions compound. What seems like "just use the default option" at the start becomes "why is this costing me $50/month" later. Worth thinking about early.
Zero egress is a game-changer. For content-heavy sites, bandwidth costs can eclipse storage costs quickly. Cloudflare understood the assignment.
Migration is mostly boring work. 90% of this project is writing scripts to find-and-replace URLs. Not glamorous, but necessary.
What's Next
I'm currently in Phase 1—setting up the R2 bucket and configuring the custom domain (images.injong.com, because aesthetics matter even for CDN URLs).
Once that's stable, I'll run the migration script during off-peak hours and pray to the demo gods that nothing explodes.
Will update with results. Or with a postmortem. Either makes for good content.
If you're running a Next.js blog and considering the same move, feel free to reach out. Misery loves company, and so does infrastructure troubleshooting.

Leave a comment