Overview
Cloudflare R2 is S3-compatible object storage with no egress fees. This guide creates a bucket, generates R2 API credentials, sets a custom public domain, and verifies access with both the AWS CLI and boto3. The full R2 configuration reference is in cloudflare-r2.
Prerequisites
- A Cloudflare account with R2 enabled. R2 requires a payment method on file but has a generous free tier (10 GB storage, 1 million Class A operations per month).
- The AWS CLI installed:
pip install awscliorbrew install awscli. The CLI is used only for the S3-compatible endpoint; no AWS account is needed. - Python 3.11+ if you want to use boto3.
- A domain on Cloudflare for the public-access subdomain. See cloudflare for the DNS configuration context.
Steps
1. Create the R2 bucket
In the Cloudflare dashboard, open R2, then click “Create bucket.”
- Bucket name:
my-assets(lowercase, hyphens allowed, no underscores). - Location: Automatic, or pick a region hint closest to your users.
Click “Create bucket.” Bucket names are globally unique per account, not globally unique across all of R2.
2. Generate R2 API keys
R2 uses a separate API token, not the main Cloudflare API key.
- In the R2 section, click “Manage R2 API Tokens,” then “Create API token.”
- Set the token name, select “Object Read and Write” for the permissions, and scope it to the specific bucket.
- Click “Create API Token.”
- Copy the Access Key ID and Secret Access Key. You cannot view the secret again after closing the dialog.
Store them in environment variables:
export R2_ACCESS_KEY_ID="your-access-key-id"
export R2_SECRET_ACCESS_KEY="your-secret-access-key"
export R2_ENDPOINT="https://<account-id>.r2.cloudflarestorage.com"The account ID appears in the R2 dashboard URL: https://dash.cloudflare.com/<account-id>/r2.
3. Attach a public domain
Public buckets let you serve files directly from a URL without signing requests.
- In the bucket settings, click “Settings,” then “Public Access.”
- Under “Custom Domains,” click “Connect Domain” and enter
assets.yourdomain.com. - Cloudflare adds a CNAME record automatically if the domain is on Cloudflare DNS.
After activation, objects at https://assets.yourdomain.com/<key> are publicly readable.
To allow list access for non-public use, skip this step and use pre-signed URLs via the SDK.
4. Configure the AWS CLI profile
aws configure --profile r2
# AWS Access Key ID: <R2_ACCESS_KEY_ID>
# AWS Secret Access Key: <R2_SECRET_ACCESS_KEY>
# Default region name: auto
# Default output format: jsonCreate ~/.aws/config entry for the endpoint:
[profile r2]
region = auto
endpoint_url = https://<account-id>.r2.cloudflarestorage.com5. Access with the AWS CLI
# List buckets.
aws s3 ls --profile r2
# Upload a file.
aws s3 cp ./image.png s3://my-assets/images/image.png --profile r2
# List objects in the bucket.
aws s3 ls s3://my-assets/ --profile r2
# Download an object.
aws s3 cp s3://my-assets/images/image.png ./downloaded.png --profile r26. Access with boto3
import boto3, os
r2 = boto3.client(
"s3",
endpoint_url=os.environ["R2_ENDPOINT"],
aws_access_key_id=os.environ["R2_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["R2_SECRET_ACCESS_KEY"],
region_name="auto",
)
# Upload.
r2.upload_file("image.png", "my-assets", "images/image.png")
# Generate a pre-signed URL (1-hour expiry).
url = r2.generate_presigned_url(
"get_object",
Params={"Bucket": "my-assets", "Key": "images/image.png"},
ExpiresIn=3600,
)
print(url)See python for the async variant using aiobotocore.
Verify it worked
# 1. CLI can list objects.
aws s3 ls s3://my-assets/ --profile r2
# 2. Upload a test file.
echo "hello r2" > test.txt
aws s3 cp test.txt s3://my-assets/test.txt --profile r2
# 3. Confirm it is publicly readable via the custom domain.
curl -sI https://assets.yourdomain.com/test.txt | head -1
# expected: HTTP/2 200
# 4. Content matches.
curl -s https://assets.yourdomain.com/test.txt
# expected: hello r2Common errors
InvalidAccessKeyId. The Access Key ID is from the main Cloudflare API token, not the R2 token. Generate a dedicated R2 API token.403 Forbiddenon public URL. The bucket’s public access is disabled. Check bucket Settings, Public Access.- AWS CLI uses the wrong region. Set
region = autoin the profile; R2 ignores the region string but the CLI requires one. - boto3
EndpointResolutionError. Theendpoint_urlis missing thehttps://prefix or the account ID is wrong. - Custom domain not active. DNS propagation takes up to five minutes. Check with
dig CNAME assets.yourdomain.com.