| name | cloudflare-r2 |
| description | Manage Cloudflare R2 buckets, lifecycle, and signed URLs. Use for low-egress object storage and media delivery. |
| license | MIT |
| metadata | {"author":"devops-skills","version":"1.0"} |
Cloudflare R2
S3-compatible object storage with zero egress fees, built on Cloudflare's global network.
When to Use
- Storing user uploads, media files, backups, or static assets.
- Replacing AWS S3 to eliminate egress costs for read-heavy workloads.
- Serving files at the edge via Workers or public bucket access.
- Building multi-cloud storage that avoids vendor lock-in (S3 API compatible).
- Storing ML model artifacts, training data, or inference results.
Prerequisites
- Cloudflare account with R2 enabled (dashboard > R2 > subscribe).
- Wrangler CLI v3+ installed:
npm install -g wrangler.
- Authenticated via
wrangler login or CLOUDFLARE_API_TOKEN.
- For S3 API access: R2 API token created under R2 > Manage R2 API Tokens.
Bucket Management with Wrangler
Create and List Buckets
npx wrangler r2 bucket create app-assets
npx wrangler r2 bucket create eu-uploads --location=eu
npx wrangler r2 bucket list
npx wrangler r2 bucket delete old-bucket
Object Operations
npx wrangler r2 object put app-assets/images/logo.png --file=./logo.png
npx wrangler r2 object put app-assets/data/report.json \
--file=./report.json \
--content-type="application/json"
npx wrangler r2 object get app-assets/images/logo.png --file=./downloaded-logo.png
npx wrangler r2 object delete app-assets/images/old-logo.png
npx wrangler r2 object head app-assets/images/logo.png
S3-Compatible API Access
R2 supports the S3 API, so existing tools (AWS CLI, boto3, s3cmd) work out of the box.
Generate R2 API Tokens
- Go to R2 > Manage R2 API Tokens > Create API token.
- Select permissions: Object Read & Write, or Object Read only.
- Scope to specific buckets if possible.
- Save the Access Key ID and Secret Access Key.
AWS CLI Configuration
aws configure --profile r2
export R2_ENDPOINT="https://<ACCOUNT_ID>.r2.cloudflarestorage.com"
aws s3 ls --endpoint-url=$R2_ENDPOINT --profile=r2
aws s3 sync ./dist s3://app-assets/static/ \
--endpoint-url=$R2_ENDPOINT \
--profile=r2
aws s3 cp ./backup.tar.gz s3://app-assets/backups/backup-$(date +%Y%m%d).tar.gz \
--endpoint-url=$R2_ENDPOINT \
--profile=r2
aws s3 ls s3://app-assets/images/ \
--endpoint-url=$R2_ENDPOINT \
--profile=r2
aws s3 rm s3://app-assets/tmp/ --recursive \
--endpoint-url=$R2_ENDPOINT \
--profile=r2
Python boto3 Client
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://<ACCOUNT_ID>.r2.cloudflarestorage.com",
aws_access_key_id="<R2_ACCESS_KEY>",
aws_secret_access_key="<R2_SECRET_KEY>",
region_name="auto",
)
s3.upload_file("./report.pdf", "app-assets", "reports/report.pdf")
url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": "app-assets", "Key": "reports/report.pdf"},
ExpiresIn=3600,
)
print(url)
response = s3.list_objects_v2(Bucket="app-assets", Prefix="images/", MaxKeys=100)
for obj in response.get("Contents", []):
print(f"{obj['Key']} - {obj['Size']} bytes")
Worker Bindings
Bind R2 buckets to Workers or Pages Functions for server-side access without API tokens.
Wrangler Configuration
name = "asset-worker"
main = "src/index.ts"
compatibility_date = "2024-09-01"
[[r2_buckets]]
binding = "ASSETS"
bucket_name = "app-assets"
[[r2_buckets]]
binding = "UPLOADS"
bucket_name = "user-uploads"
Worker with R2 Operations
interface Env {
ASSETS: R2Bucket;
UPLOADS: R2Bucket;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (request.method === "GET") {
const key = url.pathname.slice(1);
const object = await env.ASSETS.get(key);
if (!object) {
return new Response("Not Found", { status: 404 });
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("etag", object.httpEtag);
headers.set("cache-control", );
(., { headers });
}
(request. === ) {
key = url..();
contentType = request..() || ;
env..(key, request., {
: { contentType },
: { : ().() },
});
(.({ key, : }), {
: { : },
});
}
(request. === ) {
key = url..();
env..(key);
(, { : });
}
(, { : });
},
};
Presigned URL Generation in a Worker
import { AwsClient } from "aws4fetch";
interface Env {
R2_ACCESS_KEY: string;
R2_SECRET_KEY: string;
R2_ACCOUNT_ID: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const aws = new AwsClient({
accessKeyId: env.R2_ACCESS_KEY,
secretAccessKey: env.R2_SECRET_KEY,
});
const url = new URL(request.url);
const key = url.searchParams.get("key");
if (!key) return new Response("Missing key", { status: 400 });
const r2Url = `https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/app-assets/${key}`;
const signed = await aws.sign( (r2Url), {
: { : },
});
.({ : signed. });
},
};
Public Bucket Access
Enable public access to serve files directly without a Worker.
- Go to R2 > bucket > Settings > Public access.
- Enable and set a custom domain (e.g.,
assets.example.com).
- Objects are accessible at
https://assets.example.com/<key>.
Lifecycle Rules
Configure automatic object expiration or transition.
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/r2/buckets/app-assets/lifecycle" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"rules": [
{
"id": "expire-tmp-files",
"enabled": true,
"conditions": { "prefix": "tmp/" },
"actions": { "deleteObject": { "daysAfterCreationDate": 7 } }
},
{
"id": "expire-old-logs",
"enabled": true,
"conditions": { "prefix": "logs/" },
"actions": { "deleteObject": { "daysAfterCreationDate": 90 } }
}
]
}'
CORS Configuration
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/r2/buckets/app-assets/cors" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"corsRules": [
{
"allowedOrigins": ["https://example.com"],
"allowedMethods": ["GET", "PUT", "HEAD"],
"allowedHeaders": ["Content-Type", "Authorization"],
"maxAgeSeconds": 3600
}
]
}'
Troubleshooting
| Symptom | Cause | Fix |
|---|
NoSuchBucket error via S3 API | Wrong endpoint or bucket name | Verify endpoint is https://<ACCOUNT_ID>.r2.cloudflarestorage.com |
SignatureDoesNotMatch | Incorrect secret key or endpoint mismatch | Regenerate R2 API token; ensure region is auto |
| Uploads succeed but GET returns 404 | Key path mismatch (leading slash) | R2 keys should not start with / |
| Slow uploads for large files | Single-stream upload | Use multipart upload; set --expected-size with wrangler |
| CORS errors in browser | Missing CORS config on bucket | Add CORS rules for your origin domain |
Worker binding returns undefined | wrangler.toml binding name mismatch | Verify binding name matches Env interface property |
| Public access returns 403 | Public access not enabled | Enable in bucket Settings > Public access |
Related Skills