Overview
R2 is S3-compatible object storage with no egress fees. This guide creates a bucket, issues credentials limited to it, exposes it on a custom domain, and checks access with the AWS CLI and boto3. Pricing, multipart uploads, lifecycle rules, and presigned URLs are covered in cloudflare-r2.
Prerequisites
- A Cloudflare account. The free allowance is 10 GB-month of Standard storage, 1 million Class A operations, and 10 million Class B operations per month; egress is free.
- The AWS CLI (
brew install awscli, or the AWS installer) and Python 3.10 or newer withboto3for the SDK path. - For a public custom domain, a zone on Cloudflare DNS.
Steps
1. Create the bucket
Dashboard: R2 object storage, Create bucket. Names are lowercase letters, digits, and hyphens. From the CLI: npx wrangler r2 bucket create my-assets.
2. Create a scoped API token
In R2, under API Tokens, choose Manage, Create API token. Pick Object Read & Write and restrict it to the one bucket; reserve Admin permissions for bucket management. The Access Key ID and Secret Access Key are shown once, so store them immediately.
export R2_ACCESS_KEY_ID=... R2_SECRET_ACCESS_KEY=...
export R2_ENDPOINT="https://<account-id>.r2.cloudflarestorage.com"The account ID is in the dashboard URL and on the R2 overview page.
3. Serve publicly from a custom domain
Bucket, Settings, Custom Domains, Add, enter assets.yourdomain.com, and Connect Domain. Cloudflare creates the DNS record when the zone is on Cloudflare. Use a custom domain for production: the r2.dev address is rate-limited and meant for development, and caching, WAF rules, and bot management work only on custom domains. Do not point a CNAME at r2.dev. If the bucket should stay private, skip this step and use presigned URLs.
4. Use the AWS CLI
aws configure --profile r2 # region: auto; output: json
aws s3 cp ./image.png s3://my-assets/images/image.png --profile r2 --endpoint-url "$R2_ENDPOINT"
aws s3 ls s3://my-assets/ --profile r2 --endpoint-url "$R2_ENDPOINT"Every command needs the account endpoint. The region value auto is required by the SDK but unused by R2.
5. Use boto3
import os, boto3
r2 = boto3.client("s3", endpoint_url=os.environ["R2_ENDPOINT"], region_name="auto",
aws_access_key_id=os.environ["R2_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["R2_SECRET_ACCESS_KEY"])
r2.upload_file("image.png", "my-assets", "images/image.png")
print(r2.generate_presigned_url("get_object", Params={"Bucket": "my-assets", "Key": "images/image.png"}, ExpiresIn=3600))Verify it worked
echo "hello r2" > test.txt && aws s3 cp test.txt s3://my-assets/test.txt --profile r2 --endpoint-url "$R2_ENDPOINT"
curl -s https://assets.yourdomain.com/test.txt # hello r2 (custom domain step only)Common errors
InvalidAccessKeyId. The keys belong to a Cloudflare API token, not an R2 token. Create an R2 token.403on the public URL. The custom domain is not connected, or the object key is wrong.- A presigned
PUTcreated withContentTypefails. The client must send a matchingContent-Typeheader. - boto3 cannot resolve the endpoint.
endpoint_urllackshttps://, or the account ID is wrong. - The domain stays inactive. Wait for DNS and check the record with
dig CNAME assets.yourdomain.com.
FAQ
How much does Cloudflare R2 cost to start?
The free allowance is 10 GB-month of Standard storage, 1 million Class A operations, and 10 million Class B operations per month, and egress is free.
Can I serve production files from the r2.dev URL?
No; r2.dev is rate-limited and meant for development. Connect a custom domain, which is also required for caching, WAF rules, and bot management.
What region do I set for R2 in boto3 or the AWS CLI?
Set the region to auto, which the SDK requires and R2 ignores, and pass the account endpoint https://<account-id>.r2.cloudflarestorage.com on every call.
Why does R2 return InvalidAccessKeyId?
The keys belong to a Cloudflare API token instead of an R2 token; create an R2 token with Object Read & Write scoped to the bucket.