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 awscli or brew 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.

  1. In the R2 section, click “Manage R2 API Tokens,” then “Create API token.”
  2. Set the token name, select “Object Read and Write” for the permissions, and scope it to the specific bucket.
  3. Click “Create API Token.”
  4. 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.

  1. In the bucket settings, click “Settings,” then “Public Access.”
  2. Under “Custom Domains,” click “Connect Domain” and enter assets.yourdomain.com.
  3. 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: json

Create ~/.aws/config entry for the endpoint:

[profile r2]
region = auto
endpoint_url = https://<account-id>.r2.cloudflarestorage.com

5. 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 r2

6. 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 r2

Common errors

  • InvalidAccessKeyId. The Access Key ID is from the main Cloudflare API token, not the R2 token. Generate a dedicated R2 API token.
  • 403 Forbidden on public URL. The bucket’s public access is disabled. Check bucket Settings, Public Access.
  • AWS CLI uses the wrong region. Set region = auto in the profile; R2 ignores the region string but the CLI requires one.
  • boto3 EndpointResolutionError. The endpoint_url is missing the https:// 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.