Amazon S3 taught the whole internet one model of file storage: object storage with buckets, where you put everything from product photos to database backups. It also taught us something less pleasant: fees for every gigabyte of data leaving the cloud, the so-called egress. Cloudflare R2 is a direct answer to this model: S3-compatible storage in which outbound transfer costs exactly zero. In this article we check how big the difference is in practice, how to get started with R2 and how to use it, among other things, to offload WordPress media.
Cloudflare R2: object storage with no transfer fees
10.09.2026 | Author: Marcin Wiercioch
Egress: the silent budget killer
Let’s work through a concrete example. A website with a photo gallery or a store with rich product pages serves 1 TB of images a month. In classic S3, outbound transfer to the internet alone costs around 90 USD a month, even though storing those files costs pennies. At 10 TB this becomes several hundred dollars, and a popular video file can generate a bill nobody dreamt of with a single social media post.
In R2 this line item simply does not exist. You pay for storage (0.015 USD per GB per month) and for operations (fractions of a dollar per million), while data going out to the internet, whether 1 GB or 100 TB, is free. On top of that there is a generous free plan: 10 GB of storage and millions of operations a month, which for smaller projects means a cost of zero.
S3 compatibility: migration without rewriting code
R2 exposes an S3-compatible API, so the same libraries (aws-sdk, boto3), the same tools (rclone, Cyberduck) and the same plugins work with it. Migration usually comes down to changing the endpoint and the access keys. Cloudflare also offers Sippy, which migrates data from S3 “lazily”: an object is copied to R2 on the first request, so the move happens without a maintenance window.
Getting started: a bucket and access from a Worker
We create a bucket in the Cloudflare dashboard or with Wrangler:
npx wrangler r2 bucket create media-okinet
The most pleasant way to work with R2 is a binding in a Worker: no keys, no request signing, just an object in env:
{
"r2_buckets": [
{ "binding": "MEDIA", "bucket_name": "media-okinet" }
]
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
if (request.method === "PUT") {
await env.MEDIA.put(key, request.body, {
httpMetadata: { contentType: request.headers.get("content-type") }
});
return new Response("Saved: " + key, { status: 201 });
}
const object = await env.MEDIA.get(key);
if (!object) {
return new Response("Not found", { status: 404 });
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("cache-control", "public, max-age=31536000, immutable");
return new Response(object.body, { headers: headers });
}
};
A dozen or so lines and we have our own global file server, with full control over permissions, cache headers and access logic (for example authorisation before a file is served).
Public file hosting with your own domain
For simpler uses a Worker is not necessary: a bucket can be connected directly to your own subdomain (for example cdn.yourdomain.com) in a few clicks. Files are then served through the Cloudflare network with full edge caching: once an object has been fetched to a point of presence, it is served locally to the next users. For static assets (fonts, scripts, images, video) this is practically a ready-made CDN with no extra configuration.
Backups with rclone
Because R2 speaks the S3 dialect, it works great with rclone. Copying a directory from the server to a bucket is a single command that can be put in cron:
rclone sync /var/www/strona/backups r2:backupy-okinet/strona --transfers 8 --fast-list
Storing 50 GB of backups in R2 costs about 75 cents a month, and restoring data (that is, a large outbound transfer!) costs nothing, exactly the opposite of services where getting your own data back can be the most expensive operation on the price list.
R2 and WordPress: offloading media
A typical WordPress installation keeps all images in the uploads directory on the same server as PHP and the database. With higher traffic, this wastes resources: the application server spends its time serving static files instead of generating pages, and the directory keeps growing, making backups and migrations harder. Media offload plugins (such as WP Offload Media or Media Cloud) can automatically send every upload to S3-compatible storage (all you need is the R2 endpoint and keys) and replace the URLs in your content with the bucket’s domain. The result: a lighter server, faster media from a global cache and application backups that are gigabytes smaller. It is one of our favourite quick improvements when providing ongoing support for our clients’ websites.
Things to remember
- R2 has strong consistency for object operations: after a write you immediately read the new version (unlike KV),
- a single object can be up to 5 TB (multipart upload above 5 GB),
- data location can be pinned to a jurisdiction (for example the European Union), which matters for GDPR,
- R2 does not transform images; to generate thumbnails on the fly, add Cloudflare Images or a Worker with an image processing library,
- class A operations (writes, listings) cost more than class B operations (reads), so when designing your key structure, avoid unnecessary listings.
Summary
R2 strips object storage of its most unpredictable cost and adds natural integration with Workers and the Cloudflare CDN. For websites and online stores this means cheap, fast media; for teams, backups that do not hurt to restore; for applications, user files without your own infrastructure. In the next article of the series we switch to analytics and look at PostHog, a tool that shows not only how many people visit a website, but what exactly they do there. Do you have a project where the transfer bill keeps you up at night? Get in touch and we will calculate how much you can save with R2.



