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 with boto3 for 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.
  • 403 on the public URL. The custom domain is not connected, or the object key is wrong.
  • A presigned PUT created with ContentType fails. The client must send a matching Content-Type header.
  • boto3 cannot resolve the endpoint. endpoint_url lacks https://, 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.