Skip to content

boto3

Last reviewed: 15 days ago

You must generate an Access Key before getting started. All examples will utilize access_key_id and access_key_secret variables which represent the Access Key ID and Secret Access Key values you generated.


Configure boto3 to use your R2 endpoint:

Python
import boto3
s3 = boto3.client(
service_name="s3",
endpoint_url="https://<ACCOUNT_ID>.r2.cloudflarestorage.com",
aws_access_key_id="<ACCESS_KEY_ID>",
aws_secret_access_key="<SECRET_ACCESS_KEY>",
region_name="auto",
)

You can omit aws_access_key_id and aws_secret_access_key if you set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables.

Common operations using the client:

Python
# Get object metadata
s3.head_object(Bucket="my-bucket", Key="dog.png")
# Get object
response = s3.get_object(Bucket="my-bucket", Key="dog.png")
# Upload single file
with open("./dog.png", "rb") as f:
s3.upload_fileobj(f, "my-bucket", "dog.png")
# Delete object
s3.delete_object(Bucket="my-bucket", Key="dog.png")

Optimizing upload performance

For large objects (multi-GB files such as training data or video), upload_fileobj can become a throughput bottleneck. Its internal thread pool is limited by Python's GIL, and increasing max_concurrency via TransferConfig gives diminishing returns beyond ~10 threads.

Use the low-level multipart API with ThreadPoolExecutor instead:

Python
import boto3
import math
import os
from concurrent.futures import ThreadPoolExecutor
s3 = boto3.client(
service_name="s3",
endpoint_url="https://<ACCOUNT_ID>.r2.cloudflarestorage.com",
aws_access_key_id="<ACCESS_KEY_ID>",
aws_secret_access_key="<SECRET_ACCESS_KEY>",
region_name="auto",
)
bucket = "my-bucket"
key = "large-file.bin"
file_path = "./large-file.bin"
part_size = 16 * 1024 * 1024 # 16 MiB per part
max_workers = 10
# Step 1: Create the multipart upload
upload_id = None
mpu = s3.create_multipart_upload(Bucket=bucket, Key=key)
upload_id = mpu["UploadId"]
def upload_part(part_number, data):
response = s3.upload_part(
Bucket=bucket,
Key=key,
UploadId=upload_id,
PartNumber=part_number,
Body=data,
)
return {"PartNumber": part_number, "ETag": response["ETag"]}
try:
file_size = os.path.getsize(file_path)
part_count = math.ceil(file_size / part_size)
# Step 2: Upload parts in parallel
with ThreadPoolExecutor(max_workers=max_workers) as pool:
futures = []
with open(file_path, "rb") as f:
for i in range(part_count):
data = f.read(part_size)
futures.append(pool.submit(upload_part, i + 1, data))
parts = [future.result() for future in futures]
# Step 3: Complete the upload
s3.complete_multipart_upload(
Bucket=bucket,
Key=key,
UploadId=upload_id,
MultipartUpload={"Parts": parts},
)
print("Multipart upload complete.")
except Exception:
if upload_id:
try:
s3.abort_multipart_upload(Bucket=bucket, Key=key, UploadId=upload_id)
except Exception:
pass
raise

For more on multipart uploads including part size limits and lifecycle management, refer to Upload objects.

Generate presigned URLs

Generate presigned links to share temporary public read or write access to a bucket.

Python
# Generate presigned URL for reading (GET)
get_url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": "my-bucket", "Key": "dog.png"},
ExpiresIn=3600, # Valid for 1 hour
)
# Generate presigned URL for writing (PUT)
put_url = s3.generate_presigned_url(
"put_object",
Params={
"Bucket": "my-bucket",
"Key": "dog.png",
"ContentType": "image/png",
},
ExpiresIn=3600,
)
https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/dog.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=<signature>
https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/dog.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Signature=<signature>

Upload using the presigned PUT URL. When using a presigned URL with ContentType, the client must include a matching Content-Type header:

Terminal window
curl -X PUT "https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/dog.png?X-Amz-Algorithm=..." \
-H "Content-Type: image/png" \
--data-binary @dog.png

Restrict uploads with CORS and Content-Type

When generating presigned URLs for uploads, you can limit abuse and misuse by:

  1. Restricting Content-Type: Specify the allowed content type in the presigned URL parameters. The upload will fail if the client sends a different Content-Type header.

  2. Configuring CORS: Set up CORS rules on your bucket to control which origins can upload files. Configure CORS via the Cloudflare dashboard by adding a JSON policy to your bucket settings:

[
{
"AllowedOrigins": ["https://example.com"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["Content-Type"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]

Then generate a presigned URL with a Content-Type restriction:

Python
put_url = s3.generate_presigned_url(
"put_object",
Params={
"Bucket": "my-bucket",
"Key": "dog.png",
"ContentType": "image/png",
},
ExpiresIn=3600,
)

When a client uses this presigned URL, they must:

  • Make the request from an allowed origin (enforced by CORS)
  • Include the Content-Type: image/png header (enforced by the signature)