---
title: boto3
description: Configure Python boto3 to work with Cloudflare R2 via the S3-compatible API.
image: https://developers.cloudflare.com/dev-products-preview.png
---

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/r2/llms.txt  
> Use this file to discover all available pages before exploring further. 

[Skip to content](#%5Ftop) 

# boto3

You must [generate an Access Key](https://developers.cloudflare.com/r2/api/tokens/) 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 ↗](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) 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 ↗](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#using-environment-variables).

Common operations using the client:

Python

```
# Get object metadatas3.head_object(Bucket="my-bucket", Key="dog.png")
# Get objectresponse = s3.get_object(Bucket="my-bucket", Key="dog.png")
# Upload single filewith open("./dog.png", "rb") as f:    s3.upload_fileobj(f, "my-bucket", "dog.png")
# Delete objects3.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 ↗](https://en.wikipedia.org/wiki/Global%5Finterpreter%5Flock), and increasing `max_concurrency` via `TransferConfig` gives diminishing returns beyond \~10 threads.

Use the low-level multipart API with `ThreadPoolExecutor` instead:

Python

```
import boto3import mathimport osfrom 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 partmax_workers = 10
# Step 1: Create the multipart uploadupload_id = Nonempu = 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](https://developers.cloudflare.com/r2/objects/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](https://developers.cloudflare.com/r2/buckets/cors/#add-cors-policies-from-the-dashboard) on your bucket to control which origins can upload files. Configure CORS via the [Cloudflare dashboard ↗](https://dash.cloudflare.com/?to=/:account/r2/overview) 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)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/r2/examples/aws/boto3/#page","headline":"boto3 · Cloudflare R2 docs","description":"Configure Python boto3 to work with Cloudflare R2 via the S3-compatible API.","url":"https://developers.cloudflare.com/r2/examples/aws/boto3/","inLanguage":"en","image":"https://developers.cloudflare.com/dev-products-preview.png","dateModified":"2026-06-08","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/r2/","name":"R2"}},{"@type":"ListItem","position":3,"item":{"@id":"/r2/examples/","name":"Examples"}},{"@type":"ListItem","position":4,"item":{"@id":"/r2/examples/aws/","name":"S3 SDKs"}},{"@type":"ListItem","position":5,"item":{"@id":"/r2/examples/aws/boto3/","name":"boto3"}}]}
```
