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.
Create a bucket
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.
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.
Related posts about cloudflare: