Uploads and delivery
Serve public and private objects
Choose public buckets, custom domains, Worker authorization, cache policy, and download headers for the required audience.
8 minute lesson
Before serving anything from R2, answer one question per object: may anyone on the internet read this?
Public objects: custom domain
Public assets can use a custom domain attached to the bucket. Cloudflare serves them directly, no Worker code involved, and they cache well because anyone may read them:
npx wrangler r2 bucket domain add my-app-files \
--domain files.example.com --zone-id 023e105f4ecef8ad9ca31a8372d0c353
Now https://files.example.com/logos/header.svg serves the object at key logos/header.svg. Because the domain runs through Cloudflare, cache rules and Cache-Control metadata on the object apply, so a popular image is served from the edge instead of hitting the bucket every time.
There’s also an r2.dev development URL you can enable per bucket. It’s rate-limited and meant for testing, not production traffic.
Private objects: a Worker in front
Private user files should stay behind a Worker or a time-bounded presigned URL that checks authorization first. The Worker pattern:
const allowed = await userMayRead(session, exportId)
if (!allowed) return new Response('Forbidden', { status: 403 })
const object = await env.FILES.get(`exports/${exportId}.csv`)
if (!object) return new Response('Not found', { status: 404 })
const headers = new Headers()
object.writeHttpMetadata(headers)
headers.set('Content-Disposition', 'attachment; filename="report-july.csv"')
headers.set('Cache-Control', 'private, no-store')
return new Response(object.body, { headers })
Set content type and Content-Disposition deliberately. attachment forces a download with a filename you choose; inline renders in the browser. Without either, browsers guess, and they don’t all guess the same way.
Configure CORS only for the browser origins and methods that need it. A wildcard origin on a bucket that serves anything private is a standing invitation.
Keys leak
Public URL structure must not reveal a secret, because object keys leak through logs, browser history, and referrer headers. A URL like files.example.com/exports/acme-corp-revenue-2026.csv on a public bucket is one forwarded link away from being an incident. If the audience is “this user only”, the object belongs behind the Worker check, not behind an unguessable name.
Serve one public image and one private export, then verify all three behaviors: an anonymous request gets the image but a 403 on the export, the image response carries your cache headers, and the export downloads with the filename you set.
Lesson completed