Offload WordPress media to Cloudflare R2 without breaking URLs
A WordPress media library grows in one direction. Every image becomes several resized copies, every year adds a few gigabytes, and eventually the site is paying application-server prices to store static files that would be nearly free anywhere else.
Offloading moves those files to object storage and serves them from there. Done carefully it is invisible. Done carelessly it is the reason someone's images 404 two years later.
Why R2 specifically
Object storage bills you for three things: storage, operations and egress. For a media library, egress is the one that surprises people โ every visitor who loads a page pulls bytes out of the bucket, and on most providers those bytes cost money.
Cloudflare R2 charges nothing for egress. That single difference turns a media-heavy site from an unpredictable bill into a flat storage cost. R2 also speaks the S3 API, so anything that works with S3 works with R2.
If you already have S3, DigitalOcean Spaces, Backblaze B2, Wasabi or a self-hosted MinIO, all of them work the same way. The reasoning below applies to every one of them.
What usually goes wrong
The plugin rewrites your database. Some offload tools do a
search-and-replace across wp_posts to point URLs at the bucket. It
works, until you remove the plugin or change buckets and discover your
content is now full of URLs to somewhere that no longer exists.
The move is one-way. If the tool has no "bring it back" path, you have handed your library to a vendor and a bucket. Cancel either and you have a recovery project.
Nothing verifies the copy. Upload, then delete local, and hope. A truncated upload is a permanently lost original.
The sequence that is actually safe
A move should be four steps, in this order:
- Copy. Upload to the bucket. Nothing local is touched, so an interrupted run changes nothing.
- Verify. Checksum the remote object against the local file. If they differ, stop โ the original is still there.
- Release. Only after a byte-for-byte match, remove the local copy and record the new location.
- Reverse, on demand. Bring files back to the server whenever you want, byte-identical.
MediaDock Pro implements exactly this, per folder or for the whole library, and it never rewrites your database. URLs are swapped while the page renders, from a cached map, which is why removing the plugin cannot leave broken links: the content in your database always pointed at your own site.
Setting it up
The short version:
- Create an R2 bucket in the Cloudflare dashboard.
- Create an API token scoped to that bucket with Object Read & Write.
- Connect a custom domain (or enable the r2.dev subdomain) so files are publicly reachable.
- In WordPress, add the storage location, paste the endpoint and bucket,
and put the keys in
wp-config.phprather than the database. - Test the connection, then move one folder before you move everything.
The full walkthrough with the exact constants lives in the offload documentation.
What to measure afterwards
- Disk usage on the server should drop by roughly the size of what you moved. If it does not, files were copied but never released.
- Page render time should not change. Rewriting URLs from a cached map costs nothing; in our benchmark a page with 20 offloaded images renders in 47ms end to end, adding zero database queries.
- Your bucket's egress, on any provider that charges for it. This is the number that decides whether R2 was the right call.
Do you actually need this?
If your library is a few gigabytes and your host gives you plenty of disk, offloading buys you very little. It starts paying when storage is a real line item, when backups have become slow enough to skip, or when you want media served from somewhere closer to your visitors than a single origin server.
Try it on your own library
MediaDock adds folders to the WordPress Media Library, and with Pro it moves your files to Cloudflare R2 or any S3-compatible storage. Free version on WordPress.org.