Cloudflare R2: object storage without egress fees
By Flavio Copes
How to store and serve files like images and uploads with Cloudflare R2 from a Worker. S3-compatible storage with no egress fees.
When users upload a photo, or you generate a PDF, you need somewhere to put the file. On Cloudflare, that’s R2.
R2 is object storage. You store files, called objects, in a bucket, and read them back later. If you’ve used Amazon S3, it’s the same idea, and it’s even S3-compatible.
The big difference is the price. With most providers you pay every time someone downloads a file (that’s “egress”). R2 has no egress fees. If you serve a lot of files, that adds up fast.
Let’s use it.
By the way, if you’re unsure whether your data belongs in R2, KV, D1 or Durable Objects, I built a free Cloudflare storage chooser to help you decide.
Create a bucket
We create buckets with Wrangler. Use Wrangler 4, the current major version. npx wrangler runs the copy installed in your project, so if that’s older, upgrade it with npm install -D wrangler@4:
npx wrangler r2 bucket create my-app-uploads
Add it to wrangler.jsonc:
{
"r2_buckets": [
{
"binding": "UPLOADS",
"bucket_name": "my-app-uploads"
}
]
}
Now env.UPLOADS is your bucket in code.
Store a file
You store an object under a key, which is just a path-like string. The value can be text, JSON, or binary data like an uploaded file.
Here we save a file that came in from a form upload:
export default {
async fetch(request, env) {
const body = await request.arrayBuffer()
await env.UPLOADS.put('avatars/user-123.png', body)
return new Response('Saved')
},
}
Read a file back
get returns the object. You can stream it straight into a response:
const object = await env.UPLOADS.get('avatars/user-123.png')
if (!object) {
return new Response('Not found', { status: 404 })
}
return new Response(object.body)
object.body is a stream, so you’re not loading the whole file into memory. It just flows through to the user.
Set the content type
When you serve a file, the browser needs to know what it is. Store some metadata with the object, then use it when you serve:
await env.UPLOADS.put('avatars/user-123.png', body, {
httpMetadata: { contentType: 'image/png' },
})
And on the way out:
const object = await env.UPLOADS.get('avatars/user-123.png')
const headers = new Headers()
object.writeHttpMetadata(headers)
return new Response(object.body, { headers })
writeHttpMetadata copies the content type (and other metadata) onto the response for you.
If you skip this, R2 stores the object without a content type and serves it as application/octet-stream. The symptom is a browser that downloads your PNG instead of displaying it. Store the content type at upload time and this never happens.
Check a bucket from the terminal
You don’t need to write a Worker to poke at a bucket. Wrangler can put and get objects directly:
npx wrangler r2 object put my-app-uploads/test.txt --file ./test.txt --remote
npx wrangler r2 object get my-app-uploads/test.txt --file ./downloaded.txt --remote
The --remote flag targets the real bucket instead of the local simulation wrangler dev uses. This is the quickest way to verify a binding problem is in your code and not in the bucket.
List and delete
List objects, optionally by prefix:
const list = await env.UPLOADS.list({ prefix: 'avatars/' })
list.objects.forEach((o) => console.log(o.key))
And delete one when it’s no longer needed:
await env.UPLOADS.delete('avatars/user-123.png')
A note on serving files
Serving files through a Worker, like above, gives you control. You can check that the user is allowed to see the file before returning it.
If a file is public and you don’t need a check, you can also connect a custom domain to the bucket and let Cloudflare serve it directly. Even faster, even less code.
When to use R2
R2 is for files: user uploads, images, video, generated documents, backups. Anything that isn’t structured rows (D1) or simple key lookups (KV).
The no-egress pricing makes it especially good for things people download a lot. The full reference is in the R2 docs.
Want me to talk about your product? You can sponsor this site.
Related posts about cloudflare: