Sponsor this newsletter to reach 10,000+ readers →
Most developers create a new Web API and immediately start writing controllers, entities, and endpoints. I got an email from Hostinger last month. 97% of my monthly bandwidth used; 5 days left in the billing cycle. Upgrade or get throttled.
For my PakExam SaaS, I’m running a Hostinger KVM2:
2 vCPU cores
8 GB RAM
100 GB NVMe disk space
8 TB bandwidth
PakExam is built for students preparing for exams, and every time they download a past paper or a resource, that’s bandwidth. Last month I nearly ran out of the 8 TB, and the “solution” Hostinger pushes on you is simple: upgrade to KVM4.
I’d already been thinking about moving off Hostinger after 6 months there. Nothing wrong with them, but for a SaaS that hasn’t even turned subscriptions on yet, I don’t want to lock myself into a bigger, costlier plan just to fix one thing. KVM2 cost me $101.88 the first year and renews at $200 for the second. I didn’t check that going in. Lesson learned: always check the renewal price, not just the first-year offer, because by the time it kicks in you’re already dependent on the setup and switching feels like a bigger job than it should.
So before paying for a bigger VPS, I wanted to know what was actually eating the bandwidth.
I was running MinIO, the open-source S3-compatible storage server, deployed directly on my VPS. Every past paper PDF, every image, every audio (my dictionary feature provides English-to-Urdu translation) file the app served was going through that same box and the same bandwidth allowance as everything else.
Storage traffic was the biggest chunk of it. Students downloading papers all day, every day, all counted against the same 8 TB as my API and my site.
Once I saw that, the fix wasn’t “buy a bigger server.” It was “stop making my server serve files.”
I’d heard about R2 before but never had a real reason to move. Cloudflare’s free tier for R2 is generous:
10 GB-month of standard storage
1 million Class A operations (writes, lists)
10 million Class B operations (reads)
Completely free egress, no matter how much data goes out
That last point is the whole reason this made sense for me. Egress is exactly what was costing me bandwidth on Hostinger, and R2 doesn’t charge for it at all.
The other advantage: MinIO and R2 are both S3-compatible. My whole setup was already speaking that protocol, so this wasn’t a rewrite; it was a swap.
Here’s how I moved everything over:
Installed rclone on the server. Since both MinIO and R2 speak S3, this one tool could talk to both.
Grabbed my R2 credentials from the Cloudflare dashboard: an Access Key ID and Secret Access Key scoped to Object Read & Write.
Created matching buckets on R2 first, same names as my MinIO buckets, so nothing in my app code would need to change beyond the endpoint.
Set up two rclone remotes, one pointing at MinIO (source) and one at R2 (destination), using the same S3 provider type for both.
Ran a dry run on each bucket before touching anything real, just to see file counts and sizes and catch surprises early.
Copied bucket by bucket with rclone, checking progress live as it transferred.
Verified everything with
rclone checkon each bucket to confirm the file counts and checksums matched between MinIO and R2.
Once every bucket checked out clean, I swapped my app’s storage config over to R2 and kept MinIO around for a few days as a safety net before shutting it down for good.
The config side got a new section for R2 alongside the existing MinIO one, with a switch to pick which provider is active:
Under the hood, I built a new storage service for R2 and wired it into the same storage abstraction the rest of the app already uses. Since every part of the codebase that reads or writes a file goes through that interface, not a concrete MinIO class, none of those call sites needed to change. Only the DI registration and the new service itself did.
That new service and the DI setup around it deserves its own walkthrough, so I’ll cover the actual implementation in a separate article.
This isn’t the first infrastructure decision I’ve made for PakExam as a solo dev. I wrote a bit ago about why I went with a monorepo for the whole project; similar reasoning there: fewer moving parts, less to manage alone.
MinIO on my own VPS meant every file download counted against my server’s bandwidth allowance.
R2’s free egress moved that cost to $0, without giving up S3 compatibility.
rclone made the actual data migration a copy-and-verify job.
A new storage service implementing the existing abstraction meant switching providers was a config change plus one new service, not a rewrite of the app.
I work with founders and growing businesses to design, build, and improve reliable web apps. Let's talk →
Promote yourself to 10,000+ subscribers by sponsoring this newsletter →
Boost your .NET skills by subscribing to my YouTube Channel →

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.