R2 foundations
Choose Workers API or S3 API
Use a binding inside Workers and reserve S3-compatible credentials for external tools and clients that need them.
There are two ways to talk to an R2 bucket. From inside a Worker, you use a binding. From anywhere else, you use the S3-compatible API. Pick based on where the code runs.
The binding, inside Workers
A binding like env.FILES gives your Worker direct access to the bucket. No credentials in your code, no HTTP client to configure, no signing requests. Cloudflare wires the bucket to the Worker at deploy time.
Use a Worker binding when the request already runs on Workers:
const object = await env.FILES.get('reports/july.pdf')
if (!object) return new Response('Not found', { status: 404 })
return new Response(object.body)
Use the S3-compatible API for external tools and existing SDKs. Keep both credential paths server-side. Test a missing key and a large object so the implementation proves it streams rather than buffering the whole file.
That’s the whole read path. Three lines, and object.body is a stream, so a 2 GB file flows through without loading into memory.
The S3 API, from outside
R2 speaks the S3 protocol at https://<ACCOUNT_ID>.r2.cloudflarestorage.com. Any tool built for S3 works against it with two changes: the endpoint and the credentials.
You create the credentials in the dashboard, under R2 and then Manage API Tokens. Each token gives you an Access Key ID and a Secret Access Key, and you can scope it to Object Read only, or to specific buckets.
With those, the AWS CLI lists your bucket like any S3 bucket:
aws s3 ls s3://my-app-files/reports/ --endpoint-url https://0f3b2a1c9d8e7f6a5b4c3d2e1f0a9b8c.r2.cloudflarestorage.com
# 2026-08-03 10:14:22 482113 july.pdf
This is the path for backup tools, migration scripts, a CI job that uploads build artifacts, or a server running somewhere else that already uses an S3 SDK.
Where the credentials live
The binding has no secret to leak. The S3 API does. Those keys must stay on servers you control, never in browser JavaScript or a mobile app.
When a browser needs to upload or download directly, don’t hand it the keys. Have your server create a presigned URL, a link that carries a signature and an expiry and allows one bounded operation on one key. The browser uses the link. It never sees the credentials.
Rotate S3 tokens like any password, and give each tool its own token. When you retire a tool, revoke its token and nothing else breaks.
Try this: list which parts of a file service run inside Workers and which external tool genuinely needs S3 compatibility. Most applications end up with the binding for everything and one S3 token for backups.
Lesson completed