R2
- Demos and architectures
- How R2 works
- Cloudflare R2
- Pricing
- Videos
- Error codes
- API
- Authentication
- S3 API compatibility
- Extensions
- S3
- Presigned URLs
- Temporary credentials
- Workers API
- Workers API reference
- Use R2 from Workers
- Use the R2 multipart API from Workers
- Bucket locks
- Configure CORS
- Create new buckets
- Data Access Logs
- Delete buckets
- Event notifications
- Buckets
- Local uploads
- Object lifecycles
- Public buckets
- Storage classes
- Data migration
- Migration Strategies
- Sippy
- Super Slurper
- Authenticate against R2 API using auth tokens
- Authenticate against R2 with temporary credentials
- Use the Cache API
- Examples
- Rclone
- Use SSE-C
- Terraform (AWS)
- Terraform
- aws CLI
- aws-sdk-go
- aws-sdk-java
- aws-sdk-js-v3
- aws-sdk-kotlin
- aws-sdk-net
- aws-sdk-php
- aws-sdk-ruby
- aws-sdk-rust
- aws4fetch
- boto3
- Configure custom headers
- S3 SDKs
- s3mini
- CLI
- Get started
- S3
- Workers API
- Delete objects
- Download objects
- Objects
- Upload objects
- Audit Logs
- Event subscriptions
- Limits
- Metrics and analytics
- Release notes
- Troubleshooting
- Consistency model
- Data location
- Data security
- Durability
- Reference
- Unicode interoperability
- Wrangler commands
- Snowflake
- Protect an R2 Bucket with Cloudflare Access
- Tutorials
- Mastodon
- Postman
- Use event notification to summarize PDF files on upload
- Log and store upload events in R2 with event notifications
Demos and architectures
import { GlossaryTooltip, ResourcesBySelector } from "~/components";
Learn how you can use R2 within your existing application and architecture.
Reference architectures
Explore the following reference architectures that use R2:
<ResourcesBySelector types={[ "reference-architecture", "design-guide", "reference-architecture-diagram", ]} products={["r2"]} />
How R2 works
import { Render, LinkCard } from "~/components";
Cloudflare R2 is an S3-compatible object storage service with no egress fees, built on Cloudflare's global network. It is strongly consistent and designed for high data durability.
R2 is ideal for storing and serving unstructured data that needs to be accessed frequently over the internet, without incurring egress fees. It's a good fit for workloads like serving web assets, training AI models, and managing user-generated content.
Architecture
R2's architecture is composed of multiple components:
-
R2 Gateway: The entry point for all API requests that handles authentication and routing logic. This service is deployed across Cloudflare's global network via Cloudflare Workers.
-
Metadata Service: A distributed layer built on Durable Objects used to store and manage object metadata (e.g. object key, checksum) to ensure strong consistency of the object across the storage system. It includes a built-in cache layer to speed up access to metadata.
-
Tiered Read Cache: A caching layer that sits in front of the Distributed Storage Infrastructure that speeds up object reads by using Cloudflare Tiered Cache to serve data closer to the client.
-
Distributed Storage Infrastructure: The underlying infrastructure that persistently stores encrypted object data.

R2 supports multiple client interfaces including Cloudflare Workers Binding, S3-compatible API, and a REST API that powers the Cloudflare Dashboard and Wrangler CLI. All requests are routed through the R2 Gateway, which coordinates with the Metadata Service and Distributed Storage Infrastructure to retrieve the object data.
Write data to R2
When a write request (e.g. uploading an object) is made to R2, the following sequence occurs:
-
Request handling: The request is received by the R2 Gateway at the edge, close to the user, where it is authenticated.
-
Encryption and routing: The Gateway reaches out to the Metadata Service to retrieve the encryption key and determines which storage cluster to write the encrypted data to within the location set for the bucket.
-
Writing to storage: The encrypted data is written and stored in the distributed storage infrastructure, and replicated within the region (e.g. ENAM) for durability.
-
Metadata commit: Finally, the Metadata Service commits the object's metadata, making it visible in subsequent reads. Only after this commit is an
HTTP 200success response sent to the client, preventing unacknowledged writes.

Read data from R2
When a read request (e.g. fetching an object) is made to R2, the following sequence occurs:
-
Request handling: The request is received by the R2 Gateway at the edge, close to the user, where it is authenticated.
-
Metadata lookup: The Gateway asks the Metadata Service for the object metadata.
-
Reading the object: The Gateway attempts to retrieve the encrypted object from the tiered read cache. If it's not available, it retrieves the object from one of the distributed storage data centers within the region that holds the object data.
-
Serving to client: The object is decrypted and served to the user.

Performance
The performance of your operations can be influenced by factors such as the bucket's geographical location, request origin, and access patterns.
To optimize upload performance for cross-region requests, enable Local Uploads on your bucket.
To optimize read performance, enable Cloudflare Cache when using a custom domain. When caching is enabled, read requests can bypass the R2 Gateway and be served directly from Cloudflare's edge cache, reducing latency. Note that cached data may not reflect the latest version immediately.

Learn more
Cloudflare R2
import { CardGrid, Description, Feature, LinkButton, LinkTitleCard, Plan, RelatedProduct, } from "~/components";
Object storage for all your data.
Cloudflare R2 Storage allows developers to store large amounts of unstructured data without the costly egress bandwidth fees associated with typical cloud storage services.
You can use R2 for multiple scenarios, including but not limited to:
- Storage for cloud-native applications
- Cloud storage for web content
- Storage for podcast episodes
- Data lakes (analytics and big data)
- Cloud storage output for large batch processes, such as machine learning model artifacts or datasets
Features
Location Hints are optional parameters you can provide during bucket creation to indicate the primary geographical location you expect data will be accessed from.
Configure CORS to interact with objects in your bucket and configure policies on your bucket.
Public buckets expose the contents of your R2 bucket directly to the Internet.
Create bucket scoped tokens for granular control over who can access your data.
Related products
A managed Apache Iceberg data catalog built directly into your R2 bucket, so you can turn a bucket into a data warehouse or lakehouse queryable by engines like Spark, Snowflake, and Basin SQL.
A serverless execution environment that allows you to create entirely new applications or augment existing ones without configuring or maintaining infrastructure.
Upload, store, encode, and deliver live and on-demand video with one API, without configuring or maintaining infrastructure.
A suite of products tailored to your image-processing needs.
More resources
Understand pricing for free and paid tier rates.<LinkTitleCard title="Discord" href="https://discord.cloudflare.com" icon="discord"
 Ask questions, show off what you are building, and discuss the platform
with other developers.
Learn about product announcements, new tutorials, and what is new in
Cloudflare Workers.
Pricing
R2 charges based on the total volume of data stored, along with two classes of operations on that data:
- Class A operations which are more expensive and tend to mutate state.
- Class B operations which tend to read existing state.
For the Infrequent Access storage class, data retrieval fees apply. There are no charges for egress bandwidth for any storage class.
All included usage is on a monthly basis.
:::note
To learn about potential cost savings from using R2, refer to the R2 pricing calculator.
:::
R2 pricing
| Standard storage | Infrequent Access storage | |
|---|---|---|
| Storage | $0.015 / GB-month | $0.01 / GB-month |
| Class A Operations | $4.50 / million requests | $9.00 / million requests |
| Class B Operations | $0.36 / million requests | $0.90 / million requests |
| Data Retrieval (processing) | None | $0.01 / GB |
| Egress (data transfer to Internet) | Free [^1] | Free [^1] |
| Buckets per account | First 100,000 included + $0.10 per additional bucket per month | First 100,000 included + $0.10 per additional bucket per month |
The bucket allowance is shared across all storage classes and applies regardless of your Workers plan. Refer to R2 limits for bucket creation limits and how to request a limit increase.
:::caution[Billable unit rounding] Cloudflare rounds up your usage to the next billing unit.
For example:
- If you have performed one million and one operations, you will be billed for two million operations.
- If you have used 1.1 GB-month, you will be billed for 2 GB-month.
- If you have retrieved data (for infrequent access storage) for 1.1 GB, you will be billed for 2 GB. :::
Free tier
You can use the following amount of storage and operations each month for free.
| Free | |
|---|---|
| Storage | 10 GB-month / month |
| Class A Operations | 1 million requests / month |
| Class B Operations | 10 million requests / month |
| Egress (data transfer to Internet) | Free [^1] |
:::caution The free tier only applies to Standard storage, and does not apply to Infrequent Access storage. :::
Storage usage
Storage is billed using gigabyte-month (GB-month) as the billing metric. A GB-month is calculated by averaging the peak storage per day over a billing period (30 days).
For example:
- Storing 1 GB constantly for 30 days will be charged as 1 GB-month.
- Storing 3 GB constantly for 30 days will be charged as 3 GB-month.
- Storing 1 GB for 5 days, then 3 GB for the remaining 25 days will be charged as
1 GB * 5/30 month + 3 GB * 25/30 month = 2.66 GB-month
For objects stored in Infrequent Access storage, you will be charged for the object for the minimum storage duration even if the object was deleted or moved before the duration specified.
Class A operations
Class A Operations include ListBuckets, PutBucket, ListObjects, PutObject, CopyObject, CompleteMultipartUpload, CreateMultipartUpload, LifecycleStorageTierTransition, ListMultipartUploads, UploadPart, UploadPartCopy, ListParts, PutBucketEncryption, PutBucketCors and PutBucketLifecycleConfiguration.
Class B operations
Class B Operations include HeadBucket, HeadObject, GetObject, UsageSummary, GetBucketEncryption, GetBucketLocation, GetBucketCors and GetBucketLifecycleConfiguration.
Free operations
Free operations include DeleteObject, DeleteBucket and AbortMultipartUpload.
Data retrieval
Data retrieval fees apply when you access or retrieve data from the Infrequent Access storage class. This includes any time objects are read or copied.
Minimum storage duration
For objects stored in Infrequent Access storage, you will be charged for the object for the minimum storage duration even if the object was deleted, moved, or replaced before the specified duration.
| Storage class | Minimum storage duration |
|---|---|
| Standard storage | None |
| Infrequent Access storage | 30 days |
Basin Catalog pricing
Basin Catalog charges for catalog operations and compaction data processed, in addition to standard R2 storage and operations. For full details, refer to Basin Catalog pricing.
Data migration pricing
Super Slurper
Super Slurper is free to use. You are only charged for the Class A operations that Super Slurper makes to your R2 bucket. Objects with sizes < 100MiB are uploaded to R2 in a single Class A operation. Larger objects use multipart uploads to increase transfer success rates and will perform multiple Class A operations. Note that your source bucket might incur additional charges as Super Slurper copies objects over to R2.
Once migration completes, you are charged for storage & Class A/B operations as described in previous sections.
Sippy
Sippy is free to use. You are only charged for the operations Sippy makes to your R2 bucket. If a requested object is not present in R2, Sippy will copy it over from your source bucket. Objects with sizes < 200MiB are uploaded to R2 in a single Class A operation. Larger objects use multipart uploads to increase transfer success rates, and will perform multiple Class A operations. Note that your source bucket might incur additional charges as Sippy copies objects over to R2.
As objects are migrated to R2, they are served from R2, and you are charged for storage & Class A/B operations as described in previous sections.
Pricing calculator
To learn about potential cost savings from using R2, refer to the R2 pricing calculator.
R2 billing examples
Standard storage example
If a user writes 1,000 objects in R2 Standard storage for 1 month with an average size of 1 GB and reads each object 1,000 times during the month, the estimated cost for the month would be:
| Usage | Free Tier | Billable Quantity | Price | |
|---|---|---|---|---|
| Storage | (1,000 objects) * (1 GB per object) = 1,000 GB-months | 10 GB-months | 990 GB-months | $14.85 |
| Class A Operations | (1,000 objects) * (1 write per object) = 1,000 writes | 1 million | 0 | $0.00 |
| Class B Operations | (1,000 objects) * (1,000 reads per object) = 1 million reads | 10 million | 0 | $0.00 |
| Data retrieval (processing) | (1,000 objects) * (1 GB per object) = 1,000 GB | NA | None | $0.00 |
| TOTAL | $14.85 |
Infrequent access example
If a user writes 1,000 objects in R2 Infrequent Access storage with an average size of 1 GB, stores them for 5 days, and then deletes them (delete operations are free), and during those 5 days each object is read 1,000 times, the estimated cost for the month would be:
| Usage | Free Tier | Billable Quantity | Price | |
|---|---|---|---|---|
| Storage | (1,000 objects) * (1 GB per object) = 1,000 GB-months | NA | 1,000 GB-months | $10.00 |
| Class A Operations | (1,000 objects) * (1 write per object) = 1,000 writes | NA | 1,000 | $9.00 |
| Class B Operations | (1,000 objects) * (1,000 reads per object) = 1 million reads | NA | 1 million | $0.90 |
| Data retrieval (processing) | (1,000 objects) * (1 GB per object) = 1,000 GB | NA | 1,000 GB | $10.00 |
| TOTAL | $29.90 |
Note that the minimal storage duration for infrequent access storage is 30 days, which means the billable quantity is 1,000 GB-months, rather than 167 GB-months.
Asset hosting
If a user writes 100,000 files with an average size of 100 KB object and reads 10,000,000 objects per day, the estimated cost in a month would be:
| Usage | Free Tier | Billable Quantity | Price | |
|---|---|---|---|---|
| Storage | (100,000 objects) * (100KB per object) | 10 GB-months | 0 GB-months | $0.00 |
| Class A Operations | (100,000 writes) | 1 million | 0 | $0.00 |
| Class B Operations | (10,000,000 reads per day) * (30 days) | 10 million | 290,000,000 | $104.40 |
| TOTAL | $104.40 |
Cloudflare billing policy
To learn more about how usage is billed, refer to Cloudflare Billing Policy.
Frequently asked questions
Will I be charged for failed requests to my R2 bucket?
Requests that fail during authentication or authorization are not charged. This includes:
Some requests that pass authentication and authorization but fail for other reasons are counted and billed according to the operation's class. For example:
- A
PutObjectreturning HTTP 412 (PreconditionFailed) due to a conditional header likeIf-None-Matchcounts as a Class A operation. - A
PutObjectreturning HTTP 400 (BadDigest) due to a mismatchedContent-MD5counts as a Class A operation.
[^1]: Egressing directly from R2, including via the Workers API, S3 API, and r2.dev domains does not incur data transfer (egress) charges and is free. If you connect other metered services to an R2 bucket, you may be charged by those services.
Videos
import { CardGrid, LinkCard } from "~/components";
Error codes
This page documents error codes returned by R2 when using the Workers API or the S3-compatible API, along with recommended fixes to help with troubleshooting.
How errors are returned
For the Workers API, R2 operations throw exceptions that you can catch. The error code is included at the end of the message property:
try {
await env.MY_BUCKET.put("my-key", data, { customMetadata: largeMetadata });
} catch (error) {
console.error(error.message);
// "put: Your metadata headers exceed the maximum allowed metadata size. (10012)"
}
For the S3-compatible API, errors are returned as XML in the response body:
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>NoSuchKey</Code>
<Message>The specified key does not exist.</Message>
</Error>
Error code reference
Authentication and authorization errors
| Error Code | S3 Code | HTTP Status | Details | Recommended Fix |
|---|---|---|---|---|
| 10002 | Unauthorized | 401 | Missing or invalid authentication credentials. | Verify your API token or access key credentials are correct and have not expired. |
| 10003 | AccessDenied | 403 | Insufficient permissions for the requested operation. | Check that your API token has the required permissions for the bucket and operation. |
| 10018 | ExpiredRequest | 403 | Presigned URL or request signature has expired. | Regenerate the presigned URL or signature. |
| 10035 | SignatureDoesNotMatch | 403 | Request signature does not match calculated signature. | Verify your secret key and signing algorithm. Check for URL encoding issues. |
| 10042 | NotEntitled | 403 | Account not entitled to this feature. | Ensure your account has an R2 subscription. |
Bucket errors
| Error Code | S3 Code | HTTP Status | Details | Recommended Fix |
|---|---|---|---|---|
| 10005 | InvalidBucketName | 400 | Bucket name does not meet naming requirements. | Bucket names must be 3-63 chars, lowercase alphanumeric and hyphens, start/end with alphanumeric. |
| 10006 | NoSuchBucket | 404 | The specified bucket does not exist. | Verify the bucket name is correct and the bucket exists in your account. |
| 10008 | BucketNotEmpty | 409 | Cannot delete bucket that contains objects. | Delete all objects in the bucket before deleting the bucket. |
| 10009 | TooManyBuckets | 400 | Account bucket limit exceeded (default: 1,000,000 buckets). | Request a limit increase via the Limits Increase Request Form. |
| 10073 | BucketConflict | 409 | Bucket name already exists. | Choose a different bucket name. Bucket names must be unique within your account. |
Object errors
| Error Code | S3 Code | HTTP Status | Details | Recommended Fix |
|---|---|---|---|---|
| 10007 | NoSuchKey | 404 | The specified object key does not exist. For the Workers API, get() and head() return null instead of throwing. |
Verify the object key is correct and the object has not been deleted. |
| 10020 | InvalidObjectName | 400 | Object key contains invalid characters or is too long. | Use valid UTF-8 characters. Maximum key length is 1024 bytes. |
| 100100 | EntityTooLarge | 400 | Object exceeds maximum size (5 GiB for single upload, 5 TiB for multipart). | Use multipart upload for objects larger than 5 GiB. Maximum object size is 5 TiB. |
| 10012 | MetadataTooLarge | 400 | Custom metadata exceeds the 8,192 byte limit. | Reduce custom metadata size. Maximum is 8,192 bytes total for all custom metadata. |
| 10069 | ObjectLockedByBucketPolicy | 409 | Object is protected by a bucket lock rule and cannot be modified or deleted. If a conditional header on the object being written would also fail, this error is returned instead of PreconditionFailed. When CopyObject copies to a different key, changes the storage class, or uses SSE-C, source conditions (x-amz-copy-source-if-*) are evaluated before the destination lock check and return 412. When an object is copied onto itself, the lock check runs first and returns 409. |
Wait for the retention period to expire. Refer to bucket locks. |
Upload and request errors
| Error Code | S3 Code | HTTP Status | Details | Recommended Fix |
|---|---|---|---|---|
| 10033 | MissingContentLength | 411 | Content-Length header required but missing. |
Include the Content-Length header in PUT/POST requests. |
| 10013 | IncompleteBody | 400 | Request body terminated before expected Content-Length. |
Ensure the full request body is sent. Check for network interruptions or client timeouts. |
| 10014 | InvalidDigest | 400 | Checksum header format is malformed. | Ensure checksums are properly encoded (base64 for SHA/CRC checksums). |
| 10037 | BadDigest | 400 | Provided checksum does not match the uploaded content. | Verify data integrity and retry the upload. |
| 10039 | InvalidRange | 416 | Requested byte range is not satisfiable. | Ensure the range start is less than object size. Check Range header format. |
| 10031 | PreconditionFailed | 412 | Conditional headers (If-Match, If-Unmodified-Since, etc.) were not satisfied. |
Object's ETag or modification time does not match your condition. Refetch and retry. Refer to conditional operations. |
Multipart upload errors
| Error Code | S3 Code | HTTP Status | Details | Recommended Fix |
|---|---|---|---|---|
| 10011 | EntityTooSmall | 400 | Multipart part is below minimum size (5 MiB), except for the last part. | Ensure each part (except the last) is at least 5 MiB. |
| 10024 | NoSuchUpload | 404 | Multipart upload does not exist or was aborted. | Verify the uploadId is correct. By default, incomplete multipart uploads expire after 7 days. Refer to object lifecycles. |
| 10025 | InvalidPart | 400 | One or more parts could not be found when completing the upload. | Verify each part was uploaded successfully and use the exact ETag returned from UploadPart. |
| 10048 | InvalidPart | 400 | All non-trailing parts must have the same size. | Ensure all parts except the last have identical sizes. R2 requires uniform part sizes for multipart uploads. |
Service errors
| Error Code | S3 Code | HTTP Status | Details | Recommended Fix |
|---|---|---|---|---|
| 10001 | InternalError | 500 | An internal error occurred. | Retry the request. If persistent, check Cloudflare Status or contact support. |
| 10043 | ServiceUnavailable | 503 | Service is temporarily unavailable. | Retry with exponential backoff. Check Cloudflare Status. |
| 10054 | ClientDisconnect | 400 | Client disconnected before request completed. | Check network connectivity and retry. |
| 10058 | TooManyRequests | 429 | Rate limit exceeded. Often caused by multiple concurrent requests to the same object key (limit: 1 write/second per key). | Check if multiple clients are accessing the same object key. See R2 limits. |
API
import { DirectoryListing } from "~/components";
R2 provides three API surfaces for interacting with your data:
- Workers API: An in-Worker API accessed by binding an R2 bucket to a Worker. Use the Workers API to read, write, and list objects from within a Worker.
- S3-compatible API: An S3-compatible HTTP API available at
https://<ACCOUNT_ID>.r2.cloudflarestorage.com. Use existing S3 SDKs and tools to interact with R2. - Cloudflare REST API: The
api.cloudflare.comREST API used by the Cloudflare Dashboard and Wrangler CLI. Supports bucket management and object operations. Rate limits apply. Use the S3-compatible API or Workers API for high-throughput workloads.
Authentication
You can generate an API token to serve as the Access Key for usage with existing S3-compatible SDKs or XML APIs.
:::note This page contains instructions on generating API tokens specifically for R2. Note that this is different from generating API tokens for other services, as documented in Create API token. :::
You must purchase R2 before you can generate an API token.
To create an API token:
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Under the Account Details section, select Manage next to API Tokens.
-
Choose to create either:
- Create Account API token - These tokens are tied to the Cloudflare account itself and can be used by any authorized system or user. Refer to Account API tokens for role and permission requirements. These tokens remain valid until manually revoked or until they reach a preset expiration date.
- Create User API token - These tokens are tied to your individual Cloudflare user. They inherit your personal permissions and become inactive if your user is removed from the account.
-
Under Permissions, choose a permission types for your token. Refer to Permissions for information about each option.
-
(Optional) If you select the Object Read and Write or Object Read permissions, you can scope your token to a set of buckets.
-
Select Create Account API token or Create User API token.
After your token has been successfully created, review your Secret Access Key and Access Key ID values. These may often be referred to as Client Secret and Client ID, respectively.
:::caution
You will not be able to access your Secret Access Key again after this step. Copy and record both values to avoid losing them.
:::
You will also need to configure the endpoint in your S3 client to https://<ACCOUNT_ID>.r2.cloudflarestorage.com.
Find your account ID in the Cloudflare dashboard.
Buckets created with jurisdictions must be accessed via jurisdiction-specific endpoints:
- European Union (EU):
https://<ACCOUNT_ID>.eu.r2.cloudflarestorage.com - FedRAMP:
https://<ACCOUNT_ID>.fedramp.r2.cloudflarestorage.com - United States (US):
https://<ACCOUNT_ID>.us.r2.cloudflarestorage.com
:::caution
Jurisdictional buckets can only be accessed via the corresponding jurisdictional endpoint. Most S3 clients will not let you configure multiple endpoints, so you'll generally have to initialize one client per jurisdiction.
:::
Permissions
| Permission | Description |
|---|---|
| Admin Read & Write | Allows the ability to create, list, and delete buckets, edit bucket configuration, read, write, and list objects, and read and write to data catalog tables and associated metadata. |
| Admin Read only | Allows the ability to list buckets and view bucket configuration, read and list objects, and read from the data catalog tables and associated metadata. |
| Object Read & Write | Allows the ability to read, write, and list objects in specific buckets. |
| Object Read only | Allows the ability to read and list objects in specific buckets. |
:::note[Considerations]
- Basin Catalog requires an Admin Read & Write or Admin Read only permission. Read-only catalog operations (such as listing namespaces, loading tables, and querying data) work with Admin Read only, while write operations (such as creating or dropping tables and committing transactions) require Admin Read & Write. For details, refer to Authenticate your Iceberg engine.
- The Object Read & Write and Object Read only permissions are only supported by the S3-compatible API, not the Cloudflare REST API. :::
Create API tokens via API
You can create API tokens via the API and use them to generate corresponding Access Key ID and Secret Access Key values. To get started, refer to Create API tokens via the API. Below are the specifics for R2.
Access Policy
An Access Policy specifies what resources the token can access and the permissions it has.
Resources
There are two relevant resource types for R2: Account and Bucket. For more information on the Account resource type, refer to Account.
Bucket
Include a set of R2 buckets or all buckets in an account.
A specific bucket is represented as:
"com.cloudflare.edge.r2.bucket.<ACCOUNT_ID>_<JURISDICTION>_<BUCKET_NAME>": "*"
ACCOUNT_ID: Refer to Find zone and account IDs.JURISDICTION: The jurisdiction where the R2 bucket lives. For buckets not created in a specific jurisdiction this value will bedefault.BUCKET_NAME: The name of the bucket your Access Policy applies to.
All buckets in an account are represented as:
"com.cloudflare.api.account.<ACCOUNT_ID>": {
"com.cloudflare.edge.r2.bucket.*": "*"
}
ACCOUNT_ID: Refer to Find zone and account IDs.
Permission groups
Determine what permission groups should be applied.
Workers R2 Storage Write
|
Account | Can create, delete, and list buckets, edit bucket configuration, and read, write, and list objects. | ||||||||||||
Workers R2 Storage Read
|
Account | Can list buckets and view bucket configuration, and read and list objects. | ||||||||||||
Workers R2 Storage Bucket Item Write
|
Bucket | Can read, write, and list objects in buckets. | ||||||||||||
Workers R2 Storage Bucket Item Read
|
Bucket | Can read and list objects in buckets. | ||||||||||||
Workers R2 Data Catalog Write
|
Account | Can read from and write to data catalogs. This permission allows access to the Iceberg REST catalog interface. | ||||||||||||
Workers R2 Data Catalog Read
|
Account | Can read from data catalogs. This permission allows read-only access to the Iceberg REST catalog interface. |
Example Access Policy
[
{
"id": "f267e341f3dd4697bd3b9f71dd96247f",
"effect": "allow",
"resources": {
"com.cloudflare.edge.r2.bucket.4793d734c0b8e484dfc37ec392b5fa8a_default_my-bucket": "*",
"com.cloudflare.edge.r2.bucket.4793d734c0b8e484dfc37ec392b5fa8a_eu_my-eu-bucket": "*"
},
"permission_groups": [
{
"id": "6a018a9f2fc74eb6b293b0c548f38b39",
"name": "Workers R2 Storage Bucket Item Read"
}
]
}
]
Get S3 API credentials from an API token
You can get the Access Key ID and Secret Access Key values from the response of the Create Token API:
- Access Key ID: The
idof the API token. - Secret Access Key: The SHA-256 hash of the API token
value.
Refer to Authenticate against R2 API using auth tokens for a tutorial with JavaScript, Python, and Go examples.
Temporary credentials
To issue short-lived, scoped credentials derived from an API token, use temporary credentials. R2 supports generating them via the Temporary Credentials API or locally by signing a JWT with the parent token's secret access key.
S3 API compatibility
import { Details } from "~/components";
R2 implements the S3 API to allow users and their applications to migrate with ease. When comparing to AWS S3, Cloudflare has removed some API operations' features and added others. The S3 API operations are listed below with their current implementation status. Feature implementation is currently in progress. Refer back to this page for updates.
The API is available via the https://<ACCOUNT_ID>.r2.cloudflarestorage.com endpoint. Find your account ID in the Cloudflare dashboard.
How to read this page
This page has two sections: bucket-level operations and object-level operations.
Each section will have two tables: a table of implemented APIs and a table of unimplemented APIs.
Refer the feature column of each table to review which features of an API have been implemented and which have not.
✅ Feature Implemented
🚧 Feature Implemented (Experimental)
❌ Feature Not Implemented
Bucket region
When using the S3 API, the region for an R2 bucket is auto. For compatibility with tools that do not allow you to specify a region, an empty value and us-east-1 will alias to the auto region.
This also applies to the LocationConstraint for the CreateBucket API.
Checksum Types
Checksums have an algorithm and a type. Refer to the table below.
| Checksum Algorithm | FULL_OBJECT |
COMPOSITE |
|---|---|---|
CRC-64/NVME (CRC64NVME) |
✅ | ❌ |
CRC-32 (CRC32) |
❌ | ✅ |
CRC-32C (CRC32C) |
❌ | ✅ |
SHA-1 (SHA1) |
❌ | ✅ |
SHA-256 (SHA256) |
❌ | ✅ |
Bucket-level operations
The following tables are related to bucket-level operations.
Implemented bucket-level operations
Below is a list of implemented bucket-level operations. Refer to the Feature column to review which features have been implemented (✅) and have not been implemented (❌).
| API Name | Feature |
|---|---|
| ✅ ListBuckets | |
| ✅ HeadBucket | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ CreateBucket | ❌ ACL: ❌ x-amz-acl ❌ x-amz-grant-full-control ❌ x-amz-grant-read ❌ x-amz-grant-read-acp ❌ x-amz-grant-write ❌ x-amz-grant-write-acp ❌ Object Locking: ❌ x-amz-bucket-object-lock-enabled ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ DeleteBucket | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ DeleteBucketCors | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ GetBucketCors | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ GetBucketLifecycleConfiguration | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ GetBucketLocation | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ GetBucketEncryption | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ PutBucketCors | ❌ Checksums: ❌ x-amz-sdk-checksum-algorithm ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ PutBucketLifecycleConfiguration | ❌ Checksums: ❌ x-amz-sdk-checksum-algorithm ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
Unimplemented bucket-level operations
| API Name | Feature |
|---|---|
| ❌ GetBucketAccelerateConfiguration | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketAcl | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketAnalyticsConfiguration | ❌ id ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketIntelligentTieringConfiguration | ❌ id |
| ❌ GetBucketInventoryConfiguration | ❌ id ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketLifecycle | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketLogging | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketMetricsConfiguration | ❌ id ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketNotification | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketNotificationConfiguration | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketOwnershipControls | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketPolicy | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketPolicyStatus | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketReplication | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketRequestPayment | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketTagging | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketVersioning | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetBucketWebsite | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetObjectLockConfiguration | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ GetPublicAccessBlock | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ ListBucketAnalyticsConfigurations | ❌ Query Parameters: ❌ continuation-token ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ ListBucketIntelligentTieringConfigurations | ❌ Query Parameters: ❌ continuation-token ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ ListBucketInventoryConfigurations | ❌ Query Parameters: ❌ continuation-token ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ ListBucketMetricsConfigurations | ❌ Query Parameters: ❌ continuation-token ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketAccelerateConfiguration | ❌ Checksums: ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketAcl | ❌ Permissions: ❌ x-amz-grant-full-control ❌ x-amz-grant-read ❌ x-amz-grant-read-acp ❌ x-amz-grant-write ❌ x-amz-grant-write-acp ❌ Checksums: ❌ x-amz-sdk-checksum-algorithm ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketAnalyticsConfiguration | ❌ id ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketEncryption | ❌ Checksums: ❌ x-amz-sdk-checksum-algorithm ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketIntelligentTieringConfiguration | ❌ id ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketInventoryConfiguration | ❌ id ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketLifecycle | ❌ Checksums: ❌ x-amz-sdk-checksum-algorithm ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketLogging | ❌ Checksums: ❌ Content-MD5 ❌ x-amz-sdk-checksum-algorithm ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketMetricsConfiguration | ❌ id ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketNotification | ❌ Checksums: ❌ Content-MD5 ❌ x-amz-sdk-checksum-algorithm ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketNotificationConfiguration | ❌ Validation: ❌ x-amz-skip-destination-validation ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketOwnershipControls | ❌ Checksums: ❌ Content-MD5 ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketPolicy | ❌ Validation: ❌ x-amz-confirm-remove-self-bucket-access ❌ Checksums: ❌ Content-MD5 ❌ x-amz-sdk-checksum-algorithm ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketReplication | ❌ Object Locking: ❌ x-amz-bucket-object-lock-token ❌ Checksums: ❌ Content-MD5 ❌ x-amz-sdk-checksum-algorithm ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketRequestPayment | ❌ Checksums: ❌ Content-MD5 ❌ x-amz-sdk-checksum-algorithm ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketTagging | ❌ Checksums: ❌ Content-MD5 ❌ x-amz-sdk-checksum-algorithm ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketVersioning | ❌ Multi-factor authentication: ❌ x-amz-mfa ❌ Checksums: ❌ Content-MD5 ❌ x-amz-sdk-checksum-algorithm ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutBucketWebsite | ❌ Checksums: ❌ Content-MD5 ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutObjectLockConfiguration | ❌ Object Locking: ❌ x-amz-bucket-object-lock-token ❌ Checksums: ❌ Content-MD5 ❌ Request Payer: ❌ x-amz-request-payer ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ❌ PutPublicAccessBlock | ❌ Checksums: ❌ Content-MD5 ❌ x-amz-sdk-checksum-algorithm ❌ x-amz-checksum-algorithm ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
Object-level operations
The following tables are related to object-level operations.
Implemented object-level operations
Below is a list of implemented object-level operations. Refer to the Feature column to review which features have been implemented (✅) and have not been implemented (❌).
Behaviors & Limitations
UploadPart: Uploading to the same part number replaces the previous part. If a subsequent upload to the same part fails, the original part is lost and must be re-uploaded.
Conditional operations: The conditional headers on PutObject (such as If-Match and If-None-Match) are evaluated atomically against the committed bucket state, as part of the same write that commits the object. For example, when multiple clients concurrently issue PutObject with If-None-Match: * for a previously absent key, at most one write succeeds and the others fail with 412 PreconditionFailed. Refer to Consistency model for details, including how negative reads interact with in-flight writes.
| API Name | Feature |
|---|---|
| ✅ HeadObject | ✅ Conditional Operations: ✅ If-Match ✅ If-Modified-Since ✅ If-None-Match ✅ If-Unmodified-Since ✅ Range: ✅ Range (has no effect in HeadObject) ✅ partNumber ✅ SSE-C: ✅ x-amz-server-side-encryption-customer-algorithm ✅ x-amz-server-side-encryption-customer-key ✅ x-amz-server-side-encryption-customer-key-MD5 ❌ Request Payer: ❌ x-amz-request-payer ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ ListObjects | Query Parameters: ✅ delimiter ✅ encoding-type ✅ marker ✅ max-keys ✅ prefix ❌ Request Payer: ❌ x-amz-request-payer ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ ListObjectsV2 | Query Parameters: ✅ list-type ✅ continuation-token ✅ delimiter ✅ encoding-type ✅ fetch-owner ✅ max-keys ✅ prefix ✅ start-after ❌ Request Payer: ❌ x-amz-request-payer ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ GetObject | ✅ Conditional Operations: ✅ If-Match ✅ If-Modified-Since ✅ If-None-Match ✅ If-Unmodified-Since ✅ Range: ✅ Range ✅ PartNumber ✅ SSE-C: ✅ x-amz-server-side-encryption-customer-algorithm ✅ x-amz-server-side-encryption-customer-key ✅ x-amz-server-side-encryption-customer-key-MD5 ❌ Request Payer: ❌ x-amz-request-payer ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ PutObject | ✅ Conditional Operations: ✅ If-Match ✅ If-Modified-Since ✅ If-None-Match ✅ If-Unmodified-Since ✅ System Metadata: ✅ Content-Type ✅ Cache-Control ✅ Content-Disposition ✅ Content-Encoding ✅ Content-Language ✅ Expires ✅ Content-MD5 ✅ Storage Class: ✅ x-amz-storage-class ✅ STANDARD ✅ STANDARD_IA ❌ Object Lifecycle ❌ Website: ❌ x-amz-website-redirect-location ❌ SSE: ❌ x-amz-server-side-encryption-aws-kms-key-id ❌ x-amz-server-side-encryption ❌ x-amz-server-side-encryption-context ❌ x-amz-server-side-encryption-bucket-key-enabled ✅ SSE-C: ✅ x-amz-server-side-encryption-customer-algorithm ✅ x-amz-server-side-encryption-customer-key ✅ x-amz-server-side-encryption-customer-key-MD5 ❌ Request Payer: ❌ x-amz-request-payer ❌ Tagging: ❌ x-amz-tagging ❌ Object Locking: ❌ x-amz-object-lock-mode ❌ x-amz-object-lock-retain-until-date ❌ x-amz-object-lock-legal-hold ❌ ACL: ❌ x-amz-acl ❌ x-amz-grant-full-control ❌ x-amz-grant-read ❌ x-amz-grant-read-acp ❌ x-amz-grant-write-acp ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ DeleteObject | ❌ Multi-factor authentication: ❌ x-amz-mfa ❌ Object Locking: ❌ x-amz-bypass-governance-retention ❌ Request Payer: ❌ x-amz-request-payer ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ DeleteObjects | ❌ Multi-factor authentication: ❌ x-amz-mfa ❌ Object Locking: ❌ x-amz-bypass-governance-retention ❌ Request Payer: ❌ x-amz-request-payer ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ ListMultipartUploads | ✅ Query Parameters: ✅ delimiter ✅ encoding-type ✅ key-marker ✅️ max-uploads ✅ prefix ✅ upload-id-marker |
| ✅ CreateMultipartUpload | ✅ System Metadata: ✅ Content-Type ✅ Cache-Control ✅ Content-Disposition ✅ Content-Encoding ✅ Content-Language ✅ Expires ✅ Content-MD5 ✅ Storage Class: ✅ x-amz-storage-class ✅ STANDARD ✅ STANDARD_IA ❌ Website: ❌ x-amz-website-redirect-location ❌ SSE: ❌ x-amz-server-side-encryption-aws-kms-key-id ❌ x-amz-server-side-encryption ❌ x-amz-server-side-encryption-context ❌ x-amz-server-side-encryption-bucket-key-enabled ✅ SSE-C: ✅ x-amz-server-side-encryption-customer-algorithm ✅ x-amz-server-side-encryption-customer-key ✅ x-amz-server-side-encryption-customer-key-MD5 ❌ Request Payer: ❌ x-amz-request-payer ❌ Tagging: ❌ x-amz-tagging ❌ Object Locking: ❌ x-amz-object-lock-mode ❌ x-amz-object-lock-retain-until-date ❌ x-amz-object-lock-legal-hold ❌ ACL: ❌ x-amz-acl ❌ x-amz-grant-full-control ❌ x-amz-grant-read ❌ x-amz-grant-read-acp ❌ x-amz-grant-write-acp ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ CompleteMultipartUpload | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner ❌ Request Payer: ❌ x-amz-request-payer |
| ✅ AbortMultipartUpload | ❌ Request Payer: ❌ x-amz-request-payer |
| ✅ CopyObject | ✅ Operation Metadata: ✅ x-amz-metadata-directive ✅ System Metadata: ✅ Content-Type ✅ Cache-Control ✅ Content-Disposition ✅ Content-Encoding ✅ Content-Language ✅ Expires ✅ Conditional Operations: ✅ x-amz-copy-source ✅ x-amz-copy-source-if-match ✅ x-amz-copy-source-if-modified-since ✅ x-amz-copy-source-if-none-match ✅ x-amz-copy-source-if-unmodified-since ✅ Storage Class: ✅ x-amz-storage-class ✅ STANDARD ✅ STANDARD_IA ❌ ACL: ❌ x-amz-acl ❌ x-amz-grant-full-control ❌ x-amz-grant-read ❌ x-amz-grant-read-acp ❌ x-amz-grant-write-acp ❌ Website: ❌ x-amz-website-redirect-location ❌ SSE: ❌ x-amz-server-side-encryption ❌ x-amz-server-side-encryption-aws-kms-key-id ❌ x-amz-server-side-encryption-context ❌ x-amz-server-side-encryption-bucket-key-enabled ✅ SSE-C: ✅ x-amz-server-side-encryption-customer-algorithm ✅ x-amz-server-side-encryption-customer-key ✅ x-amz-server-side-encryption-customer-key-MD5 ✅ x-amz-copy-source-server-side-encryption-customer-algorithm ✅ x-amz-copy-source-server-side-encryption-customer-key ✅ x-amz-copy-source-server-side-encryption-customer-key-MD5 ❌ Request Payer: ❌ x-amz-request-payer ❌ Tagging: ❌ x-amz-tagging ❌ x-amz-tagging-directive ❌ Object Locking: ❌ x-amz-object-lock-mode ❌ x-amz-object-lock-retain-until-date ❌ x-amz-object-lock-legal-hold ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner ❌ x-amz-source-expected-bucket-owner ❌ Checksums: ❌ x-amz-checksum-algorithm |
| ✅ UploadPart | ✅ System Metadata: ✅ Content-MD5 ❌ SSE: ❌ x-amz-server-side-encryption ✅ SSE-C: ✅ x-amz-server-side-encryption-customer-algorithm ✅ x-amz-server-side-encryption-customer-key ✅ x-amz-server-side-encryption-customer-key-MD5 ❌ Request Payer: ❌ x-amz-request-payer ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
| ✅ UploadPartCopy | ✅ Copy Source: ✅ x-amz-copy-source (required) ❌ Conditional Operations: ❌ x-amz-copy-source-if-match ❌ x-amz-copy-source-if-modified-since ❌ x-amz-copy-source-if-none-match ❌ x-amz-copy-source-if-unmodified-since ✅ Range: ✅ x-amz-copy-source-range ✅ SSE-C: ✅ x-amz-server-side-encryption-customer-algorithm ✅ x-amz-server-side-encryption-customer-key ✅ x-amz-server-side-encryption-customer-key-MD5 ✅ x-amz-copy-source-server-side-encryption-customer-algorithm ✅ x-amz-copy-source-server-side-encryption-customer-key ✅ x-amz-copy-source-server-side-encryption-customer-key-MD5 ❌ Request Payer: ❌ x-amz-request-payer ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner ❌ x-amz-source-expected-bucket-owner |
| ✅ ListParts | Query Parameters: ✅ max-parts ✅ part-number-marker ❌ Request Payer: ❌ x-amz-request-payer ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
:::caution
Even though ListObjects is a supported operation, it is recommended that you use ListObjectsV2 instead when developing applications. For more information, refer to ListObjects.
:::
Unimplemented object-level operations
| API Name | Feature |
|---|---|
| ❌ GetObjectTagging | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner ❌ Request Payer: ❌ x-amz-request-payer |
| ❌ PutObjectTagging | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner ❌ Request Payer: ❌ x-amz-request-payer ❌ Checksums: ❌ x-amz-sdk-checksum-algorithm |
| ❌ DeleteObjectTagging | ❌ Bucket Owner: ❌ x-amz-expected-bucket-owner |
Extensions
R2 implements some extensions on top of the basic S3 API. This page outlines these additional, available features. Some of the functionality described in this page requires setting a custom header. For examples on how to do so, refer to Configure custom headers.
Extended metadata using Unicode
The Workers R2 API supports Unicode in keys and values natively without requiring any additional encoding or decoding for the customMetadata field. These fields map to the x-amz-meta--prefixed headers used within the R2 S3-compatible API endpoint.
HTTP header names and values may only contain ASCII characters, which is a small subset of the Unicode character library. To easily accommodate users, R2 adheres to RFC 2047 and automatically decodes all x-amz-meta-* header values before storage. On retrieval, any metadata values with unicode are RFC 2047-encoded before rendering the response. The length limit for metadata values is applied to the decoded Unicode value.
:::caution[Metadata variance]
Be mindful when using both Workers and S3 API endpoints to access the same data. If the R2 metadata keys contain Unicode, they are stripped when accessed through the S3 API and the x-amz-missing-meta header is set to the number of keys that were omitted.
:::
These headers map to the httpMetadata field in the R2 bindings:
| HTTP Header | Property Name |
|---|---|
Content-Encoding |
httpMetadata.contentEncoding |
Content-Type |
httpMetadata.contentType |
Content-Language |
httpMetadata.contentLanguage |
Content-Disposition |
httpMetadata.contentDisposition |
Cache-Control |
httpMetadata.cacheControl |
Expires |
httpMetadata.expires |
If using Unicode in object key names, refer to Unicode Interoperability.
Auto-creating buckets on upload
If you are creating buckets on demand, you might initiate an upload with the assumption that a target bucket exists. In this situation, if you received a NoSuchBucket error, you would probably issue a CreateBucket operation. However, following this approach can cause issues: if the body has already been partially consumed, the upload will need to be aborted. A common solution to this issue, followed by other object storage providers, is to use the HTTP 100 response to detect whether the body should be sent, or if the bucket must be created before retrying the upload. However, Cloudflare does not support the HTTP 100 response. Even if the HTTP 100 response was supported, you would still have additional latency due to the round trips involved.
To support sending an upload with a streaming body to a bucket that may not exist yet, upload operations such as PutObject or CreateMultipartUpload allow you to specify a header that will ensure the NoSuchBucket error is not returned. If the bucket does not exist at the time of upload, it is implicitly instantiated with the following CreateBucket request:
PUT / HTTP/1.1
Host: bucket.account.r2.cloudflarestorage.com
<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<LocationConstraint>auto</LocationConstraint>
</CreateBucketConfiguration>
This is only useful if you are creating buckets on demand because you do not know the name of the bucket or the preferred access location ahead of time. For example, you have one bucket per one of your customers and the bucket is created on first upload to the bucket and not during account registration. In these cases, the ListBuckets extension, which supports accounts with more than 1,000 buckets, may also be useful.
PutObject and CreateMultipartUpload
cf-create-bucket-if-missing
Add a cf-create-bucket-if-missing header with the value true to implicitly create the bucket if it does not exist yet. Refer to Auto-creating buckets on upload for a more detailed explanation of when to add this header.
Conditional operations in PutObject
PutObject supports the standard S3 conditional headers If-Match, If-None-Match, If-Modified-Since, and If-Unmodified-Since. If-None-Match: * in particular is commonly used to implement "create-once" semantics: the write only succeeds if the key does not exist yet.
These conditions are evaluated atomically, as part of the same write that commits the object. If two or more clients concurrently issue PutObject with If-None-Match: * for a previously absent key, at most one request succeeds and every other request fails with a 412 PreconditionFailed error code. Competing conditional writes can never both observe success.
A negative read (NoSuchKey) is not proof that a prior write failed if that write's outcome is unknown — an in-flight write may still commit after the read returns. Refer to Consistency model for details.
CopyObject
MERGE metadata directive
The x-amz-metadata-directive allows a MERGE value, in addition to the standard COPY and REPLACE options. When used, MERGE is a combination of COPY and REPLACE, which will COPY any metadata keys from the source object and REPLACE those that are specified in the request with the new value. You cannot use MERGE to remove existing metadata keys from the source — use REPLACE instead.
ListBuckets
ListBuckets supports all the same search parameters as ListObjectsV2 in R2 because some customers may have more than 1,000 buckets. Because tooling, like existing S3 libraries, may not expose a way to set these search parameters, these values may also be sent in via headers. Values in headers take precedence over the search parameters.
| Search parameter | HTTP Header | Meaning |
|---|---|---|
prefix |
cf-prefix |
Show buckets with this prefix only. |
start-after |
cf-start-after |
Show buckets whose name appears lexicographically in the account. |
continuation-token |
cf-continuation-token |
Resume listing from a previously returned continuation token. |
max-keys |
cf-max-keys |
Return this maximum number of buckets. Default and max is 1000. |
The XML response contains a NextContinuationToken and IsTruncated elements as appropriate. Since these may not be accessible from existing S3 APIs, these are also available in response headers:
| XML Response Element | HTTP Response Header | Meaning |
|---|---|---|
IsTruncated |
cf-is-truncated |
This is set to true if the list of buckets returned is not all the buckets on the account. |
NextContinuationToken |
cf-next-continuation-token |
This is set to continuation token to pass on a subsequent ListBuckets to resume the listing. |
StartAfter |
This is the start-after value that was passed in on the request. | |
KeyCount |
The number of buckets returned. | |
ContinuationToken |
The continuation token that was supplied in the request. | |
MaxKeys |
The max keys that were specified in the request. | |
Conditional operations in CopyObject for the destination object
:::note
This feature is currently in beta. If you have feedback, reach out to us on the Cloudflare Developer Discord in the #r2-storage channel or open a thread on the Community Forum. :::
CopyObject already supports conditions that relate to the source object through the x-amz-copy-source-if-... headers as part of our compliance with the S3 API. In addition to this, R2 supports an R2 specific set of headers that allow the CopyObject operation to be conditional on the target object:
cf-copy-destination-if-matchcf-copy-destination-if-none-matchcf-copy-destination-if-modified-sincecf-copy-destination-if-unmodified-since
These headers work akin to the similarly named conditional headers supported on PutObject. When the preceding state of the destination object to does not match the specified conditions the CopyObject operation will be rejected with a 412 PreconditionFailed error code.
Non-atomicity relative to x-amz-copy-source-if
The x-amz-copy-source-if-... headers are guaranteed to be checked when the source object for the copy operation is selected, and the cf-copy-destination-if-... headers are guaranteed to be checked when the object is committed to the bucket state.
However, the time at which the source object is selected for copying, and the point in time when the destination object is committed to the bucket state are not necessarily the same. This means that the cf-copy-destination-if-... headers are not atomic in relation to the x-amz-copy-source-if... headers.
S3
import { DirectoryListing } from "~/components";
Presigned URLs
import { Tabs, TabItem, LinkCard, TypeScriptExample } from "~/components";
Presigned URLs are an S3 concept for granting temporary access to objects without exposing your API credentials. A presigned URL includes signature parameters in the URL itself, authorizing anyone with the URL to perform a specific operation (like GetObject or PutObject) on a specific object until the URL expires.
They are ideal for granting temporary access to specific objects, such as allowing users to upload files directly to R2 or providing time-limited download links.
To generate a presigned URL, you specify:
- Resource identifier: Account ID, bucket name, and object path
- Operation: The S3 API operation permitted (GET, PUT, HEAD, or DELETE)
- Expiry: Timeout from 1 second to 7 days (604,800 seconds)
Presigned URLs are generated server-side with no communication with R2, requiring only your R2 API credentials and an implementation of the AWS Signature Version 4 signing algorithm.
Choosing an approach
R2 supports two patterns for time-limited access. They overlap but have different trade-offs:
| Pattern | Grants | Good for |
|---|---|---|
| Presigned URLs (this page) | A single S3 operation on a single object | Granting direct HTTP access to a single object without an S3 client, such as a browser upload or a shareable download link |
| Temporary credentials | Multiple S3 operations, scoped to a bucket and a set of permitted operations, and optionally to specific paths | Callers that use a standard S3 client or SDK to perform multiple operations in a scoped session |
Generate a presigned URL
Prerequisites
- Account ID (for constructing the S3 endpoint URL)
- R2 API token (Access Key ID and Secret Access Key)
- AWS SDK or compatible S3 client library
:::note[Generating presigned URLs from a Worker]
The R2 Workers API binding cannot generate presigned URLs, so you must sign them with an S3 client. Inside a Worker, you can use aws4fetch, a small library built on fetch and SubtleCrypto, or the AWS SDK for JavaScript v3 with nodejs_compat enabled.
:::
SDK examples
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const S3 = new S3Client({
region: "auto", // Required by SDK but not used by R2
// Provide your Cloudflare account ID
endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
// Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
credentials: {
accessKeyId: '<ACCESS_KEY_ID>',
secretAccessKey: '<SECRET_ACCESS_KEY>',
},
});
// Generate presigned URL for reading (GET)
const getUrl = await getSignedUrl(
S3,
new GetObjectCommand({ Bucket: "my-bucket", Key: "image.png" }),
{ expiresIn: 3600 }, // Valid for 1 hour
);
// https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/image.png?X-Amz-Algorithm=...
// Generate presigned URL for writing (PUT)
// Specify ContentType to restrict uploads to a specific file type
const putUrl = await getSignedUrl(
S3,
new PutObjectCommand({
Bucket: "my-bucket",
Key: "image.png",
ContentType: "image/png",
}),
{ expiresIn: 3600 },
);
import { AwsClient } from "aws4fetch";
export default {
async fetch(request, env): Promise<Response> {
const client = new AwsClient({
service: "s3",
region: "auto",
// Store your S3 API credentials as secrets (see: https://developers.cloudflare.com/r2/api/tokens)
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
});
const R2_URL = `https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com`;
const getUrl = (
await client.sign(
new Request(`${R2_URL}/my-bucket/image.png?X-Amz-Expires=3600`),
{ aws: { signQuery: true } },
)
).url.toString();
const putUrl = (
await client.sign(
new Request(`${R2_URL}/my-bucket/image.png?X-Amz-Expires=3600`, {
method: "PUT",
headers: { "Content-Type": "image/png" },
}),
{ aws: { signQuery: true, allHeaders: true } },
)
).url.toString();
return Response.json({ getUrl, putUrl });
},
} satisfies ExportedHandler<Env>;
import boto3
s3 = boto3.client(
service_name="s3",
# Provide your Cloudflare account ID
endpoint_url='https://<ACCOUNT_ID>.r2.cloudflarestorage.com',
# Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
aws_access_key_id='<ACCESS_KEY_ID>',
aws_secret_access_key='<SECRET_ACCESS_KEY>',
region_name="auto", # Required by SDK but not used by R2
)
# Generate presigned URL for reading (GET)
get_url = s3.generate_presigned_url(
'get_object',
Params={'Bucket': 'my-bucket', 'Key': 'image.png'},
ExpiresIn=3600 # Valid for 1 hour
)
# https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/image.png?X-Amz-Algorithm=...
# Generate presigned URL for writing (PUT)
# Specify ContentType to restrict uploads to a specific file type
put_url = s3.generate_presigned_url(
'put_object',
Params={
'Bucket': 'my-bucket',
'Key': 'image.png',
'ContentType': 'image/png'
},
ExpiresIn=3600
)
# Generate presigned URL for reading (GET)
# The AWS CLI presign command defaults to GET operations
aws s3 presign --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com \
s3://my-bucket/image.png \
--expires-in 3600
# Output:
# https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...
# Note: The AWS CLI presign command only supports GET operations.
# For PUT operations, use one of the SDK examples above.
For complete examples and additional operations, refer to the SDK-specific documentation:
- AWS SDK for JavaScript (Node.js)
- aws4fetch (Workers)
- AWS SDK for Python (Boto3)
- AWS CLI
- AWS SDK for Go
- AWS SDK for PHP
Best practices
When generating presigned URLs, you can limit abuse and misuse by:
- Restricting Content-Type: Specify the allowed
Content-Typein your SDK's parameters. The signature will include this header, so uploads will fail with a403/SignatureDoesNotMatcherror if the client sends a differentContent-Typefor an upload request. When using aws4fetch, setallHeaders: trueso theContent-Typeheader is signed, and send the sameContent-Typewith the upload. - Configuring CORS: If your presigned URLs will be used from a browser, set up CORS rules on your bucket to control which origins can make requests.
Using a presigned URL
Once generated, use a presigned URL like any HTTP endpoint. The signature is embedded in the URL, so no additional authentication headers are required.
# Download using a GET presigned URL
curl "https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/image.png?X-Amz-Algorithm=..."
# Upload using a PUT presigned URL
curl -X PUT "https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/image.png?X-Amz-Algorithm=..." \
--data-binary @image.png
You can also use presigned URLs directly in web browsers, mobile apps, or any HTTP client. The same presigned URL can be reused multiple times until it expires.
Presigned URL example
The following is an example of a presigned URL that was created using R2 API credentials and following the AWS Signature Version 4 signing process:
https://my-bucket.123456789abcdef0123456789abcdef.r2.cloudflarestorage.com/photos/cat.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=CFEXAMPLEKEY12345%2F20251201%2Fauto%2Fs3%2Faws4_request&X-Amz-Date=20251201T180512Z&X-Amz-Expires=3600&X-Amz-Signature=8c3ac40fa6c83d64b4516e0c9e5fa94c998bb79131be9ddadf90cefc5ec31033&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject
In this example, this presigned url performs a GetObject on the object photos/cat.png within bucket my-bucket in the account with id 123456789abcdef0123456789abcdef. The key signature parameters that compose this presigned URL are:
X-Amz-Algorithm: Identifies the algorithm used to sign the URL.X-Amz-Credential: Contains information about the credentials used to calculate the signature.X-Amz-Date: The date and time (in ISO 8601 format) when the signature was created.X-Amz-Expires: The duration in seconds that the presigned URL remains valid, starting fromX-Amz-Date.X-Amz-Signature: The signature proving the URL was signed using the secret key.X-Amz-SignedHeaders: Lists the HTTP headers that were included in the signature calculation.
:::note
The signature parameters (e.g. X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-Signature) cannot be tampered with. Attempting to modify the resource, operation, or expiry will result in a 403/SignatureDoesNotMatch error.
:::
Supported operations
R2 supports presigned URLs for the following HTTP methods:
GET: Fetch an object from a bucketHEAD: Fetch an object's metadata from a bucketPUT: Upload an object to a bucketDELETE: Delete an object from a bucket
POST (multipart form uploads via HTML forms) is not currently supported.
Security considerations
Treat presigned URLs as bearer tokens. Anyone with the URL can perform the specified operation until it expires. Share presigned URLs only with intended recipients and consider using short expiration times for sensitive operations.
Custom domains
Presigned URLs work with the S3 API domain (<ACCOUNT_ID>.r2.cloudflarestorage.com) and cannot be used with custom domains.
If you need authentication with R2 buckets accessed via custom domains (public buckets), use the WAF HMAC validation feature (requires Pro plan or above).
Related resources
Temporary credentials
import { Tabs, TabItem, LinkCard } from "~/components";
Temporary credentials are short-lived, scoped S3 credentials derived from an existing R2 API token. They authenticate with AWS Signature Version 4, the same as a long-lived token, but include a session token and expire automatically. The session token is sent with every request via the X-Amz-Security-Token header; all S3-compatible clients expose this as a standard session token credential field.
Use temporary credentials to delegate access without issuing a long-lived token. For example, granting a mobile client read access to a single prefix for 15 minutes, or issuing per-request upload credentials scoped to one object.
Choosing an approach
R2 supports two patterns for time-limited access. They overlap but have different trade-offs:
| Pattern | Grants | Good for |
|---|---|---|
| Temporary credentials (this page) | Multiple S3 operations, scoped to a bucket and a set of permitted operations, and optionally to specific paths | Callers that use a standard S3 client or SDK to perform multiple operations in a scoped session |
| Presigned URLs | A single S3 operation on a single object | Granting direct HTTP access to a single object without an S3 client, such as a browser upload or a shareable download link |
Generate temporary credentials
Via the Temporary Credentials API
The Temporary Credentials API accepts a parent API token, the bucket name, and optional scoping parameters, and returns a new access key ID, secret access key, and session token. Cloudflare signs the session token on your behalf.
Use this method when you want Cloudflare to manage the signing flow for you.
For a runnable walkthrough, refer to Authenticate against R2 with temporary credentials.
Locally (client-side signing)
You can also generate temporary credentials locally by signing a JWT with your parent API token's secret access key and using it as the session token.
Use this method when:
- You are issuing many short-lived credentials and want to avoid per-mint API latency.
- You need to mint credentials in an environment that cannot reach the Cloudflare API.
- You want to scope credentials by S3 action (see Scope by action), which is currently supported via local signing only.
Signing happens in three steps:
- Build a JWT payload that identifies the bucket and the scope of access.
- Sign the JWT with HS256 using your parent secret access key.
- Derive the temporary secret access key by taking the SHA-256 hex digest of the signed JWT. Encode the session token as
base64("jwt/" + <signed-jwt>).
The parent access key ID is reused as the temporary access key ID.
A complete, runnable example is available in Authenticate against R2 with temporary credentials.
Scope of a credential
Every temporary credential is bound to a single bucket and a set of permitted operations. You can optionally restrict the credential further to specific paths within the bucket.
A temporary credential cannot exceed the permissions of its parent token.
Bucket
A temporary credential is bound to exactly one bucket, identified by name. Cross-bucket access is not supported within a single credential.
Permitted operations
Specify permitted operations using either scope (passed as permission to the API) or actions. You must provide exactly one. A credential that sets both, or neither, is rejected with a 400 InvalidArgument error on X-Amz-Security-Token.
Scope
scope is a preset category of operations. Refer to Permissions for full definitions.
| Scope | Allows |
|---|---|
object-read-only |
Read and list objects in the bucket. |
object-read-write |
Read, write, and list objects in the bucket. |
admin-read-only |
Read and list objects, view bucket configuration, and read from the data catalog. |
admin-read-write |
Read, write, and list objects, edit bucket configuration, and read and write to the data catalog. |
Actions
actions is a non-empty list of permitted S3 operations.
For example, actions: ["GetObject", "HeadObject"] grants read of individual objects but denies ListObjectsV2, even though the broader object-read-only scope would allow listing. Do not set scope alongside actions.
:::note
actions is currently supported via local signing only. Support in the Temporary Credentials API is coming soon.
:::
Valid actions:
| Category | Actions |
|---|---|
| Read | HeadObject, GetObject, GetBucketLocation, ListObjectsV1, ListObjectsV2, ListMultipartUploads, ListParts |
| Write | PutObject, DeleteObject, DeleteObjects, CopyObject |
| Multipart | CreateMultipartUpload, UploadPart, UploadPartCopy, AbortMultipartUpload, CompleteMultipartUpload |
Paths
Restrict access to specific prefixes or objects within the bucket. Omit these fields to grant access to the entire bucket, subject to the permitted operations.
Temporary Credentials API: pass prefixes and objects as top-level fields on the request body.
{
"prefixes": ["uploads/user-123/"],
"objects": ["shared/manifest.json"]
}
Local signing: set paths.prefixPaths and paths.objectPaths on the JWT payload.
{
"paths": {
"prefixPaths": ["uploads/user-123/"],
"objectPaths": ["shared/manifest.json"]
}
}
prefixes/prefixPaths: keys starting with any listed prefix.objects/objectPaths: exact object keys.
Using temporary credentials
Any S3-compatible client that supports session tokens will accept R2 temporary credentials. Pass all three values (access key ID, secret access key, session token) using the client's standard credential fields.
import { AwsClient } from "aws4fetch";
const R2_URL = `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`;
const client = new AwsClient({
accessKeyId: ACCESS_KEY_ID,
secretAccessKey: SECRET_ACCESS_KEY,
sessionToken: SESSION_TOKEN,
service: "s3",
});
const response = await client.fetch(`${R2_URL}/my-bucket/image.png`);
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>",
aws_session_token="<SESSION_TOKEN>",
region_name="auto",
)
Most AWS SDKs and the AWS CLI read credentials from these environment variables by default:
AWS_ACCESS_KEY_ID=<ACCESS_KEY_ID>
AWS_SECRET_ACCESS_KEY=<SECRET_ACCESS_KEY>
AWS_SESSION_TOKEN=<SESSION_TOKEN>
Security considerations
Treat temporary credentials as bearer tokens. Anyone in possession of all three values can perform the allowed operations until the credential expires.
- Scope as narrowly as possible. Set paths and permission scope so the credential can only do what the caller needs.
- Use short TTLs. Set
ttlSecondsto the shortest value that fits your use case. A credential that lives for 15 minutes has a smaller blast radius than one that lives for a day. - A temporary credential cannot exceed its parent. If you revoke the parent API token, all temporary credentials derived from it stop working immediately.
- Never ship your parent secret access key to a client. Local signing must happen in a trusted environment (such as your backend or a Worker).
Related resources
Workers API
import { DirectoryListing } from "~/components";
Workers API reference
import { Type, MetaInfo, WranglerConfig, TabItem, Tabs } from "~/components";
The in-Worker R2 API is accessed by binding an R2 bucket to a Worker. The Worker you write can expose external access to buckets via a route or manipulate R2 objects internally.
The R2 API includes some extensions and semantic differences from the S3 API. If you need S3 compatibility, consider using the S3-compatible API.
Concepts
R2 organizes the data you store, called objects, into containers, called buckets. Buckets are the fundamental unit of performance, scaling, and access within R2.
Create a binding
:::note[Bindings]
A binding is how your Worker interacts with external resources such as KV Namespaces, Durable Objects, or R2 Buckets. A binding is a runtime variable that the Workers runtime provides to your code. You can declare a variable name in your Wrangler file that will be bound to these resources at runtime, and interact with them through this variable. Every binding's variable name and behavior is determined by you when deploying the Worker. Refer to Environment Variables for more information.
A binding is defined in the Wrangler file of your Worker project's directory.
:::
To bind your R2 bucket to your Worker, add the following to your Wrangler file. Update the binding property to a valid JavaScript variable identifier and bucket_name to the name of your R2 bucket:
{
"r2_buckets": [
{
"binding": "MY_BUCKET", // <~ valid JavaScript variable name
"bucket_name": "<YOUR_BUCKET_NAME>"
}
]
}
Within your Worker, your bucket binding is now available under the MY_BUCKET variable and you can begin interacting with it using the bucket methods described below.
Bucket method definitions
The following methods are available on the bucket binding object injected into your code.
For example, to issue a PUT object request using the binding above:
export default {
async fetch(request, env) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
switch (request.method) {
case "PUT":
await env.MY_BUCKET.put(key, request.body);
return new Response(`Put ${key} successfully!`);
default:
return new Response(`${request.method} is not allowed.`, {
status: 405,
headers: {
Allow: "PUT",
},
});
}
},
};
from workers import WorkerEntrypoint, Response
from urllib.parse import urlparse
class Default(WorkerEntrypoint):
async def fetch(self, request):
url = urlparse(request.url)
key = url.path[1:]
if request.method == "PUT":
await self.env.MY_BUCKET.put(key, request.body)
return Response(f"Put {key} successfully!")
else:
return Response(
f"{request.method} is not allowed.",
status=405,
headers={"Allow": "PUT"}
)
-
head- Retrieves the
R2Objectfor the given key containing only object metadata, if the key exists, andnullif the key does not exist.
- Retrieves the
-
get- Retrieves the
R2ObjectBodyfor the given key containing object metadata and the object body as aReadableStream, if the key exists, andnullif the key does not exist. - In the event that a precondition specified in
optionsfails,get()returns anR2Objectwithbodyundefined.
- Retrieves the
-
put- Stores the given
valueand metadata under the associatedkey. Once the write succeeds, returns anR2Objectcontaining metadata about the stored Object. - In the event that a precondition specified in
optionsfails,put()returnsnull, and the object will not be stored. - R2 writes are strongly consistent. Once the Promise resolves, all subsequent read operations will see this key value pair globally.
- Stores the given
-
delete- Deletes the given
valuesand metadata under the associatedkeys. Once the delete succeeds, returnsvoid. - R2 deletes are strongly consistent. Once the Promise resolves, all subsequent read operations will no longer see the provided key value pairs globally.
- Up to 1000 keys may be deleted per call.
- Deletes the given
-
list- Returns an
R2Objectscontaining a list ofR2Objectcontained within the bucket. - The returned list of objects is ordered lexicographically.
- Returns up to 1000 entries, but may return less in order to minimize memory pressure within the Worker.
- To explicitly set the number of objects to list, provide an R2ListOptions object with the
limitproperty set.
- Returns an
-
createMultipartUpload- Creates a multipart upload.
- Returns Promise which resolves to an
R2MultipartUploadobject representing the newly created multipart upload. Once the multipart upload has been created, the multipart upload can be immediately interacted with globally, either through the Workers API, or through the S3 API.
-
resumeMultipartUpload- Returns an object representing a multipart upload with the given key and uploadId.
- The resumeMultipartUpload operation does not perform any checks to ensure the validity of the uploadId, nor does it verify the existence of a corresponding active multipart upload. This is done to minimize latency before being able to call subsequent operations on the
R2MultipartUploadobject.
R2Object definition
R2Object is created when you PUT an object into an R2 bucket. R2Object represents the metadata of an object based on the information provided by the uploader. Every object that you PUT into an R2 bucket will have an R2Object created.
-
key- The object's key.
-
version- Random unique string associated with a specific upload of a key.
-
size- Size of the object in bytes.
-
etag
:::note
Cloudflare recommends using the httpEtag field when returning an etag in a response header. This ensures the etag is quoted and conforms to RFC 9110.
:::
-
The etag associated with the object upload.
-
httpEtag- The object's etag, in quotes so as to be returned as a header.
-
uploaded- A Date object representing the time the object was uploaded.
-
httpMetadata- Various HTTP headers associated with the object. Refer to HTTP Metadata.
-
customMetadata- A map of custom, user-defined metadata associated with the object.
-
range- A
R2Rangeobject containing the returned range of the object.
- A
-
checksums- A
R2Checksumsobject containing the stored checksums of the object. Refer to checksums.
- A
-
writeHttpMetadata- Retrieves the
httpMetadatafrom theR2Objectand applies their corresponding HTTP headers to theHeadersinput object. Refer to HTTP Metadata.
- Retrieves the
-
storageClass- The storage class associated with the object. Refer to Storage Classes.
-
ssecKeyMd5- Hex-encoded MD5 hash of the SSE-C key used for encryption (if one was provided). Hash can be used to identify which key is needed to decrypt object.
R2ObjectBody definition
R2ObjectBody represents an object's metadata combined with its body. It is returned when you GET an object from an R2 bucket. The full list of keys for R2ObjectBody includes the list below and all keys inherited from R2Object.
-
body- The object's value.
-
bodyUsed- Whether the object's value has been consumed or not.
-
arrayBuffer- Returns a Promise that resolves to an
ArrayBuffercontaining the object's value.
- Returns a Promise that resolves to an
-
text- Returns a Promise that resolves to a string containing the object's value.
-
json- Returns a Promise that resolves to the given object containing the object's value.
-
blob- Returns a Promise that resolves to a binary Blob containing the object's value.
R2MultipartUpload definition
An R2MultipartUpload object is created when you call createMultipartUpload or resumeMultipartUpload. R2MultipartUpload is a representation of an ongoing multipart upload.
Uncompleted multipart uploads will be automatically aborted after 7 days.
:::note
An R2MultipartUpload object does not guarantee that there is an active underlying multipart upload corresponding to that object.
A multipart upload can be completed or aborted at any time, either through the S3 API, or by a parallel invocation of your Worker. Therefore it is important to add the necessary error handling code around each operation on a R2MultipartUpload object in case the underlying multipart upload no longer exists.
:::
-
key- The
keyfor the multipart upload.
- The
-
uploadId- The
uploadIdfor the multipart upload.
- The
-
uploadPart- Uploads a single part with the specified part number to this multipart upload. Each part must be uniform in size with an exception for the final part which can be smaller.
- Returns an
R2UploadedPartobject containing theetagandpartNumber. TheseR2UploadedPartobjects are required when completing the multipart upload.
-
abort- Aborts the multipart upload. Returns a Promise that resolves when the upload has been successfully aborted.
-
complete- Completes the multipart upload with the given parts.
- Returns a Promise that resolves when the complete operation has finished. Once this happens, the object is immediately accessible globally by any subsequent read operation.
Method-specific types
R2GetOptions
-
onlyIf- Specifies that the object should only be returned given satisfaction of certain conditions in the
R2Conditionalor in the conditional Headers. Refer to Conditional operations.
- Specifies that the object should only be returned given satisfaction of certain conditions in the
-
range- Specifies that only a specific length (from an optional offset) or suffix of bytes from the object should be returned given the range in the
R2Rangeor in the rangeHeaders. Refer to Ranged reads.
- Specifies that only a specific length (from an optional offset) or suffix of bytes from the object should be returned given the range in the
-
ssecKey- Specifies a key to be used for SSE-C. Key must be 32 bytes in length, in the form of a hex-encoded string or an ArrayBuffer.
Ranged reads
R2GetOptions accepts a range parameter, which can be used to restrict the data returned in body.
There are 3 variations of arguments that can be used in a range:
-
An offset with an optional length.
-
An optional offset with a length.
-
A suffix.
-
offset- The byte to begin returning data from, inclusive.
-
length- The number of bytes to return. If more bytes are requested than exist in the object, fewer bytes than this number may be returned.
-
suffix- The number of bytes to return from the end of the file, starting from the last byte. If more bytes are requested than exist in the object, fewer bytes than this number may be returned.
R2PutOptions
-
onlyIf- Specifies that the object should only be stored given satisfaction of certain conditions in the
R2Conditional. Refer to Conditional operations.
- Specifies that the object should only be stored given satisfaction of certain conditions in the
-
httpMetadata- Various HTTP headers associated with the object. Refer to HTTP Metadata.
-
customMetadata- A map of custom, user-defined metadata that will be stored with the object.
:::note
Only a single hashing algorithm can be specified at once.
:::
-
md5- A md5 hash to use to check the received object's integrity.
-
sha1- A SHA-1 hash to use to check the received object's integrity.
-
sha256- A SHA-256 hash to use to check the received object's integrity.
-
sha384- A SHA-384 hash to use to check the received object's integrity.
-
sha512- A SHA-512 hash to use to check the received object's integrity.
-
storageClass- Sets the storage class of the object if provided. Otherwise, the object will be stored in the default storage class associated with the bucket. Refer to Storage Classes.
-
ssecKey- Specifies a key to be used for SSE-C. Key must be 32 bytes in length, in the form of a hex-encoded string or an ArrayBuffer.
R2MultipartOptions
-
httpMetadata- Various HTTP headers associated with the object. Refer to HTTP Metadata.
-
customMetadata- A map of custom, user-defined metadata that will be stored with the object.
-
storageClass- Sets the storage class of the object if provided. Otherwise, the object will be stored in the default storage class associated with the bucket. Refer to Storage Classes.
-
ssecKey- Specifies a key to be used for SSE-C. Key must be 32 bytes in length, in the form of a hex-encoded string or an ArrayBuffer.
R2ListOptions
-
limit-
The number of results to return. Defaults to
1000, with a maximum of1000. -
If
includeis set, you may receive fewer thanlimitresults in your response to accommodate metadata.
-
-
prefix- The prefix to match keys against. Keys will only be returned if they start with given prefix.
-
cursor- An opaque token that indicates where to continue listing objects from. A cursor can be retrieved from a previous list operation.
-
delimiter- The character to use when grouping keys.
-
include-
Can include
httpMetadataand/orcustomMetadata. If included, items returned by the list will include the specified metadata. -
Note that there is a limit on the total amount of data that a single
listoperation can return. If you request data, you may receive fewer thanlimitresults in your response to accommodate metadata. -
The compatibility date must be set to
2022-08-04or later in your Wrangler file. If not, then ther2_list_honor_includecompatibility flag must be set. Otherwise it is treated asinclude: ['httpMetadata', 'customMetadata']regardless of what theincludeoption provided actually is.
This means applications must be careful to avoid comparing the amount of returned objects against your
limit. Instead, use thetruncatedproperty to determine if thelistrequest has more data to be returned. -
const options = {
limit: 500,
include: ["customMetadata"],
};
const listed = await env.MY_BUCKET.list(options);
let truncated = listed.truncated;
let cursor = truncated ? listed.cursor : undefined;
// ❌ - if your limit can't fit into a single response or your
// bucket has less objects than the limit, it will get stuck here.
while (listed.objects.length < options.limit) {
// ...
}
// ✅ - use the truncated property to check if there are more
// objects to be returned
while (truncated) {
const next = await env.MY_BUCKET.list({
...options,
cursor: cursor,
});
listed.objects.push(...next.objects);
truncated = next.truncated;
cursor = next.cursor;
}
limit = 500
include = ["customMetadata"]
listed = await self.env.MY_BUCKET.list(limit=limit, include=include)
truncated = listed.truncated
cursor = listed.cursor if truncated else None
# ❌ - if your limit can't fit into a single response or your
# bucket has less objects than the limit, it will get stuck here.
while len(listed.objects) < limit:
...
# ✅ - use the truncated property to check if there are more
# objects to be returned
while truncated:
next_page = await self.env.MY_BUCKET.list(limit=limit, include=include, cursor=cursor)
listed.objects.extend(next_page.objects)
truncated = next_page.truncated
cursor = next_page.cursor
R2Objects
An object containing an R2Object array, returned by BUCKET_BINDING.list().
-
objects- An array of objects matching the
listrequest.
- An array of objects matching the
-
truncatedboolean- If true, indicates there are more results to be retrieved for the current
listrequest.
- If true, indicates there are more results to be retrieved for the current
-
cursor- A token that can be passed to future
listcalls to resume listing from that point. Only present if truncated is true.
- A token that can be passed to future
-
delimitedPrefixes-
If a delimiter has been specified, contains all prefixes between the specified prefix and the next occurrence of the delimiter.
-
For example, if no prefix is provided and the delimiter is '/',
foo/bar/bazwould returnfooas a delimited prefix. Iffoo/was passed as a prefix with the same structure and delimiter,foo/barwould be returned as a delimited prefix.
-
Conditional operations
You can pass an R2Conditional object to R2GetOptions and R2PutOptions. If the condition check for get() fails, the body will not be returned. This will make get() have lower latency.
If the condition check for put() fails, null will be returned instead of the R2Object.
-
etagMatches- Performs the operation if the object's etag matches the given string.
-
etagDoesNotMatch- Performs the operation if the object's etag does not match the given string.
-
uploadedBefore- Performs the operation if the object was uploaded before the given date.
-
uploadedAfter- Performs the operation if the object was uploaded after the given date.
Alternatively, you can pass a Headers object containing conditional headers to R2GetOptions and R2PutOptions. For information on these conditional headers, refer to the MDN docs on conditional requests. All conditional headers aside from If-Range are supported.
For more specific information about conditional requests, refer to RFC 7232.
HTTP Metadata
Generally, these fields match the HTTP metadata passed when the object was created. They can be overridden when issuing GET requests, in which case, the given values will be echoed back in the response.
-
contentType -
contentLanguage -
contentDisposition -
contentEncoding -
cacheControl -
cacheExpiry
Checksums
If a checksum was provided when using the put() binding, it will be available on the returned object under the checksums property. The MD5 checksum will be included by default for non-multipart objects.
-
md5- The MD5 checksum of the object.
-
sha1- The SHA-1 checksum of the object.
-
sha256- The SHA-256 checksum of the object.
-
sha384- The SHA-384 checksum of the object.
-
sha512- The SHA-512 checksum of the object.
R2UploadedPart
An R2UploadedPart object represents a part that has been uploaded. R2UploadedPart objects are returned from uploadPart operations and must be passed to completeMultipartUpload operations.
-
partNumber- The number of the part.
-
etag- The
etagof the part.
- The
Storage Class
The storage class where an R2Object is stored. The available storage classes are Standard and InfrequentAccess. Refer to Storage classes
for more information.
Use R2 from Workers
import { Render, PackageManagers, WranglerConfig } from "~/components";
1. Create a new application with C3
C3 (create-cloudflare-cli) is a command-line tool designed to help you set up and deploy Workers & Pages applications to Cloudflare as fast as possible.
To get started, open a terminal window and run:
<PackageManagers type="create" pkg="cloudflare@latest" args={"r2-worker"} />
<Render file="c3-post-run-steps-hello-world" product="workers" params={{ type: "Worker only", lang: "JavaScript", }} />
Then, move into your newly created directory:
cd r2-worker
2. Create your bucket
Create your bucket by running:
npx wrangler r2 bucket create <YOUR_BUCKET_NAME>
To check that your bucket was created, run:
npx wrangler r2 bucket list
After running the list command, you will see all bucket names, including the one you have just created.
3. Bind your bucket to a Worker
You will need to bind your bucket to a Worker.
:::note[Bindings]
A binding is how your Worker interacts with external resources such as KV Namespaces, Durable Objects, or R2 Buckets. A binding is a runtime variable that the Workers runtime provides to your code. You can declare a variable name in your Wrangler file that will be bound to these resources at runtime, and interact with them through this variable. Every binding's variable name and behavior is determined by you when deploying the Worker. Refer to the Environment Variables documentation for more information.
A binding is defined in the Wrangler file of your Worker project's directory.
:::
To bind your R2 bucket to your Worker, add the following to your Wrangler file. Update the binding property to a valid JavaScript variable identifier and bucket_name to the <YOUR_BUCKET_NAME> you used to create your bucket in step 2:
{
"r2_buckets": [
{
"binding": "MY_BUCKET", // <~ valid JavaScript variable name
"bucket_name": "<YOUR_BUCKET_NAME>"
}
]
}
For more detailed information on configuring your Worker (for example, if you are using jurisdictions), refer to the Wrangler Configuration documentation.
4. Access your R2 bucket from your Worker
Within your Worker code, your bucket is now available under the MY_BUCKET variable and you can begin interacting with it.
:::caution[Local Development mode in Wrangler]
By default wrangler dev runs in local development mode. In this mode, all operations performed by your local worker will operate against local storage on your machine.
If you want the R2 operations that are performed during development to be performed against a real R2 bucket, you can set "remote" : true in the R2 binding configuration. Refer to remote bindings documentation for more information.
:::
An R2 bucket is able to READ, LIST, WRITE, and DELETE objects. You can see an example of all operations below using the Module Worker syntax. Add the following snippet into your project's index.js file:
import { TabItem, Tabs } from "~/components";
import { WorkerEntrypoint } from "cloudflare:workers";
export default class extends WorkerEntrypoint<Env> {
async fetch(request: Request) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
switch (request.method) {
case "PUT": {
await this.env.MY_BUCKET.put(key, request.body, {
onlyIf: request.headers,
httpMetadata: request.headers,
});
return new Response(`Put ${key} successfully!`);
}
case "GET": {
const object = await this.env.MY_BUCKET.get(key, {
onlyIf: request.headers,
range: request.headers,
});
if (object === null) {
return new Response("Object Not Found", { status: 404 });
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("etag", object.httpEtag);
// When no body is present, preconditions have failed
return new Response("body" in object ? object.body : undefined, {
status: "body" in object ? 200 : 412,
headers,
});
}
case "DELETE": {
await this.env.MY_BUCKET.delete(key);
return new Response("Deleted!");
}
default:
return new Response("Method Not Allowed", {
status: 405,
headers: {
Allow: "PUT, GET, DELETE",
},
});
}
}
};
export default {
async fetch(request, env) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
switch (request.method) {
case "PUT": {
await env.MY_BUCKET.put(key, request.body, {
onlyIf: request.headers,
httpMetadata: request.headers,
});
return new Response(`Put ${key} successfully!`);
}
case "GET": {
const object = await env.MY_BUCKET.get(key, {
onlyIf: request.headers,
range: request.headers,
});
if (object === null) {
return new Response("Object Not Found", { status: 404 });
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("etag", object.httpEtag);
// When no body is present, preconditions have failed
return new Response("body" in object ? object.body : undefined, {
status: "body" in object ? 200 : 412,
headers,
});
}
case "DELETE": {
await env.MY_BUCKET.delete(key);
return new Response("Deleted!");
}
default:
return new Response("Method Not Allowed", {
status: 405,
headers: {
Allow: "PUT, GET, DELETE",
},
});
}
}
}
```py
from workers import WorkerEntrypoint, Response
from urllib.parse import urlparse
class Default(WorkerEntrypoint): async def fetch(self, request): url = urlparse(request.url) key = url.path[1:]
if request.method == "PUT":
await self.env.MY_BUCKET.put(
key,
request.body,
onlyIf=request.headers,
httpMetadata=request.headers,
)
return Response(f"Put {key} successfully!")
elif request.method == "GET":
obj = await self.env.MY_BUCKET.get(
key,
onlyIf=request.headers,
range=request.headers,
)
if obj is None:
return Response("Object Not Found", status=404)
# When no body is present, preconditions have failed
body = obj.body if hasattr(obj, "body") else None
status = 200 if hasattr(obj, "body") else 412
headers = {"etag": obj.httpEtag}
return Response(body, status=status, headers=headers)
elif request.method == "DELETE":
await self.env.MY_BUCKET.delete(key)
return Response("Deleted!")
else:
return Response(
"Method Not Allowed",
status=405,
headers={"Allow": "PUT, GET, DELETE"},
)
</TabItem>
</Tabs>
<Render file="request-dot-clone-warning" product="workers" />
## 5. Bucket access and privacy
With the above code added to your Worker, every incoming request has the ability to interact with your bucket. This means your bucket is publicly exposed and its contents can be accessed and modified by undesired actors.
You must now define authorization logic to determine who can perform what actions to your bucket. This logic lives within your Worker's code, as it is your application's job to determine user privileges. The following is a short list of resources related to access and authorization practices:
1. [Basic Authentication](/workers/examples/basic-auth/): Shows how to restrict access using the HTTP Basic schema.
2. [Using Custom Headers](/workers/examples/auth-with-headers/): Allow or deny a request based on a known pre-shared key in a header.
{/* <!-- 3. [Authorizing users with Auth0](/workers/tutorials/authorize-users-with-auth0/#overview): Integrate Auth0, an identity management platform, into a Cloudflare Workers application. --> */}
Continuing with your newly created bucket and Worker, you will need to protect all bucket operations.
For `PUT` and `DELETE` requests, you will make use of a new `AUTH_KEY_SECRET` environment variable, which you will define later as a Wrangler secret.
For `GET` requests, you will ensure that only a specific file can be requested. All of this custom logic occurs inside of an `authorizeRequest` function, with the `hasValidHeader` function handling the custom header logic. If all validation passes, then the operation is allowed.
<Tabs syncKey="workersExamples">
<TabItem label="JavaScript" icon="seti:javascript">
```js
const ALLOW_LIST = ["cat-pic.jpg"];
// Check requests for a pre-shared secret
const hasValidHeader = (request, env) => {
return request.headers.get("X-Custom-Auth-Key") === env.AUTH_KEY_SECRET;
};
function authorizeRequest(request, env, key) {
switch (request.method) {
case "PUT":
case "DELETE":
return hasValidHeader(request, env);
case "GET":
return ALLOW_LIST.includes(key);
default:
return false;
}
}
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
if (!authorizeRequest(request, env, key)) {
return new Response("Forbidden", { status: 403 });
}
// ...
},
};
from workers import WorkerEntrypoint, Response
from urllib.parse import urlparse
ALLOW_LIST = ["cat-pic.jpg"]
# Check requests for a pre-shared secret
def has_valid_header(request, env):
return request.headers.get("X-Custom-Auth-Key") == env.AUTH_KEY_SECRET
def authorize_request(request, env, key):
if request.method in ["PUT", "DELETE"]:
return has_valid_header(request, env)
elif request.method == "GET":
return key in ALLOW_LIST
else:
return False
class Default(WorkerEntrypoint):
async def fetch(self, request):
url = urlparse(request.url)
key = url.path[1:]
if not authorize_request(request, self.env, key):
return Response("Forbidden", status=403)
# ...
For this to work, you need to create a secret via Wrangler:
npx wrangler secret put AUTH_KEY_SECRET
This command will prompt you to enter a secret in your terminal:
npx wrangler secret put AUTH_KEY_SECRET
Enter the secret text you'd like assigned to the variable AUTH_KEY_SECRET on the script named <YOUR_WORKER_NAME>:
*********
🌀 Creating the secret for script name <YOUR_WORKER_NAME>
✨ Success! Uploaded secret AUTH_KEY_SECRET.
This secret is now available as AUTH_KEY_SECRET on the env parameter in your Worker.
6. Deploy your Worker
With your Worker and bucket set up, run the npx wrangler deploy command to deploy to Cloudflare's global network:
npx wrangler deploy
You can verify your authorization logic is working through the following commands, using your deployed Worker endpoint:
:::caution
When uploading files to R2 via curl, ensure you use --data-binary instead of --data or -d. Files will otherwise be truncated.
:::
# Attempt to write an object without providing the "X-Custom-Auth-Key" header
curl https://your-worker.dev/cat-pic.jpg -X PUT --data-binary 'test'
#=> Forbidden
# Expected because header was missing
# Attempt to write an object with the wrong "X-Custom-Auth-Key" header value
curl https://your-worker.dev/cat-pic.jpg -X PUT --header "X-Custom-Auth-Key: hotdog" --data-binary 'test'
#=> Forbidden
# Expected because header value did not match the AUTH_KEY_SECRET value
# Attempt to write an object with the correct "X-Custom-Auth-Key" header value
# Note: Assume that "*********" is the value of your AUTH_KEY_SECRET Wrangler secret
curl https://your-worker.dev/cat-pic.jpg -X PUT --header "X-Custom-Auth-Key: *********" --data-binary 'test'
#=> Put cat-pic.jpg successfully!
# Attempt to read object called "foo"
curl https://your-worker.dev/foo
#=> Forbidden
# Expected because "foo" is not in the ALLOW_LIST
# Attempt to read an object called "cat-pic.jpg"
curl https://your-worker.dev/cat-pic.jpg
#=> test
# Note: This is the value that was successfully PUT above
By completing this guide, you have successfully installed Wrangler and deployed your R2 bucket to Cloudflare.
Related resources
Use the R2 multipart API from Workers
By following this guide, you will create a Worker through which your applications can perform multipart uploads. This example worker could serve as a basis for your own use case where you can add authentication to the worker, or even add extra validation logic when uploading each part. This guide also contains an example Python application that uploads files to this worker.
This guide assumes you have set up the R2 binding for your Worker. Refer to Use R2 from Workers for instructions on setting up an R2 binding.
An example Worker using the multipart API
The following example Worker exposes an HTTP API which enables applications to use the multipart API through the Worker.
In this example, each request is routed based on the HTTP method and the action request parameter. As your Worker becomes more complicated, consider utilizing a serverless web framework such as Hono to handle the routing for you.
The following example Worker includes any new information about the state of the multipart upload in the response to each request. For the request which creates the multipart upload, the uploadId is returned. For requests uploading a part, the part number and etag are returned. In turn, the client keeps track of this state, and includes the uploadId in subsequent requests, and the etag and part number of each part when completing a multipart upload.
import { Tabs, TabItem } from "~/components";
Add the following code to your project's index.ts file and replace MY_BUCKET with your bucket's name:
interface Env {
MY_BUCKET: R2Bucket;
}
export default {
async fetch(
request,
env,
ctx
): Promise<Response> {
const bucket = env.MY_BUCKET;
const url = new URL(request.url);
const key = url.pathname.slice(1);
const action = url.searchParams.get("action");
if (action === null) {
return new Response("Missing action type", { status: 400 });
}
// Route the request based on the HTTP method and action type
switch (request.method) {
case "POST":
switch (action) {
case "mpu-create": {
const multipartUpload = await bucket.createMultipartUpload(key);
return new Response(
JSON.stringify({
key: multipartUpload.key,
uploadId: multipartUpload.uploadId,
})
);
}
case "mpu-complete": {
const uploadId = url.searchParams.get("uploadId");
if (uploadId === null) {
return new Response("Missing uploadId", { status: 400 });
}
const multipartUpload = env.MY_BUCKET.resumeMultipartUpload(
key,
uploadId
);
interface completeBody {
parts: R2UploadedPart[];
}
const completeBody: completeBody = await request.json();
if (completeBody === null) {
return new Response("Missing or incomplete body", {
status: 400,
});
}
// Error handling in case the multipart upload does not exist anymore
try {
const object = await multipartUpload.complete(completeBody.parts);
return new Response(null, {
headers: {
etag: object.httpEtag,
},
});
} catch (error: any) {
return new Response(error.message, { status: 400 });
}
}
default:
return new Response(`Unknown action ${action} for POST`, {
status: 400,
});
}
case "PUT":
switch (action) {
case "mpu-uploadpart": {
const uploadId = url.searchParams.get("uploadId");
const partNumberString = url.searchParams.get("partNumber");
if (partNumberString === null || uploadId === null) {
return new Response("Missing partNumber or uploadId", {
status: 400,
});
}
if (request.body === null) {
return new Response("Missing request body", { status: 400 });
}
const partNumber = parseInt(partNumberString);
const multipartUpload = env.MY_BUCKET.resumeMultipartUpload(
key,
uploadId
);
try {
const uploadedPart: R2UploadedPart =
await multipartUpload.uploadPart(partNumber, request.body);
return new Response(JSON.stringify(uploadedPart));
} catch (error: any) {
return new Response(error.message, { status: 400 });
}
}
default:
return new Response(`Unknown action ${action} for PUT`, {
status: 400,
});
}
case "GET":
if (action !== "get") {
return new Response(`Unknown action ${action} for GET`, {
status: 400,
});
}
const object = await env.MY_BUCKET.get(key);
if (object === null) {
return new Response("Object Not Found", { status: 404 });
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("etag", object.httpEtag);
return new Response(object.body, { headers });
case "DELETE":
switch (action) {
case "mpu-abort": {
const uploadId = url.searchParams.get("uploadId");
if (uploadId === null) {
return new Response("Missing uploadId", { status: 400 });
}
const multipartUpload = env.MY_BUCKET.resumeMultipartUpload(
key,
uploadId
);
try {
multipartUpload.abort();
} catch (error: any) {
return new Response(error.message, { status: 400 });
}
return new Response(null, { status: 204 });
}
case "delete": {
await env.MY_BUCKET.delete(key);
return new Response(null, { status: 204 });
}
default:
return new Response(`Unknown action ${action} for DELETE`, {
status: 400,
});
}
default:
return new Response("Method Not Allowed", {
status: 405,
headers: { Allow: "PUT, POST, GET, DELETE" },
});
}
},
} satisfies ExportedHandler<Env>;
from workers import WorkerEntrypoint, Response
from urllib.parse import urlparse, parse_qs
import json
class Default(WorkerEntrypoint):
async def fetch(self, request):
bucket = self.env.MY_BUCKET
url = urlparse(request.url)
key = url.path[1:]
params = parse_qs(url.query)
action = params.get("action", [None])[0]
if action is None:
return Response("Missing action type", status=400)
if request.method == "POST":
if action == "mpu-create":
multipart_upload = await bucket.createMultipartUpload(key)
return Response.json({
"key": multipart_upload.key,
"uploadId": multipart_upload.uploadId,
})
elif action == "mpu-complete":
upload_id = params.get("uploadId", [None])[0]
if upload_id is None:
return Response("Missing uploadId", status=400)
multipart_upload = bucket.resumeMultipartUpload(key, upload_id)
complete_body = await request.json()
if complete_body is None:
return Response("Missing or incomplete body", status=400)
try:
obj = await multipart_upload.complete(complete_body.parts)
return Response(None, headers={"etag": obj.httpEtag})
except Exception as error:
return Response(str(error), status=400)
else:
return Response(f"Unknown action {action} for POST", status=400)
elif request.method == "PUT":
if action == "mpu-uploadpart":
upload_id = params.get("uploadId", [None])[0]
part_number_str = params.get("partNumber", [None])[0]
if part_number_str is None or upload_id is None:
return Response("Missing partNumber or uploadId", status=400)
if request.body is None:
return Response("Missing request body", status=400)
part_number = int(part_number_str)
multipart_upload = bucket.resumeMultipartUpload(key, upload_id)
try:
uploaded_part = await multipart_upload.uploadPart(part_number, request.body)
return Response.json(uploaded_part)
except Exception as error:
return Response(str(error), status=400)
else:
return Response(f"Unknown action {action} for PUT", status=400)
elif request.method == "GET":
if action != "get":
return Response(f"Unknown action {action} for GET", status=400)
obj = await bucket.get(key)
if obj is None:
return Response("Object Not Found", status=404)
body = await obj.text()
headers = {"etag": obj.httpEtag}
return Response(body, headers=headers)
elif request.method == "DELETE":
if action == "mpu-abort":
upload_id = params.get("uploadId", [None])[0]
if upload_id is None:
return Response("Missing uploadId", status=400)
multipart_upload = bucket.resumeMultipartUpload(key, upload_id)
try:
await multipart_upload.abort()
except Exception as error:
return Response(str(error), status=400)
return Response(None, status=204)
elif action == "delete":
await bucket.delete(key)
return Response(None, status=204)
else:
return Response(f"Unknown action {action} for DELETE", status=400)
else:
return Response(
"Method Not Allowed",
status=405,
headers={"Allow": "PUT, POST, GET, DELETE"},
)
After you have updated your Worker with the above code, run npx wrangler deploy.
You can now use this Worker to perform multipart uploads. You can either send requests from your existing application to this Worker to perform uploads or use a script to upload files through this Worker.
The next section is optional and shows an example of a Python script which uploads a chosen file on your machine to your Worker.
Perform a multipart upload with your Worker (optional)
This example application uploads a local file to the Worker in multiple parts. It uses Python's built-in ThreadPoolExecutor to parallelize the uploading of parts to the Worker, which increases upload speeds. HTTP requests to the Worker are made with the requests library.
Utilizing the multipart API in this way also allows you to use your Worker to upload files larger than the Workers request body size limit. The uploading of individual parts is still subject to this limit.
Save the following code in a file named mpuscript.py on your local machine. Change the worker_endpoint variable to where your worker is deployed. Pass the file you want to upload as an argument when running this script: python3 mpuscript.py myfile. This will upload the file myfile from your machine to your bucket through the Worker.
import math
import os
import requests
from requests.adapters import HTTPAdapter, Retry
import sys
import concurrent.futures
# Take the file to upload as an argument
filename = sys.argv[1]
# The endpoint for our worker, change this to wherever you deploy your worker
worker_endpoint = "https://myworker.myzone.workers.dev/"
# Configure the part size to be 10MB. 5MB is the minimum part size, except for the last part
partsize = 10 * 1024 * 1024
def upload_file(worker_endpoint, filename, partsize):
url = f"{worker_endpoint}{filename}"
# Create the multipart upload
uploadId = requests.post(url, params={"action": "mpu-create"}).json()["uploadId"]
part_count = math.ceil(os.stat(filename).st_size / partsize)
# Create an executor for up to 25 concurrent uploads.
executor = concurrent.futures.ThreadPoolExecutor(25)
# Submit a task to the executor to upload each part
futures = [
executor.submit(upload_part, filename, partsize, url, uploadId, index)
for index in range(part_count)
]
concurrent.futures.wait(futures)
# get the parts from the futures
uploaded_parts = [future.result() for future in futures]
# complete the multipart upload
response = requests.post(
url,
params={"action": "mpu-complete", "uploadId": uploadId},
json={"parts": uploaded_parts},
)
if response.status_code == 200:
print("🎉 successfully completed multipart upload")
else:
print(response.text)
def upload_part(filename, partsize, url, uploadId, index):
# Open the file in rb mode, which treats it as raw bytes rather than attempting to parse utf-8
with open(filename, "rb") as file:
file.seek(partsize * index)
part = file.read(partsize)
# Retry policy for when uploading a part fails
s = requests.Session()
retries = Retry(total=3, status_forcelist=[400, 500, 502, 503, 504])
s.mount("https://", HTTPAdapter(max_retries=retries))
return s.put(
url,
params={
"action": "mpu-uploadpart",
"uploadId": uploadId,
"partNumber": str(index + 1),
},
data=part,
).json()
upload_file(worker_endpoint, filename, partsize)
State management
The stateful nature of multipart uploads does not easily map to the usage model of Workers, which are inherently stateless. In a normal multipart upload, the multipart upload is usually performed in one continuous execution of the client application. This is different from multipart uploads in a Worker, which will often be completed over multiple invocations of that Worker. This makes state management more challenging.
To overcome this, the state associated with a multipart upload, namely the uploadId and which parts have been uploaded, needs to be kept track of somewhere outside of the Worker.
In the example Worker and Python application described in this guide, the state of the multipart upload is tracked in the client application which sends requests to the Worker, with the necessary state contained in each request. Keeping track of the multipart state in the client application enables maximal flexibility and allows for parallel and unordered uploads of each part.
When keeping track of this state in the client is impossible, alternative designs can be considered. For example, you could track the uploadId and which parts have been uploaded in a Durable Object or other database.
Bucket locks
Bucket locks prevent the deletion and overwriting of objects in an R2 bucket for a specified period — or indefinitely. When enabled, bucket locks enforce retention policies on your objects, helping protect them from accidental or premature deletions.
Get started with bucket locks
Before getting started, you will need:
- An existing R2 bucket. If you do not already have an existing R2 bucket, refer to Create buckets.
- (API only) An API token with permissions to edit R2 bucket configuration.
Enable bucket lock via dashboard
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Select the bucket you would like to add bucket lock rule to.
-
Switch to the Settings tab, then scroll down to the Bucket lock rules card.
-
Select Add rule and enter the rule name, prefix, and retention period.
-
Select Save changes.
Enable bucket lock via Wrangler
- Install
npm. - Install Wrangler, the Developer Platform CLI.
- Log in to Wrangler with the
wrangler logincommand. - Add a bucket lock rule to your bucket by running the
r2 bucket lock addcommand.
npx wrangler r2 bucket lock add <BUCKET_NAME> [OPTIONS]
Alternatively, you can set the entire bucket lock configuration for a bucket from a JSON file using the r2 bucket lock set command.
npx wrangler r2 bucket lock set <BUCKET_NAME> --file <FILE_PATH>
The JSON file should be in the format of the request body of the put bucket lock configuration API.
Enable bucket lock via API
For information about getting started with the Cloudflare API, refer to Make API calls. For information on required parameters and more examples of how to set bucket lock configuration, refer to the API documentation.
Below is an example of setting a bucket lock configuration (a collection of rules):
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/r2/buckets/<BUCKET_NAME>/lock" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"rules": [
{
"id": "lock-logs-7d",
"enabled": true,
"prefix": "logs/",
"condition": {
"type": "Age",
"maxAgeSeconds": 604800
}
},
{
"id": "lock-images-indefinite",
"enabled": true,
"prefix": "images/",
"condition": {
"type": "Indefinite"
}
}
]
}'
This request creates two rules:
lock-logs-7d: Objects under thelogs/prefix are retained for 7 days (604800 seconds).lock-images-indefinite: Objects under theimages/prefix are locked indefinitely.
:::note
If your bucket is setup with jurisdictional restrictions, you will need to pass a cf-r2-jurisdiction request header with that jurisdiction. For example, cf-r2-jurisdiction: eu.
:::
Get bucket lock rules for your R2 bucket
Dashboard
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Select the bucket you would like to add bucket lock rule to.
-
Switch to the Settings tab, then scroll down to the Bucket lock rules card.
Wrangler
To list bucket lock rules, run the r2 bucket lock list command:
npx wrangler r2 bucket lock list <BUCKET_NAME>
API
For more information on required parameters and examples of how to get bucket lock rules, refer to the API documentation.
Remove bucket lock rules from your R2 bucket
Dashboard
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Select the bucket you would like to add bucket lock rule to.
-
Switch to the Settings tab, then scroll down to the Bucket lock rules card.
-
Locate the rule you want to remove, select the
...icon next to it, and then select Delete.
Wrangler
To remove a bucket lock rule, run the r2 bucket lock remove command:
npx wrangler r2 bucket lock remove <BUCKET_NAME> --id <RULE_ID>
API
To remove bucket lock rules via API, exclude them from your updated configuration and use the put bucket lock configuration API.
Bucket lock rules
A bucket lock configuration can include up to 1,000 rules. Each rule specifies which objects it covers (via prefix) and how long those objects must remain locked. You can:
- Lock objects for a specific duration. For example, 90 days.
- Retain objects until a certain date. For example, until January 1, 2026.
- Keep objects locked indefinitely.
If multiple rules apply to the same prefix or object key, the strictest (longest) retention requirement takes precedence.
Notes
- Rules without prefix apply to all objects in the bucket.
- Bucket locks protect both existing objects and objects created after the rule is enabled. They do not prevent new object creation.
- Once a rule update succeeds, changes to protected objects are blocked from that point on. This includes overwrites, deletes, completing uploads that started earlier, and removal by lifecycle rules.
- A single-object S3 API request blocked by a bucket lock rule fails with HTTP
409and theObjectLockedByBucketPolicyerror code. For multi-object deletes (DeleteObjects), R2 returns HTTP200and reportsObjectLockedByBucketPolicyfor each locked key in the response body. The lock check runs before conditions on the object being written, such asIf-None-MatchorIf-Match, so a write that fails both checks returnsObjectLockedByBucketPolicy, notPreconditionFailed(412). In either case the existing object is not modified. - Bucket lock rules take precedence over lifecycle rules. For example, if a lifecycle rule attempts to delete an object at 30 days but a bucket lock rule requires it be retained for 90 days, the object will not be deleted until the 90-day requirement is met.
- A bucket cannot be emptied while any bucket lock rules are configured. Remove all lock rules before emptying a bucket. Bucket lock rules also apply when deleting folders from the dashboard.
Configure CORS
Cross-Origin Resource Sharing (CORS) is a standardized method that prevents domain X from accessing the resources of domain Y. It does so by using special headers in HTTP responses from domain Y, that allow your browser to verify that domain Y permits domain X to access these resources.
While CORS can help protect your data from malicious websites, CORS is also used to interact with objects in your bucket and configure policies on your bucket.
CORS is used when you interact with a bucket from a web browser, and you have two options:
Set a bucket to public: This option makes your bucket accessible on the Internet as read-only, which means anyone can request and load objects from your bucket in their browser or anywhere else. This option is ideal if your bucket contains images used in a public blog.
Presigned URLs: Allows anyone with access to the unique URL to perform specific actions on your bucket.
Prerequisites
Before you configure CORS, you must have:
- An R2 bucket with at least one object. If you need to create a bucket, refer to Create a public bucket.
- A domain you can use to access the object. This can also be a
localhost. - (Optional) Access keys. An access key is only required when creating a presigned URL.
Use CORS with a public bucket
To use CORS with a public bucket, ensure your bucket is set to allow public access.
Next, add a CORS policy to your bucket to allow the file to be shared.
Use CORS with a presigned URL
Presigned URLs allow temporary access to perform specific actions on your bucket without exposing your credentials. While presigned URLs handle authentication, you still need to configure CORS when making requests from a browser.
When a browser makes a request to a presigned URL on a different origin, the browser enforces CORS. Without a CORS policy, browser-based uploads and downloads using presigned URLs will fail, even though the presigned URL itself is valid.
Expired presigned URLs return a 403 ExpiredRequest response. R2 does not include CORS response headers on expired presigned URL responses, so browser JavaScript cannot read the error body. Refresh presigned URLs before they expire, or route requests through your application server if the browser needs to handle expiration errors directly.
To enable browser-based access with presigned URLs:
-
Add a CORS policy to your bucket that allows requests from your application's origin.
-
Set
AllowedMethodsto match the operations your presigned URLs perform, useGET,PUT,HEAD, and/orDELETE. -
Set
AllowedHeadersto include any headers the client will send when using the presigned URL, such as headers for content type, checksums, caching, or custom metadata. -
(Optional) Set
ExposeHeadersto allow your JavaScript to read response headers likeETag, which contains the object's hash and is useful for verifying uploads. -
(Optional) Set
MaxAgeSecondsto cache the preflight response and reduce the number of preflight requests the browser makes.
The following example allows browser-based uploads from https://example.com with a Content-Type header:
[
{
"AllowedOrigins": ["https://example.com"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["Content-Type"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
Use CORS with a custom domain
Custom domains connected to an R2 bucket with a CORS policy automatically return CORS response headers for cross-origin requests.
Cross-origin requests must include a valid Origin request header, for example, Origin: https://example.com. If you are testing directly or using a command-line tool such as curl, you will not see CORS Access-Control-* response headers unless the Origin request header is included in the request.
:::caution[Caching and CORS headers]
If you set a CORS policy on a bucket that is already serving traffic using a custom domain, any existing cached assets will not reflect the CORS response headers until they are refreshed in cache. Use Cache Purge to purge the cache for that hostname after making any CORS policy related changes.
:::
Add CORS policies from the dashboard
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Locate and select your bucket from the list.
-
Select Settings.
-
Under CORS Policy, select Add CORS policy.
-
From the JSON tab, manually enter or copy and paste your policy into the text box.
-
When you are done, select Save.
Your policy displays on the Settings page for your bucket.
Add CORS policies via Wrangler CLI
You can configure CORS rules using the Wrangler CLI.
- Create a JSON file with your CORS configuration:
{
"rules": [
{
"allowed": {
"origins": ["https://example.com"],
"methods": ["GET"]
}
}
]
}
- Apply the CORS policy to your bucket:
npx wrangler r2 bucket cors set <BUCKET_NAME> --file cors.json
- Verify the CORS policy was applied:
npx wrangler r2 bucket cors list <BUCKET_NAME>
Response headers
The following fields in an R2 CORS policy map to HTTP response headers. These response headers are only returned when the incoming HTTP request is a valid CORS request.
| Field Name | Description | Example |
|---|---|---|
AllowedOrigins |
Specifies the value for the Access-Control-Allow-Origin header R2 sets when requesting objects in a bucket from a browser. |
If a website at www.test.com needs to access resources (e.g. fonts, scripts) on a custom domain of static.example.com, you would set https://www.test.com as an AllowedOrigin. |
AllowedMethods |
Specifies the value for the Access-Control-Allow-Methods header R2 sets when requesting objects in a bucket from a browser. |
GET, POST, PUT |
AllowedHeaders |
Specifies the value for the Access-Control-Allow-Headers header R2 sets when requesting objects in this bucket from a browser.Cross-origin requests that include custom headers (e.g. x-user-id) should specify these headers as AllowedHeaders. |
x-requested-by, User-Agent |
ExposeHeaders |
Specifies the headers that can be exposed back, and accessed by, the JavaScript making the cross-origin request. If you need to access headers beyond the safelisted response headers, such as Content-Encoding or cf-cache-status, you must specify it here. |
Content-Encoding, cf-cache-status, Date |
MaxAgeSeconds |
Specifies the amount of time (in seconds) browsers are allowed to cache CORS preflight responses. Browsers may limit this to 2 hours or less, even if the maximum value (86400) is specified. | 3600 |
Example
This example shows a CORS policy added for a bucket that contains the Roboto-Light.ttf object, which is a font file.
The AllowedOrigins specify the web server being used, and localhost:3000 is the hostname where the web server is running. The AllowedMethods specify that only GET requests are allowed and can read objects in your bucket.
[
{
"AllowedOrigins": ["http://localhost:3000"],
"AllowedMethods": ["GET"]
}
]
In general, a good strategy for making sure you have set the correct CORS rules is to look at the network request that is being blocked by your browser.
- Make sure the rule's
AllowedOriginsincludes the origin where the request is being made from. (likehttp://localhost:3000orhttps://yourdomain.com) - Make sure the rule's
AllowedMethodsincludes the blocked request's method. - Make sure the rule's
AllowedHeadersincludes the blocked request's headers.
Also note that CORS rule propagation can, in rare cases, take up to 30 seconds.
Common Issues
- Only a cross-origin request includes CORS response headers. R2 identifies these requests by an
OriginHTTP request header. The header value must match an origin in your policy'sAllowedOriginsfield. Requests without anOriginheader do not return CORS response headers. - The
AllowedOriginsvalues in your CORS policy must be either*or a valid pattern based on an HTTP Origin header value. A validOriginheader does not include a path component and must only contain ascheme://host[:port](where port is optional).- Use
*to allow requests from any origin. - An origin pattern can contain at most one
*wildcard. The wildcard can span periods. For example,https://*.example.commatches bothhttps://api.example.comandhttps://a.b.example.com, but nothttps://example.com. - Origin values without a wildcard must match the request's
Originheader exactly. - Ports cannot contain wildcards. To allow multiple localhost ports, list each origin separately, such as
http://localhost:3000andhttp://localhost:5173. - Valid
AllowedOriginsvalue:https://static.example.com- includes the scheme and host. A port is optional and implied by the scheme. - Invalid
AllowedOriginsvalue:https://static.example.com/orhttps://static.example.com/fonts/Calibri.woff2- incorrectly includes the path component.
- Use
- If you need to access specific header values via JavaScript on the origin page, such as when using a video player, ensure you set
Access-Control-Expose-Headerscorrectly and include the headers your JavaScript needs access to, such asContent-Length.
Create new buckets
You can create a bucket from the Cloudflare dashboard or using Wrangler.
:::note
Wrangler is a command-line tool for building with Cloudflare's developer products, including R2.
The R2 support in Wrangler allows you to manage buckets and perform basic operations against objects in your buckets. For more advanced use-cases, including bulk uploads or mirroring files from legacy object storage providers, we recommend rclone or an S3-compatible tool of your choice.
:::
Bucket-Level Operations
Create a bucket with the r2 bucket create command:
wrangler r2 bucket create your-bucket-name
:::note
- Bucket names can only contain lowercase letters (a-z), numbers (0-9), and hyphens (-).
- Bucket names cannot begin or end with a hyphen.
- Bucket names can only be between 3-63 characters in length.
The placeholder text is only for the example.
:::
List buckets in the current account with the r2 bucket list command:
wrangler r2 bucket list
To delete a bucket, you must first empty it and then delete it. For detailed instructions, refer to Delete buckets.
Notes
- Bucket names and buckets are not public by default. To allow public access to a bucket, refer to Public buckets.
- For information on controlling access to your R2 bucket with Cloudflare Access, refer to Protect an R2 Bucket with Cloudflare Access.
- Invalid (unauthorized) access attempts to private buckets do not incur R2 operations charges against that bucket. Refer to the R2 pricing FAQ to understand what operations are billed vs. not billed.
Data Access Logs
R2 Data Access Logs provide per-request records for object operations in a bucket. The logs are generally available for R2 buckets without a jurisdiction.
Data Access Logs differ from Audit Logs, which record bucket configuration changes. They also differ from R2 metrics, which provide aggregated request and storage data.
:::note
Data Access Logs include requests with HTTP status codes below 400, including 304 Not Modified. Log delivery is asynchronous and best effort, and events may be delayed or omitted. Requests with status codes of 400 or greater are excluded. Do not rely on Data Access Logs as a complete record of bucket activity.
:::
Supported interfaces
Data Access Logs record requests from these interfaces:
| Interface | Source |
|---|---|
S3 |
Requests through the S3-compatible API. |
API |
Object operations from the Cloudflare dashboard or API. |
Workers |
Object operations through an R2 binding. These events include the Worker script name. |
Public |
Requests to public buckets through r2.dev or custom domains. These requests are unauthenticated. |
Logged operations
Data Access Logs record the following operations:
| Category | Operations |
|---|---|
| Read | GetObject, HeadObject |
| Write and copy | PutObject, CopyObject |
| List | ListObjectsV1, ListObjectsV2 |
| Multipart upload | CreateMultipartUpload, UploadPart, UploadPartCopy, CompleteMultipartUpload, AbortMultipartUpload, ListMultipartUploads, ListParts |
| Delete | DeleteObject, DeleteObjects, DeleteObjectsByPrefix |
Bucket and configuration operations are not included. Failed requests with an HTTP status code of 400 or greater are also not included.
Turn on Data Access Logs
1. In the Cloudflare dashboard, go to the bucket you wish to enable.- Under Data Access Logs, select Enabled.
R2 records new supported operations after you turn on Data Access Logs. Earlier operations are not added retroactively.
Data Access Logs have seven-day retention. Beginning December 1, 2026, they use Cloudflare Observability pricing.
View Data Access Logs
In the Data Access Logs section of the bucket settings, select View logs in Workers Observability. The Events view opens with the r2 dataset and current bucket selected for the previous hour.
Use the Query Builder to change the time range, add filters, or aggregate events.
Turn off Data Access Logs
1. In the Cloudflare dashboard, go to the bucket you wish to disable.- In Data Access Logs, select Disabled.
Log fields
Every event can include these fields:
| Field | Description |
|---|---|
$metadata.timestamp |
Event timestamp in Unix milliseconds. |
$metadata.service |
Bucket name used as the service name. |
$metadata.namespace |
Event namespace. The value is r2. |
action |
R2 operation name. |
actor.type |
Actor type: user or service. Public bucket requests use service. |
actor.id |
Identifier for the authenticated actor. Public bucket requests use public. |
actor.email |
Email address for a user actor. |
actor.accessKeyId |
Access key ID for a request authenticated with AWS Signature Version 4. |
bucket |
Target bucket name. |
interface |
Request interface: S3, API, Workers, or Public. |
request.bytes |
Request Content-Length value in bytes. |
response.bytes |
Response Content-Length value in bytes. |
response.errorCode |
Response error code. This value is NotModified for a 304 response and null for a 2xx response. |
response.errorMessage |
Response error message. This value describes a 304 response and is null for a 2xx response. |
requestMetadata.colo |
Cloudflare data center code, or XXX when the data center is unavailable. |
S3-compatible API, Cloudflare API, and public bucket events can also include these fields:
| Field | Description |
|---|---|
request.method |
HTTP request method. |
request.uri |
Request path. |
response.status |
HTTP response status code. |
requestMetadata.ip |
Client IP address. |
requestMetadata.userAgent |
Client user agent. |
Workers binding events include this additional field:
| Field | Description |
|---|---|
scriptName |
Name of the Worker script that triggered the operation. |
Workers binding events do not include the HTTP method, URI, response status, client IP address, or user agent.
An event can include these operation-specific fields:
| Field | Description |
|---|---|
resource.key |
Object key. |
resource.type |
Resource type: object or multipart_upload. |
resource.size |
Object size in bytes when available. |
resource.uploadId |
Upload ID for a multipart upload. |
sourceResource |
Source bucket, key, and resource type for a copy operation. |
prefix |
Prefix used by a list or prefix-delete operation. |
delimiter |
Delimiter used by a list operation. |
maxKeys |
Maximum keys requested by an object list operation. |
maxUploads |
Maximum uploads requested by a multipart upload list operation. |
objects |
Object keys included in a bulk delete operation. |
The request.bytes and response.bytes fields reflect Content-Length values, not exact transferred-byte measurements. A byte count or resource size of 0 can mean either zero bytes or that the value was unavailable when R2 created the event.
Delete buckets
To delete an R2 bucket, you must first remove all objects from it. Once the bucket is empty, you can delete it.
Empty a bucket
Emptying a bucket deletes every object inside it. The bucket itself and its configuration (lifecycle rules, CORS, event notifications, custom domains) are preserved.
Objects removed during this process cannot be recovered. If your bucket has bucket lock rules, you must remove them before emptying the bucket.
You can empty a bucket in various ways.
Dashboard
The dashboard provides an Empty Bucket action that handles this for you, regardless of how many objects the bucket contains. For large buckets, the operation runs in the background and the dashboard displays progress until all objects have been removed.
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Select the bucket you want to empty.
-
Go to the Settings tab.
-
Scroll to the Empty Bucket section.
-
Select Empty.
-
Confirm the action in the dialog that appears.
:::caution
Emptying a bucket is irreversible. All objects in the bucket are permanently deleted.
:::
Object lifecycle rules
You can configure an object lifecycle rule that expires all objects in the bucket.
- Add a lifecycle rule with no prefix filter and an expiration of 1 day.
- Wait for the rule to take effect. Objects are typically removed within 24 hours, but large buckets may take longer.
- After the bucket is empty, remove the lifecycle rule.
:::note
Objects uploaded while the lifecycle rule is active are also subject to expiration. Remove the rule as soon as the bucket is empty to avoid deleting new objects.
:::
Wrangler
Add a lifecycle rule that expires all objects after 1 day:
npx wrangler r2 bucket lifecycle add <BUCKET_NAME> --expire-days 1
After the bucket is empty, remove the rule:
npx wrangler r2 bucket lifecycle remove <BUCKET_NAME> --id <RULE_ID>
For the full list of lifecycle commands, refer to Wrangler R2 commands.
You can also configure lifecycle rules using the S3 API or the dashboard. For more information, refer to Object lifecycles.
Other approaches
You can also write a script that lists and deletes objects in batches using the S3 API or Workers API, or use rclone to remove objects from the command line.
Delete a bucket
Once a bucket is empty, you can permanently delete it. Deleting a bucket removes the bucket and all of its configuration, including lifecycle rules, CORS settings, event notifications, and custom domains.
You can delete a bucket in various ways.
Dashboard
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Select the bucket you want to delete.
-
Go to the Settings tab.
-
Scroll to the Delete Bucket section. If the bucket is not empty, select Empty Bucket first to clear all objects.
-
Select Delete.
-
Confirm the action in the dialog that appears.
Wrangler
Use the r2 bucket delete command to delete an empty bucket:
npx wrangler r2 bucket delete <BUCKET_NAME>
The command fails if the bucket still contains objects. Empty the bucket before running this command.
API
Use the delete bucket API endpoint to delete an empty bucket:
curl -X DELETE "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/r2/buckets/<BUCKET_NAME>" \
-H "Authorization: Bearer <API_TOKEN>"
If the bucket is in a jurisdiction, include the cf-r2-jurisdiction header:
curl -X DELETE "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/r2/buckets/<BUCKET_NAME>" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "cf-r2-jurisdiction: eu"
Behavior
- A bucket must be completely empty before it can be deleted. Attempting to delete a bucket that contains objects returns an error.
- You cannot empty a bucket that has bucket lock rules. Remove all lock rules before emptying the bucket.
- Event notifications configured on the bucket are removed when the bucket is deleted.
- If you use a custom domain with the bucket, remove the domain association before or after deletion to avoid dangling DNS records.
Related resources
Event notifications
Event notifications send messages to your queue when data in your R2 bucket changes. You can consume these messages with a consumer Worker or pull over HTTP from outside of Cloudflare Workers.
Get started with event notifications
Prerequisites
Before getting started, you will need:
- An existing R2 bucket. If you do not already have an existing R2 bucket, refer to Create buckets.
- An existing queue. If you do not already have a queue, refer to Create a queue.
- A consumer Worker or HTTP pull enabled on your Queue.
Enable event notifications via Dashboard
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Select the bucket you'd like to add an event notification rule to.
-
Switch to the Settings tab, then scroll down to the Event notifications card.
-
Select Add notification and choose the queue you'd like to receive notifications and the type of events that will trigger them.
-
Select Add notification.
Enable event notifications via Wrangler
Set up Wrangler
To begin, install npm. Then install Wrangler, the Developer Platform CLI.
Enable event notifications on your R2 bucket
Log in to Wrangler with the wrangler login command. Then add an event notification rule to your bucket by running the r2 bucket notification create command.
npx wrangler r2 bucket notification create <BUCKET_NAME> --event-type <EVENT_TYPE> --queue <QUEUE_NAME>
To add filtering based on prefix or suffix use the --prefix or --suffix flag, respectively.
# Filter using prefix
$ npx wrangler r2 bucket notification create <BUCKET_NAME> --event-type <EVENT_TYPE> --queue <QUEUE_NAME> --prefix "<PREFIX_VALUE>"
# Filter using suffix
$ npx wrangler r2 bucket notification create <BUCKET_NAME> --event-type <EVENT_TYPE> --queue <QUEUE_NAME> --suffix "<SUFFIX_VALUE>"
# Filter using prefix and suffix. Both the conditions will be used for filtering
$ npx wrangler r2 bucket notification create <BUCKET_NAME> --event-type <EVENT_TYPE> --queue <QUEUE_NAME> --prefix "<PREFIX_VALUE>" --suffix "<SUFFIX_VALUE>"
For a more complete step-by-step example, refer to the Log and store upload events in R2 with event notifications example.
Event notification rules
Event notification rules determine the event types that trigger notifications and optionally enable filtering based on object prefix and suffix. You can have up to 100 event notification rules per R2 bucket.
Event types
| Event type | Description | Trigger actions |
|---|---|---|
object-create |
Triggered when new objects are created or existing objects are overwritten. |
|
object-delete |
Triggered when an object is explicitly removed from the bucket. |
|
Message format
Queue consumers receive notifications as Messages. The following is an example of the body of a message that a consumer Worker will receive:
{
"account": "3f4b7e3dcab231cbfdaa90a6a28bd548",
"action": "CopyObject",
"bucket": "my-bucket",
"object": {
"key": "my-new-object",
"size": 65536,
"eTag": "c846ff7a18f28c2e262116d6e8719ef0"
},
"eventTime": "2024-05-24T19:36:44.379Z",
"copySource": {
"bucket": "my-bucket",
"object": "my-original-object"
}
}
Properties
| Property | Type | Description |
|---|---|---|
account |
String | The Cloudflare account ID that the event is associated with. |
action |
String | The type of action that triggered the event notification. Example actions include: PutObject, CopyObject, CompleteMultipartUpload, DeleteObject. |
bucket |
String | The name of the bucket where the event occurred. |
object |
Object | A nested object containing details about the object involved in the event. |
object.key |
String | The key (or name) of the object within the bucket. |
object.size |
Number | The size of the object in bytes. Note: not present for object-delete events. |
object.eTag |
String | The entity tag (eTag) of the object. Note: not present for object-delete events. |
eventTime |
String | The time when the action that triggered the event occurred. |
copySource |
Object | A nested object containing details about the source of a copied object. Note: only present for events triggered by CopyObject. |
copySource.bucket |
String | The bucket that contained the source object. |
copySource.object |
String | The name of the source object. |
Notes
- Queues per-queue message throughput is currently 5,000 messages per second. If your workload produces more than 5,000 notifications per second, we recommend splitting notification rules across multiple queues.
- Rules without prefix/suffix apply to all objects in the bucket.
- Overlapping or conflicting rules that could trigger multiple notifications for the same event are not allowed. For example, if you have an
object-create(orPutObjectaction) rule without a prefix and suffix, then adding anotherobject-create(orPutObjectaction) rule with a prefix likeimages/could trigger more than one notification for a single upload, which is invalid.
Buckets
import { DirectoryListing } from "~/components"
With object storage, all of your objects are stored in buckets. Buckets do not contain folders that group the individual files, but instead, buckets have a flat structure which simplifies the way you access and retrieve the objects in your bucket.
Learn more about bucket level operations from the items below.
Local uploads
import { Tabs, TabItem, DashButton } from "/components";
import R2LocalUploadsDiagram from "/components/cf/R2LocalUploadsDiagram.astro";
You can enable Local Uploads on your bucket to improve the performance of upload requests when clients upload data from a different region than your bucket. Local Uploads writes object data to a nearby location, then asynchronously copies it to your bucket. Data is available immediately and remains strongly consistent.
How it works
The following sections describe how R2 handles upload requests with and without Local Uploads enabled.
Without Local Uploads
When a client uploads an object to your R2 bucket, the object data must travel from the client to the storage infrastructure of your bucket. This behavior can result in higher latency and lower reliability when your client is in a different region than the bucket. Refer to How R2 works for details.
With Local Uploads
When you make an upload request (i.e. PutObject and UploadPart) to a bucket with Local Uploads enabled, there are two cases that are handled:
- Client and bucket in same region: R2 follows the normal upload flow where object data is uploaded from the client to the storage infrastructure of your bucket.
- Client and bucket in different regions: Object data is written to storage near the client, then asynchronously replicated to your bucket. The object is immediately accessible and remains durable during the process.
When to use local uploads
Local uploads are built for workloads that receive a lot of uploads originating from different geographic regions than where your bucket is located. This feature is ideal when:
- Your users are globally distributed
- Upload performance and reliability is critical to your application
- You want to optimize write performance without changing your bucket's primary location
To understand the geographic distribution of where your read and write requests are initiated:
-
Log in to the Cloudflare dashboard, and go to R2 Overview.
-
Select your bucket.
-
Select Metrics and view the Request Distribution chart.
Read latency considerations
When local uploads is enabled, uploaded data may temporarily reside near the client before replication completes.
If your workload requires immediate read after write, consider where your read requests originate. Reads from the uploader's region will be fast, while reads from near the bucket's region may experience cross-region latency until replication completes.
Jurisdiction restriction
Local uploads are not supported for buckets with jurisdictional restrictions, because it requires temporarily routing data through locations outside the bucket’s region.
Enable local uploads
When you enable Local Uploads, existing uploads will complete as expected with no interruption to traffic.
-
Log in to the Cloudflare dashboard, and go to R2 Overview.
-
Select your bucket.
-
Select Settings.
-
Under Local Uploads, select Enable.
Run the following command:
npx wrangler r2 bucket local-uploads enable <BUCKET_NAME>
Disable local uploads
You can disable local uploads at any time. Existing requests made with local uploads will complete replication with no interruption to your traffic.
-
Log in to the Cloudflare dashboard, and go to R2 Overview.
-
Select your bucket.
-
Select Settings.
-
Under Local Uploads, select Disable.
Run the following command:
npx wrangler r2 bucket local-uploads disable <BUCKET_NAME>
Pricing
There is no additional cost to enable local uploads. Upload requests made with this feature enabled incur the standard Class A operation costs, same as upload requests made without local uploads.
Object lifecycles
Object lifecycles determine the retention period of objects uploaded to your bucket and allow you to specify when objects should transition from Standard storage to Infrequent Access storage.
A lifecycle configuration is a collection of lifecycle rules that define actions to apply to objects during their lifetime.
For example, you can create an object lifecycle rule to delete objects after 90 days, or you can set a rule to transition objects to Infrequent Access storage after 30 days.
Behavior
- Objects will typically be removed from a bucket within 24 hours of the
x-amz-expirationvalue. - When a lifecycle configuration is applied that deletes objects, newly uploaded objects'
x-amz-expirationvalue immediately reflects the expiration based on the new rules, but existing objects may experience a delay. Most objects will be transitioned within 24 hours but may take longer depending on the number of objects in the bucket. While objects are being migrated, you may see old applied rules from the previous configuration. - An object is no longer billable once it has been deleted.
- Buckets have a default lifecycle rule to expire multipart uploads seven days after initiation.
- When an object is transitioned from Standard storage to Infrequent Access storage, a Class A operation is incurred.
- When rules conflict and specify both a storage class transition and expire transition within a 24-hour period, the expire (or delete) lifecycle transition takes precedence over transitioning storage class.
Configure lifecycle rules for your bucket
When you create an object lifecycle rule, you can specify which prefix you would like it to apply to.
- Note that object lifecycles currently has a 1000 rule maximum.
- Managing object lifecycles is a bucket-level action, and requires an API token with the
Workers R2 Storage Writepermission group.
Dashboard
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Locate and select your bucket from the list.
-
From the bucket page, select Settings.
-
Under Object Lifecycle Rules, select Add rule.
-
Fill out the fields for the new rule.
-
When you are done, select Save changes.
Wrangler
- Install
npm. - Install Wrangler, the Developer Platform CLI.
- Log in to Wrangler with the
wrangler logincommand. - Add a lifecycle rule to your bucket by running the
r2 bucket lifecycle addcommand.
npx wrangler r2 bucket lifecycle add <BUCKET_NAME> [OPTIONS]
Alternatively you can set the entire lifecycle configuration for a bucket from a JSON file using the r2 bucket lifecycle set command.
npx wrangler r2 bucket lifecycle set <BUCKET_NAME> --file <FILE_PATH>
The JSON file should be in the format of the request body of the put object lifecycle configuration API.
S3 API
Below is an example of configuring a lifecycle configuration (a collection of lifecycle rules) with different sets of rules for different potential use cases.
import {
S3Client,
PutBucketLifecycleConfigurationCommand,
} from "@aws-sdk/client-s3";
const client = new S3Client({
endpoint: "https://<account_id>.r2.cloudflarestorage.com",
credentials: {
accessKeyId: "<access_key_id>",
secretAccessKey: "<access_key_secret>",
},
region: "auto",
});
await client.send(
new PutBucketLifecycleConfigurationCommand({
Bucket: "testBucket",
LifecycleConfiguration: {
Rules: [
// Example: deleting objects on a specific date
// Delete 2019 documents in 2024
{
ID: "Delete 2019 Documents",
Status: "Enabled",
Filter: {
Prefix: "2019/",
},
Expiration: {
Date: new Date("2024-01-01"),
},
},
// Example: transitioning objects to Infrequent Access storage by age
// Transition objects older than 30 days to Infrequent Access storage
{
ID: "Transition Objects To Infrequent Access",
Status: "Enabled",
Transitions: [
{
Days: 30,
StorageClass: "STANDARD_IA",
},
],
},
// Example: deleting objects by age
// Delete logs older than 90 days
{
ID: "Delete Old Logs",
Status: "Enabled",
Filter: {
Prefix: "logs/",
},
Expiration: {
Days: 90,
},
},
// Example: abort all incomplete multipart uploads after a week
{
ID: "Abort Incomplete Multipart Uploads",
Status: "Enabled",
AbortIncompleteMultipartUpload: {
DaysAfterInitiation: 7,
},
},
// Example: abort user multipart uploads after a day
{
ID: "Abort User Incomplete Multipart Uploads",
Status: "Enabled",
Filter: {
Prefix: "useruploads/",
},
AbortIncompleteMultipartUpload: {
// For uploads matching the prefix, this rule will take precedence
// over the one above due to its earlier expiration.
DaysAfterInitiation: 1,
},
},
],
},
}),
);
Get lifecycle rules for your bucket
Wrangler
To get the list of lifecycle rules associated with your bucket, run the r2 bucket lifecycle list command.
npx wrangler r2 bucket lifecycle list <BUCKET_NAME>
S3 API
import {
S3Client,
GetBucketLifecycleConfigurationCommand,
} from "@aws-sdk/client-s3";
// Configure the S3 client to talk to R2.
const client = new S3Client({
endpoint: "https://<account_id>.r2.cloudflarestorage.com",
credentials: {
accessKeyId: "<access_key_id>",
secretAccessKey: "<access_key_secret>",
},
region: "auto",
});
// Get lifecycle configuration for bucket
console.log(
await client.send(
new GetBucketLifecycleConfigurationCommand({
Bucket: "bucketName",
}),
),
);
Delete lifecycle rules from your bucket
Dashboard
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Locate and select your bucket from the list.
-
From the bucket page, select Settings.
-
Under Object lifecycle rules, select the rules you would like to delete.
-
When you are done, select Delete rule(s).
Wrangler
To remove a specific lifecycle rule from your bucket, run the r2 bucket lifecycle remove command.
npx wrangler r2 bucket lifecycle remove <BUCKET_NAME> --id <RULE_ID>
S3 API
import { S3Client, DeleteBucketLifecycleCommand } from "@aws-sdk/client-s3";
// Configure the S3 client to talk to R2.
const client = new S3Client({
endpoint: "https://<account_id>.r2.cloudflarestorage.com",
credentials: {
accessKeyId: "<access_key_id>",
secretAccessKey: "<access_key_secret>",
},
region: "auto",
});
// Delete lifecycle configuration for bucket
await client.send(
new DeleteBucketLifecycleCommand({
Bucket: "bucketName",
}),
);
Public buckets
import { Render } from "~/components";
Public Bucket is a feature that allows users to expose the contents of their R2 buckets directly to the Internet. By default, buckets are never publicly accessible and will always require explicit user permission to enable.
Public buckets can be set up in either of two ways:
- Expose your bucket as a custom domain under your control.
- Expose your bucket using a Cloudflare-managed
https://r2.devsubdomain for non-production use cases.
These options can be used independently. Enabling custom domains does not require enabling r2.dev access.
To use features like WAF custom rules, caching, access controls, or Bot Management, you must configure your bucket behind a custom domain. These capabilities are not available when using the r2.dev development url.
:::note
Currently, public buckets do not let you list the bucket contents at the root of your (sub) domain.
:::
Custom domains
Caching
Domain access through a custom domain allows you to use Cloudflare Cache to accelerate access to your R2 bucket.
Configure your cache to use Smart Tiered Cache to have a single upper-tier data center next to your R2 bucket.
:::note
By default, only certain file types are cached. To cache all files in your bucket, you must set a Cache Everything page rule.
For more information on default Cache behavior and how to customize it, refer to Default Cache Behavior
:::
Access control
To restrict access to your custom domain's bucket, use Cloudflare's existing security products.
- Cloudflare Zero Trust Access: Protects buckets that should only be accessible by your teammates. Refer to Protect an R2 Bucket with Cloudflare Access tutorial for more information.
- Cloudflare WAF Token Authentication: Restricts access to documents, files, and media to selected users by providing them with an access token.
:::caution
Disable public access to your r2.dev subdomain when using products like WAF or Cloudflare Access. If you do not disable public access, your bucket will remain publicly available through your r2.dev subdomain.
:::
Minimum TLS Version & Cipher Suites
To customise the minimum TLS version or cipher suites of a custom hostname of an R2 bucket, you can issue an API call to edit R2 custom domain settings. You will need to add the optional minTLS and ciphers parameters to the request body. For a list of the cipher suites you can specify, refer to Supported cipher suites.
Add your domain to Cloudflare
The domain being used must have been added as a zone in the same account as the R2 bucket.
- If your domain is already managed by Cloudflare, you can proceed to Connect a bucket to a custom domain.
- If your domain is not managed by Cloudflare, you need to set it up using a partial (CNAME) setup to add it to your account.
Once the domain exists in your Cloudflare account (regardless of setup type), you can link it to your bucket.
Connect a bucket to a custom domain
To view the added DNS record, select ... next to the connected domain and select Manage DNS.
:::note
If the zone is on an Enterprise plan, make sure that you release the zone hold before adding the custom domain.
A zone hold would prevent the custom subdomain from activating.
:::
Disable domain access
Disabling a domain will turn off public access to your bucket through that domain. Access through other domains or the managed r2.dev subdomain are unaffected.
The specified domain will also remain connected to R2 until you remove it or delete the bucket.
To disable a domain:
- In R2, select the bucket you want to modify.
- On the bucket page, Select Settings, go to Custom Domains.
- Next to the domain you want to disable, select ... and Disable domain.
- The badge under Access to Bucket will update to Not allowed.
Remove domain
Removing a custom domain will disconnect it from your bucket and delete its configuration from the dashboard. Your bucket will remain publicly accessible through any other enabled access method, but the domain will no longer appear in the connected domains list.
To remove a domain:
- In R2, select the bucket you want to modify.
- On the bucket page, Select Settings, go to Custom Domains.
- Next to the domain you want to disable, select ... and Remove domain.
- Select Remove domain in the confirmation window. This step also removes the CNAME record pointing to the domain. You can always add the domain again.
Public development URL
Expose the contents of this R2 bucket to the internet through a Cloudflare-managed r2.dev subdomain. This endpoint is intended for non-production traffic.
:::note
Public access through r2.dev subdomains is rate-limited and should only be used for development purposes.
To enable access management, cache, and bot management features, you must set up a custom domain when enabling public access to your bucket.
Avoid creating a CNAME record pointing to the r2.dev subdomain. This is an unsupported access path, and we cannot guarantee consistent reliability or performance. For production use, add your domain to Cloudflare instead.
:::
Enable public development URL
When you enable public development URL access for your bucket, its contents become available on the internet through a Cloudflare-managed r2.dev subdomain.
To enable access through r2.dev for your buckets:
- In R2, select the bucket you want to modify.
- On the bucket page, select Settings.
- Under Public Development URL, select Enable.
- In Allow Public Access?, confirm your choice by typing
allowto confirm and select Allow. - You can now access the bucket and its objects using the Public Bucket URL.
To verify that your bucket is publicly accessible, check that Public URL Access shows Allowed in your bucket settings.
Disable public development URL
Disabling public development URL access removes your bucket's exposure through the r2.dev subdomain. The bucket and its objects will no longer be accessible via the Public Bucket URL.
If you have connected other domains, the bucket will remain accessible for those domains.
To disable public access for your bucket:
- In R2, select the bucket you want to modify.
- On the bucket page, select Settings.
- Under Public Development URL, select Disable.
- In Disallow Public Access?, type
disallowto confirm and select Disallow.
Storage classes
import { Badge, Tabs, TabItem } from "~/components"
Storage classes allow you to trade off between the cost of storage and the cost of accessing data. Every object stored in R2 has an associated storage class.
All storage classes share the following characteristics:
- Compatible with Workers API, S3 API, and public buckets.
- 99.999999999% (eleven 9s) of annual durability.
- No minimum object size.
Available storage classes
| Storage class | Minimum storage duration | Data retrieval fees (processing) | Egress fees (data transfer to Internet) |
|---|---|---|---|
| Standard | None | None | None |
| Infrequent Access | 30 days | Yes | None |
For more information on how storage classes impact pricing, refer to Pricing.
Standard storage
Standard storage is designed for data that is accessed frequently. This is the default storage class for new R2 buckets unless otherwise specified.
Example use cases
- Website and application data
- Media content (e.g., images, video)
- Storing large datasets for analysis and processing
- AI training data
- Other workloads involving frequently accessed data
Infrequent Access storage
Infrequent Access storage is ideal for data that is accessed less frequently. This storage class offers lower storage cost compared to Standard storage, but includes retrieval fees and a 30 day minimum storage duration requirement.
:::note For objects stored in Infrequent Access storage, you will be charged for the object for the minimum storage duration even if the object was deleted, moved, or replaced before the specified duration. :::
Example use cases
- Long-term data archiving (for example, logs and historical records needed for compliance)
- Data backup and disaster recovery
- Long tail user-generated content
Set default storage class for buckets
By setting the default storage class for a bucket, all objects uploaded into the bucket will automatically be assigned the selected storage class unless otherwise specified. Default storage class can be changed after bucket creation in the Dashboard.
To learn more about creating R2 buckets, refer to Create new buckets.
Set storage class for objects
Specify storage class during object upload
To learn more about how to specify the storage class for new objects, refer to the Workers API and S3 API documentation.
Use object lifecycle rules to transition objects to Infrequent Access storage
:::note Once an object is stored in Infrequent Access, it cannot be transitioned to Standard Access using lifecycle policies. :::
To learn more about how to transition objects from Standard storage to Infrequent Access storage, refer to Object lifecycles.
Change storage class for objects
You can change the storage class of an object which is already stored in R2 using the CopyObject API.
Use the x-amz-storage-class header to change between STANDARD and STANDARD_IA.
An example of switching an object from STANDARD to STANDARD_IA using aws cli is shown below:
aws s3api copy-object \
--endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com \
--bucket bucket-name \
--key path/to/object.txt \
--copy-source /bucket-name/path/to/object.txt \
--storage-class STANDARD_IA
- Refer to aws CLI for more information on using
aws CLI. - Refer to object-level operations for the full list of object-level API operations with R2-compatible S3 API.
Data migration
Quickly and easily migrate data from other cloud providers to R2. Explore each option further by navigating to their respective documentation page.
| Super Slurper | Quickly migrate large amounts of data from other cloud providers to R2. |
|
||||||||||||
| Sippy | Incremental data migration, populating your R2 bucket as objects are requested. |
|
For information on how to leverage these tools effectively, refer to Migration Strategies
Migration Strategies
import { Render, FileTree } from "~/components";
You can use a combination of Super Slurper and Sippy to effectively migrate all objects with minimal downtime.
When the source bucket is actively being read from / written to
- Enable Sippy and start using the R2 bucket in your application.
- This copies objects from your previous bucket into the R2 bucket on demand when they are requested by the application.
- New uploads will go to the R2 bucket.
- Use Super Slurper to trigger a one-off migration to copy the remaining objects into the R2 bucket.
- In the Destination R2 bucket > Overwrite files?, select "Skip existing".
When the source bucket is not being read often
- Use Super Slurper to copy all objects to the R2 bucket.
- Note that Super Slurper may skip some objects if they are uploaded after it lists the objects to be copied.
- Enable Sippy on your R2 bucket, then start using the R2 bucket in your application.
- New uploads will go to the R2 bucket.
- Objects which were uploaded while Super Slurper was copying the objects will be copied on-demand (by Sippy) when they are requested by the application.
Optimizing your Slurper data migration performance
For an account, you can run three concurrent Slurper migration jobs at any given time, and each Slurper migration job can process a set amount of requests per second.
To increase overall throughput and reliability, we recommend splitting your migration into smaller, concurrent jobs using the prefix (or bucket subpath) option.
When creating a migration job:
- Go to the Source bucket step.
- Under Define rules, in Bucket subpath, specify subpaths to divide your data by prefix.
- Complete the data migration set up.
For example, suppose your source bucket contains:
- photos - 2024 - file1.jpg - file2.jpg - 2023 - file3.jpg - 2019 - file4.jpgYou can create separate jobs with prefixes such as:
/photos/2024to migrate all 2024 files/photos/202to migrate all files from 2023 and 2024
Each prefix runs as an independent migration job, allowing Slurper to transfer data in parallel. This improves total transfer speed and ensures that a failure in one job does not interrupt the others.
Sippy
import { Render, DashButton, Steps } from "~/components";
Sippy is a data migration service that allows you to copy data from other cloud providers to R2 as the data is requested, without paying unnecessary cloud egress fees typically associated with moving large amounts of data.
Migration-specific egress fees are reduced by leveraging requests within the flow of your application where you would already be paying egress fees to simultaneously copy objects to R2.
How it works
When enabled for an R2 bucket, Sippy implements the following migration strategy across Workers, S3 API, and public buckets:
- When an object is requested, it is served from your R2 bucket if it is found.
- If the object is not found in R2, the object will simultaneously be returned from your source storage bucket and copied to R2.
- All other operations, including put and delete, continue to work as usual.
When is Sippy useful?
Using Sippy as part of your migration strategy can be a good choice when:
- You want to start migrating your data, but you want to avoid paying upfront egress fees to facilitate the migration of your data all at once.
- You want to experiment by serving frequently accessed objects from R2 to eliminate egress fees, without investing time in data migration.
- You have frequently changing data and are looking to conduct a migration while avoiding downtime. Sippy can be used to serve requests while Super Slurper can be used to migrate your remaining data.
If you are looking to migrate all of your data from an existing cloud provider to R2 at one time, we recommend using Super Slurper.
Get started with Sippy
Before getting started, you will need:
- An existing R2 bucket. If you don't already have one, refer to Create buckets.
- API credentials for your source object storage bucket.
- (Wrangler only) Cloudflare R2 Access Key ID and Secret Access Key with read and write permissions. For more information, refer to Authentication.
Enable Sippy via the Dashboard
1. In the Cloudflare dashboard, go to the **R2 object storage** page. 2. Select the bucket you would like to migrate objects to. 3. Switch to the **Settings** tab, then scroll down to the **On Demand Migration** card. 4. Select **Enable** and enter details for the bucket you would like to migrate objects from. The credentials you enter must have permissions to read from this bucket. Cloudflare also recommends scoping your credentials to only allow reads from this bucket. 5. Select **Enable**.Enable Sippy via Wrangler
Set up Wrangler
To begin, install npm. Then install Wrangler, the Developer Platform CLI.
Enable Sippy on your R2 bucket
Log in to Wrangler with the wrangler login command. Then run the r2 bucket sippy enable command:
npx wrangler r2 bucket sippy enable "<BUCKET_NAME>"
This will prompt you to select between supported object storage providers and lead you through setup.
Enable Sippy via API
For information on required parameters and examples of how to enable Sippy, refer to the API documentation. For information about getting started with the Cloudflare API, refer to Make API calls.
:::note
If your bucket is setup with jurisdictional restrictions, you will need to pass a cf-r2-jurisdiction request header with that jurisdiction. For example, cf-r2-jurisdiction: eu.
:::
View migration metrics
When enabled, Sippy exposes metrics that help you understand the progress of your ongoing migrations.
| Metric | Description |
|---|---|
| Requests served by Sippy | The percentage of overall requests served by R2 over a period of time. A higher percentage indicates that fewer requests need to be made to the source bucket. |
| Data migrated by Sippy | The amount of data that has been copied from the source bucket to R2 over a period of time. Reported in bytes. |
To view current and historical metrics:
1. In the Cloudflare dashboard, go to the **R2 object storage** page. 2. Select your bucket. 3. Select the **Metrics** tab.You can optionally select a time window to query. This defaults to the last 24 hours.
Disable Sippy on your R2 bucket
Dashboard
1. In the Cloudflare dashboard, go to the **R2 object storage** page. 2. Select the bucket you'd like to disable Sippy for. 3. Switch to the **Settings** tab and scroll down to the **On Demand Migration** card. 4. Press **Disable**.Wrangler
To disable Sippy, run the r2 bucket sippy disable command:
npx wrangler r2 bucket sippy disable "<BUCKET_NAME>"
API
For more information on required parameters and examples of how to disable Sippy, refer to the API documentation.
Supported cloud storage providers
Cloudflare currently supports copying data from the following cloud object storage providers to R2:
- Amazon S3
- Google Cloud Storage (GCS)
- Azure Blob Storage
- S3-compatible storage providers
R2 API interactions
When Sippy is enabled, it changes the behavior of certain actions on your R2 bucket across Workers, S3 API, and public buckets.
| Action | New behavior |
|---|---|
| GetObject | Calls to GetObject will first attempt to retrieve the object from your R2 bucket. If the object is not present, the object will be served from the source storage bucket and simultaneously uploaded to the requested R2 bucket. Additional considerations:
|
| HeadObject | Behaves similarly to GetObject, but only retrieves object metadata. Will not copy objects to the requested R2 bucket. |
| PutObject | No change to behavior. Calls to PutObject will add objects to the requested R2 bucket. |
| DeleteObject | No change to behavior. Calls to DeleteObject will delete objects in the requested R2 bucket. Additional considerations:
|
Actions not listed above have no change in behavior. For more information, refer to Workers API reference or S3 API compatibility.
Create credentials for storage providers
Amazon S3
To copy objects from Amazon S3, Sippy requires access permissions to your bucket. While you can use any AWS Identity and Access Management (IAM) user credentials with the correct permissions, Cloudflare recommends you create a user with a narrow set of permissions.
To create credentials with the correct permissions:
1. Log in to your AWS IAM account. 2. Create a policy with the following format and replace `` with the bucket you want to grant access to: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:ListBucket*", "s3:GetObject*"], "Resource": [ "arn:aws:s3:::", "arn:aws:s3:::/*" ] } ] } ``` 3. Create a new user and attach the created policy to that user.You can now use both the Access Key ID and Secret Access Key when enabling Sippy.
Google Cloud Storage
To copy objects from Google Cloud Storage (GCS), Sippy requires access permissions to your bucket. Cloudflare recommends using the Google Cloud predefined Storage Object Viewer role.
To create credentials with the correct permissions:
1. Log in to your Google Cloud console. 2. Go to **IAM & Admin** > **Service Accounts**. 3. Create a service account with the predefined `Storage Object Viewer` role. 4. Go to the **Keys** tab of the service account you created. 5. Select **Add Key** > **Create a new key** and download the JSON key file.You can now use this JSON key file when enabling Sippy via Wrangler or API.
Azure Blob Storage
To copy objects from Azure Blob Storage, Sippy requires the name of your Azure Storage account, the container to copy from, and either an account key or a shared access signature (SAS) token. Provide exactly one of the two credential types. Sippy needs read and list permissions on the container.
To use an account key:
1. Log in to the Azure portal and go to your storage account. 2. Under **Security + networking**, select **Access keys**. 3. Copy your storage account name and one of the listed keys.To use a SAS token instead, Cloudflare recommends scoping it to only the container you are migrating:
1. Log in to the Azure portal and go to your storage account. 2. Under **Data storage**, select **Containers**, then open the container you want to migrate from. 3. Select **Shared access tokens**, grant **Read** and **List** permissions, then select **Generate SAS token and URL**. 4. Copy the generated SAS token.You can now use the account name, container name, and either the account key or SAS token when enabling Sippy.
S3-compatible storage
To copy objects from an S3-compatible storage provider, Sippy requires the S3 API endpoint URL for your bucket, along with an Access Key ID and Secret Access Key that can read from it. Cloudflare recommends scoping these credentials to only allow reads from the bucket you are migrating.
Refer to the documentation for your storage provider to find the S3 API endpoint for your bucket and to create read-only access credentials.
You can now use the bucket URL, Access Key ID, and Secret Access Key when enabling Sippy.
Caveats
ETags
<Render file="migrator-etag-caveat" params={{ one: "Sippy" }} product="r2" />
Super Slurper
import { InlineBadge, Render, DashButton } from "~/components";
Super Slurper allows you to quickly and easily copy objects from other cloud providers to an R2 bucket of your choice.
Migration jobs:
- Preserve custom object metadata from source bucket by copying them on the migrated objects on R2.
- Do not delete any objects from source bucket.
- Use TLS encryption over HTTPS connections for safe and private object transfers.
When to use Super Slurper
Using Super Slurper as part of your strategy can be a good choice if the cloud storage bucket you are migrating consists primarily of objects less than 1 TB. Objects greater than 1 TB will be skipped and need to be copied separately.
For migration use cases that do not meet the above criteria, we recommend using tools such as rclone.
Use Super Slurper to migrate data to R2
-
In the Cloudflare dashboard, go to the R2 data migration page.
-
Select Migrate files.
-
Select the source cloud storage provider that you will be migrating data from.
-
Enter your source bucket name and associated credentials and select Next.
-
Enter your R2 bucket name and associated credentials and select Next.
-
After you finish reviewing the details of your migration, select Migrate files.
You can view the status of your migration job at any time by selecting your migration from Data Migration page.
Source bucket options
Bucket sub path (optional)
This setting specifies the prefix within the source bucket where objects will be copied from.
Destination R2 bucket options
Overwrite files?
This setting determines what happens when an object being copied from the source storage bucket matches the path of an existing object in the destination R2 bucket. There are two options:
- Overwrite (default)
- Skip
Supported cloud storage providers
Cloudflare currently supports copying data from the following cloud object storage providers to R2:
- Amazon S3
- Cloudflare R2
- Google Cloud Storage (GCS)
- All S3-compatible storage providers
Tested S3-compatible storage providers
The following S3-compatible storage providers have been tested and verified to work with Super Slurper:
- Backblaze B2
- DigitalOcean Spaces
- Scaleway Object Storage
- Wasabi Cloud Object Storage
Super Slurper should support transfers from all S3-compatible storage providers, but the ones listed have been explicitly tested.
:::note
Have you tested and verified another S3-compatible provider? Open a pull request or create a GitHub issue.
:::
Create credentials for storage providers
Amazon S3
To copy objects from Amazon S3, Super Slurper requires access permissions to your S3 bucket. While you can use any AWS Identity and Access Management (IAM) user credentials with the correct permissions, Cloudflare recommends you create a user with a narrow set of permissions.
To create credentials with the correct permissions:
- Log in to your AWS IAM account.
- Create a policy with the following format and replace
<BUCKET_NAME>with the bucket you want to grant access to:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:Get*", "s3:List*"],
"Resource": ["arn:aws:s3:::<BUCKET_NAME>", "arn:aws:s3:::<BUCKET_NAME>/*"]
}
]
}
- Create a new user and attach the created policy to that user.
You can now use both the Access Key ID and Secret Access Key when defining your source bucket.
Google Cloud Storage
To copy objects from Google Cloud Storage (GCS), Super Slurper requires access permissions to your GCS bucket. You can use the Google Cloud predefined Storage Admin role, but Cloudflare recommends creating a custom role with a narrower set of permissions.
To create a custom role with the necessary permissions:
- Log in to your Google Cloud console.
- Go to IAM & Admin > Roles.
- Find the
Storage Object Viewerrole and select Create role from this role. - Give your new role a name.
- Select Add permissions and add the
storage.buckets.getpermission. - Select Create.
To create credentials with your custom role:
- Log in to your Google Cloud console.
- Go to IAM & Admin > Service Accounts.
- Create a service account with the your custom role.
- Go to the Keys tab of the service account you created.
- Select Add Key > Create a new key and download the JSON key file.
You can now use this JSON key file when enabling Super Slurper.
Caveats
ETags
<Render file="migrator-etag-caveat" params={{ one: "Super Slurper" }} product="r2" />
Archive storage classes
Objects stored using AWS S3 archival storage classes will be skipped and need to be copied separately. Specifically:
- Files stored using S3 Glacier tiers (not including Glacier Instant Retrieval) will be skipped and logged in the migration log.
- Files stored using S3 Intelligent Tiering and placed in Deep Archive tier will be skipped and logged in the migration log.
Authenticate against R2 API using auth tokens
import { PackageManagers, Tabs, TabItem } from '~/components';
The following example shows how to authenticate against R2 using the S3 API and an API token.
:::note For providing secure access to bucket objects for anonymous users, we recommend using pre-signed URLs instead.
Pre-signed URLs do not require users to be a member of your organization and enable direct programmatic access to R2. :::
Ensure you have set the following environment variables prior to running either example. Refer to Authentication for more information.
export AWS_REGION=auto
export AWS_ENDPOINT_URL=https://<account_id>.r2.cloudflarestorage.com
export AWS_ACCESS_KEY_ID=your_access_key_id
export AWS_SECRET_ACCESS_KEY=your_secret_access_key
Install the `@aws-sdk/client-s3` package for the S3 API:
<PackageManagers pkg="@aws-sdk/client-s3" />
Run the following Node.js script with `node index.js`. Ensure you change `Bucket` to the name of your bucket, and `Key` to point to an existing file in your R2 bucket.
Note, tutorial below should function for TypeScript as well.
```javascript title="index.js"
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
const s3 = new S3Client();
const Bucket = "<YOUR_BUCKET_NAME>";
const Key = "pfp.jpg";
const object = await s3.send(
new GetObjectCommand({
Bucket,
Key,
}),
);
console.log("Successfully fetched the object", object.$metadata);
// Process the data as needed
// For example, to get the content as a Buffer:
// const content = data.Body;
// Or to save the file (requires 'fs' module):
// import { writeFile } from "node:fs/promises";
// await writeFile('ingested_0001.parquet', data.Body);
```
Install the `boto3` S3 API client:
```sh
pip install boto3
```
Run the following Python script with `python3 get_r2_object.py`. Ensure you change `bucket` to the name of your bucket, and `object_key` to point to an existing file in your R2 bucket.
```python title="get_r2_object.py"
import boto3
from botocore.client import Config
# Configure the S3 client for Cloudflare R2
s3_client = boto3.client('s3',
config=Config(signature_version='s3v4')
)
# Specify the object key
#
bucket = '<YOUR_BUCKET_NAME>'
object_key = '2024/08/02/ingested_0001.parquet'
try:
# Fetch the object
response = s3_client.get_object(Bucket=bucket, Key=object_key)
print('Successfully fetched the object')
# Process the response content as needed
# For example, to read the content:
# object_content = response['Body'].read()
# Or to save the file:
# with open('ingested_0001.parquet', 'wb') as f:
# f.write(response['Body'].read())
except Exception as e:
print(f'Failed to fetch the object. Error: {str(e)}')
</TabItem>
<TabItem label="Go" icon="seti:go">
Use `go get` to add the `aws-sdk-go-v2` packages to your Go project:
```sh
go get github.com/aws/aws-sdk-go-v2
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/credentials
go get github.com/aws/aws-sdk-go-v2/service/s3
```
Run the following Go application as a script with `go run main.go`. Ensure you change `bucket` to the name of your bucket, and `objectKey` to point to an existing file in your R2 bucket.
```go
package main
import (
"context"
"fmt"
"io"
"log"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
func main() {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
log.Fatalf("Unable to load SDK config, %v", err)
}
// Create an S3 client
client := s3.NewFromConfig(cfg)
// Specify the object key
bucket := "<YOUR_BUCKET_NAME>"
objectKey := "pfp.jpg"
// Fetch the object
output, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(objectKey),
})
if err != nil {
log.Fatalf("Unable to fetch object, %v", err)
}
defer output.Body.Close()
fmt.Println("Successfully fetched the object")
// Process the object content as needed
// For example, to save the file:
// file, err := os.Create("ingested_0001.parquet")
// if err != nil {
// log.Fatalf("Unable to create file, %v", err)
// }
// defer file.Close()
// _, err = io.Copy(file, output.Body)
// if err != nil {
// log.Fatalf("Unable to write file, %v", err)
// }
// Or to read the content:
content, err := io.ReadAll(output.Body)
if err != nil {
log.Fatalf("Unable to read object content, %v", err)
}
fmt.Printf("Object content length: %d bytes\n", len(content))
}
```
</TabItem>
</Tabs>
Authenticate against R2 with temporary credentials
import { PackageManagers, Tabs, TabItem } from "~/components";
The following examples show how to generate R2 temporary credentials via both the Temporary Credentials API and local client-side signing, and how to use the resulting credentials with an S3 client.
Prerequisites
- A parent R2 API token with at least the permissions you plan to delegate. Never ship parent credentials to a client.
- Your Cloudflare account ID.
- An S3 client that supports session tokens. The examples below use aws4fetch.
Generate via the Temporary Credentials API
Call the Temporary Credentials API from a trusted server, then use the returned credentials with any S3 client.
curl https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/r2/temp-access-credentials \
--header "Authorization: Bearer <PARENT_API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"bucket": "my-bucket",
"parentAccessKeyId": "<PARENT_ACCESS_KEY_ID>",
"permission": "object-read-only",
"ttlSeconds": 900,
"objects": ["reports/2026-q1.pdf"]
}'
The response wraps the credentials in a result object:
{
"result": {
"accessKeyId": "<accessKeyId>",
"secretAccessKey": "<secretAccessKey>",
"sessionToken": "<sessionToken>"
},
"errors": [],
"messages": [],
"success": true
}
Generate locally (client-side signing)
This example uses jose to sign the JWT and aws4fetch to issue signed requests.
The following helper signs a JWT with your parent secret access key and derives the temporary secret access key and session token:
import { SignJWT } from "jose";
type R2Scope =
| "object-read-only"
| "object-read-write"
| "admin-read-only"
| "admin-read-write";
// Permitted operations: set exactly one of `scope` or `actions`, never both.
type PermittedOperations =
| { scope: R2Scope; actions?: never }
| { actions: [string, ...string[]]; scope?: never };
export type TempCredentialOptions = PermittedOperations & {
// Time-to-live in seconds. Defaults to 1 hour.
ttlSeconds?: number;
// Optional: restrict access to specific prefixes or objects.
paths?: { prefixPaths?: string[]; objectPaths?: string[] };
};
export async function createTempCredentials(
endpoint: string,
accountId: string,
parentAccessKeyId: string,
parentSecretAccessKey: string,
bucket: string,
opts: TempCredentialOptions,
): Promise<{
accessKeyId: string;
secretAccessKey: string;
sessionToken: string;
}> {
const ttl = opts.ttlSeconds ?? 3600;
const claims: Record<string, unknown> = { bucket };
if (opts.actions !== undefined) {
claims.actions = opts.actions;
} else {
claims.scope = opts.scope;
}
if (opts.paths !== undefined) {
claims.paths = {
prefixPaths: opts.paths.prefixPaths ?? [],
objectPaths: opts.paths.objectPaths ?? [],
};
}
// Sign the JWT with the parent secret access key. R2 validates this signature.
const jwt = await new SignJWT(claims)
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setSubject(accountId)
.setIssuer(parentAccessKeyId)
.setAudience(new URL(endpoint).host)
.setIssuedAt()
.setExpirationTime(`${ttl}s`)
.sign(new TextEncoder().encode(parentSecretAccessKey));
// The temporary secret access key is the SHA-256 hex digest of the signed JWT.
const digest = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(jwt),
);
const secretAccessKey = Array.from(new Uint8Array(digest))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return {
// Reuse the parent access key ID as the temporary access key ID.
accessKeyId: parentAccessKeyId,
secretAccessKey,
// The session token is base64("jwt/" + signed JWT).
sessionToken: btoa(`jwt/${jwt}`),
};
}
The following example returns a credential that is valid for 15 minutes and can only GetObject and HeadObject under the data/ prefix:
import { createTempCredentials } from "./temp-credentials";
const R2_URL = `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`;
const creds = await createTempCredentials(
R2_URL,
ACCOUNT_ID,
PARENT_ACCESS_KEY_ID,
PARENT_SECRET_ACCESS_KEY,
"my-bucket",
{
actions: ["GetObject", "HeadObject"],
ttlSeconds: 900,
paths: { prefixPaths: ["data/"] },
},
);
Use the credentials
Once you have a temporary credential, usage is the same regardless of how it was generated. Pass the three values to your S3 client and issue requests. The following example uses credentials scoped to the data/ prefix to demonstrate one permitted and one rejected request:
import { AwsClient } from "aws4fetch";
const R2_URL = `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`;
const client = new AwsClient({
accessKeyId: ACCESS_KEY_ID,
secretAccessKey: SECRET_ACCESS_KEY,
sessionToken: SESSION_TOKEN,
service: "s3",
});
// Allowed: object under the data/ prefix.
const ok = await client.fetch(`${R2_URL}/my-bucket/data/file.bin`);
console.log(ok.status); // 200
// Rejected with 403 AccessDenied because the object is outside the data/ prefix.
const denied = await client.fetch(`${R2_URL}/my-bucket/other/file.bin`);
console.log(denied.status); // 403
Related resources
- Temporary credentials: concept reference and scoping model.
- R2 API tokens: create the parent token.
- Error codes: authentication error reference.
Use the Cache API
Use the Cache API to store R2 objects in Cloudflare's cache.
:::note
You will need to connect a custom domain or route to your Worker in order to use the Cache API. Cache API operations in the Cloudflare Workers dashboard editor, Playground previews, and any *.workers.dev deployments will have no impact.
:::
export default {
async fetch(request, env, context) {
try {
const url = new URL(request.url);
// Construct the cache key from the cache URL
const cacheKey = new Request(url.toString(), request);
const cache = caches.default;
// Check whether the value is already available in the cache
// if not, you will need to fetch it from R2, and store it in the cache
// for future access
let response = await cache.match(cacheKey);
if (response) {
console.log(`Cache hit for: ${request.url}.`);
return response;
}
console.log(
`Response for request url: ${request.url} not present in cache. Fetching and caching request.`
);
// If not in cache, get it from R2
const objectKey = url.pathname.slice(1);
const object = await env.MY_BUCKET.get(objectKey);
if (object === null) {
return new Response('Object Not Found', { status: 404 });
}
// Set the appropriate object headers
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
// Cache API respects Cache-Control headers. Setting s-max-age to 10
// will limit the response to be in cache for 10 seconds max
// Any changes made to the response here will be reflected in the cached value
headers.append('Cache-Control', 's-maxage=10');
response = new Response(object.body, {
headers,
});
// Store the fetched response as cacheKey
// Use waitUntil so you can return the response without blocking on
// writing to cache
context.waitUntil(cache.put(cacheKey, response.clone()));
return response;
} catch (e) {
return new Response('Error thrown ' + e.message);
}
},
};
Examples
import { DirectoryListing, GlossaryTooltip } from "~/components";
Explore the following examples of how to use SDKs and other tools with R2.
Rclone
import { Render, Steps } from "~/components";
Rclone is a command-line tool which manages files on cloud storage. You can use rclone to upload objects to R2 concurrently.
Configure rclone
With rclone installed, you may run rclone config to configure a new S3 storage provider. You will be prompted with a series of questions for the new provider details.
:::note[Recommendation] It is recommended that you choose a unique provider name and then rely on all default answers to the prompts.
This will create a rclone configuration file, which you can then modify with the preset configuration given below.
:::
:::note
Ensure you are running rclone v1.59 or greater (rclone downloads). Versions prior to v1.59 may return HTTP 401: Unauthorized errors, as earlier versions of rclone do not strictly align to the S3 specification in all cases.
:::
Edit an existing rclone configuration
If you have already configured rclone in the past, you may run rclone config file to print the location of your rclone configuration file:
rclone config file
# Configuration file is stored at:
# ~/.config/rclone/rclone.conf
Then use an editor (nano or vim, for example) to add or edit the new provider. This example assumes you are adding a new r2 provider:
[r2]
type = s3
provider = Cloudflare
access_key_id = abc123
secret_access_key = xyz456
endpoint = https://<accountid>.r2.cloudflarestorage.com
acl = private
:::note
If you are using a token with Object-level permissions, you will need to add no_check_bucket = true to the configuration to avoid errors.
:::
You may then use the new rclone provider for any of your normal workflows.
List buckets & objects
The rclone tree command can be used to list the contents of the remote, in this case Cloudflare R2.
rclone tree r2:
# /
# ├── user-uploads
# │ └── foobar.png
# └── my-bucket-name
# ├── cat.png
# └── todos.txt
rclone tree r2:my-bucket-name
# /
# ├── cat.png
# └── todos.txt
Upload and retrieve objects
The rclone copy command can be used to upload objects to an R2 bucket and vice versa - this allows you to upload files up to the 5 TB maximum object size that R2 supports.
# Upload dog.txt to the user-uploads bucket
rclone copy dog.txt r2:user-uploads/
rclone tree r2:user-uploads
# /
# ├── foobar.png
# └── dog.txt
# Download dog.txt from the user-uploads bucket
rclone copy r2:user-uploads/dog.txt .
A note about multipart upload part sizes
For multipart uploads, part sizes can significantly affect the number of Class A operations that are used, which can alter how much you end up being charged.
Every part upload counts as a separate operation, so larger part sizes will use fewer operations, but might be costly to retry if the upload fails. Also consider that a multipart upload is always going to consume at least 3 times as many operations as a single PutObject, because it will include at least one CreateMultipartUpload, UploadPart & CompleteMultipartUpload operations.
Balancing part size depends heavily on your use-case, but these factors can help you minimize your bill, so they are worth thinking about.
You can configure rclone's multipart upload part size using the --s3-chunk-size CLI argument. Note that you might also have to adjust the --s3-upload-cutoff argument to ensure that rclone is using multipart uploads. Both of these can be set in your configuration file as well. Generally, --s3-upload-cutoff will be no less than --s3-chunk-size.
rclone copy long-video.mp4 r2:user-uploads/ --s3-upload-cutoff=100M --s3-chunk-size=100M
Generate presigned URLs
You can also generate presigned links which allow you to share public access to a file temporarily using the rclone link command.
# You can pass the --expire flag to determine how long the presigned link is valid. The --unlink flag isn't supported by R2.
rclone link r2:my-bucket-name/cat.png --expire 3600
# https://<accountid>.r2.cloudflarestorage.com/my-bucket-name/cat.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=<signature>
Use SSE-C
import { Tabs, TabItem } from "~/components";
The following tutorial shows some snippets for how to use Server-Side Encryption with Customer-Provided Keys (SSE-C) on R2.
Before you begin
- When using SSE-C, make sure you store your encryption key(s) in a safe place. In the event you misplace them, Cloudflare will be unable to recover the body of any objects encrypted using those keys.
- While SSE-C does provide MD5 hashes, this hash can be used for identification of keys only. The MD5 hash is not used in the encryption process itself.
Workers
```typescript interface Environment { R2: R2Bucket /** * In this example, your SSE-C is stored as a hexadecimal string (preferably a secret). * The R2 API also supports providing an ArrayBuffer directly, if you want to generate/ * store your keys dynamically. */ SSEC_KEY: string } export default { async fetch(req: Request, env: Env) { const { SSEC_KEY, R2 } = env; const { pathname: filename } = new URL(req.url); switch(req.method) { case "GET": { const maybeObj = await env.BUCKET.get(filename, { onlyIf: req.headers, ssecKey: SSEC_KEY, }); if(!maybeObj) { return new Response("Not Found", { status: 404 }); } const headers = new Headers(); maybeObj.writeHttpMetadata(headers); return new Response(body, { headers }); } case 'POST': { const multipartUpload = await env.BUCKET.createMultipartUpload(filename, { httpMetadata: req.headers, ssecKey: SSEC_KEY, }); /** * This example only provides a single-part "multipart" upload. * For multiple parts, the process is the same(the key must be provided) * for every part. */ const partOne = await multipartUpload.uploadPart(1, req.body, ssecKey); const obj = await multipartUpload.complete([partOne]); const headers = new Headers(); obj.writeHttpMetadata(headers); return new Response(null, { headers, status: 201 }); } case 'PUT': { const obj = await env.BUCKET.put(filename, req.body, { httpMetadata: req.headers, ssecKey: SSEC_KEY, }); const headers = new Headers(); maybeObj.writeHttpMetadata(headers); return new Response(null, { headers, status: 201 }); } default: { return new Response("Method not allowed", { status: 405 }); } } } } ``` ```javascript /** * In this example, your SSE-C is stored as a hexadecimal string(preferably a secret). * The R2 API also supports providing an ArrayBuffer directly, if you want to generate/ * store your keys dynamically. */ export default { async fetch(req, env) { const { SSEC_KEY, R2 } = env; const { pathname: filename } = new URL(req.url); switch(req.method) { case "GET": { const maybeObj = await env.BUCKET.get(filename, { onlyIf: req.headers, ssecKey: SSEC_KEY, }); if(!maybeObj) { return new Response("Not Found", { status: 404 }); } const headers = new Headers(); maybeObj.writeHttpMetadata(headers); return new Response(body, { headers }); } case 'POST': { const multipartUpload = await env.BUCKET.createMultipartUpload(filename, { httpMetadata: req.headers, ssecKey: SSEC_KEY, }); /** * This example only provides a single-part "multipart" upload. * For multiple parts, the process is the same(the key must be provided) * for every part. */ const partOne = await multipartUpload.uploadPart(1, req.body, ssecKey); const obj = await multipartUpload.complete([partOne]); const headers = new Headers(); obj.writeHttpMetadata(headers); return new Response(null, { headers, status: 201 }); } case 'PUT': { const obj = await env.BUCKET.put(filename, req.body, { httpMetadata: req.headers, ssecKey: SSEC_KEY, }); const headers = new Headers(); maybeObj.writeHttpMetadata(headers); return new Response(null, { headers, status: 201 }); } default: { return new Response("Method not allowed", { status: 405 }); } } } } ```S3-API
```typescript import { UploadPartCommand, PutObjectCommand, S3Client, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, type UploadPartCommandOutput } from "@aws-sdk/client-s3"; const s3 = new S3Client({
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
},
});
const SSECustomerAlgorithm = "AES256";
const SSECustomerKey = process.env.R2_SSEC_KEY;
const SSECustomerKeyMD5 = process.env.R2_SSEC_KEY_MD5;
await s3.send(
new PutObjectCommand({
Bucket: "your-bucket",
Key: "single-part",
Body: "BeepBoop",
SSECustomerAlgorithm,
SSECustomerKey,
SSECustomerKeyMD5,
}),
);
const multi = await s3.send(
new CreateMultipartUploadCommand({
Bucket: "your-bucket",
Key: "multi-part",
SSECustomerAlgorithm,
SSECustomerKey,
SSECustomerKeyMD5,
}),
);
const UploadId = multi.UploadId;
const parts: UploadPartCommandOutput[] = [];
parts.push(
await s3.send(
new UploadPartCommand({
Bucket: "your-bucket",
Key: "multi-part",
UploadId,
// filledBuf()` generates some random data.
// Replace with a function/body of your choice.
Body: filledBuf(),
PartNumber: 1,
SSECustomerAlgorithm,
SSECustomerKey,
SSECustomerKeyMD5,
}),
),
);
parts.push(
await s3.send(
new UploadPartCommand({
Bucket: "your-bucket",
Key: "multi-part",
UploadId,
// filledBuf()` generates some random data.
// Replace with a function/body of your choice.
Body: filledBuf(),
PartNumber: 2,
SSECustomerAlgorithm,
SSECustomerKey,
SSECustomerKeyMD5,
}),
),
);
await s3.send(
new CompleteMultipartUploadCommand({
Bucket: "your-bucket",
Key: "multi-part",
UploadId,
MultipartUpload: {
Parts: parts.map(({ ETag }, PartNumber) => ({
ETag,
PartNumber: PartNumber + 1,
})),
},
SSECustomerAlgorithm,
SSECustomerKey,
SSECustomerKeyMD5,
}),
);
const HeadObjectOutput = await s3.send(
new HeadObjectCommand({
Bucket: "your-bucket",
Key: "multi-part",
SSECustomerAlgorithm,
SSECustomerKey,
SSECustomerKeyMD5,
}),
);
const GetObjectOutput = await s3.send(
new GetObjectCommand({
Bucket: "your-bucket",
Key: "single-part",
SSECustomerAlgorithm,
SSECustomerKey,
SSECustomerKeyMD5,
}),
);
```
Terraform (AWS)
import { Render } from "~/components";
This example shows how to configure R2 with Terraform using the AWS provider.
:::note[Note for using AWS provider]
For using only the Cloudflare provider, see Terraform.
:::
With terraform installed:
- Create
main.tffile, or edit your existing Terraform configuration - Populate the endpoint URL at
endpoints.s3with your Cloudflare account ID - Populate
access_keyandsecret_keywith the corresponding R2 API credentials. - Ensure that
skip_region_validation = true,skip_requesting_account_id = true, andskip_credentials_validation = trueare set in the provider configuration.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5"
}
}
}
provider "aws" {
region = "us-east-1"
access_key = <R2 Access Key>
secret_key = <R2 Secret Key>
# Required for R2.
# These options disable S3-specific validation on the client (Terraform) side.
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
endpoints {
s3 = "https://<account id>.r2.cloudflarestorage.com"
}
}
resource "aws_s3_bucket" "default" {
bucket = "<org>-test"
}
resource "aws_s3_bucket_cors_configuration" "default" {
bucket = aws_s3_bucket.default.id
cors_rule {
allowed_methods = ["GET"]
allowed_origins = ["*"]
}
}
resource "aws_s3_bucket_lifecycle_configuration" "default" {
bucket = aws_s3_bucket.default.id
rule {
id = "expire-bucket"
status = "Enabled"
expiration {
days = 1
}
}
rule {
id = "abort-multipart-upload"
status = "Enabled"
abort_incomplete_multipart_upload {
days_after_initiation = 1
}
}
}
You can then use terraform plan to view the changes and terraform apply to apply changes.
Terraform
import { Render } from "~/components"
This example shows how to configure R2 with Terraform using the Cloudflare provider.
:::note[Note for using AWS provider]
When using the Cloudflare Terraform provider, you can only manage buckets. To configure items such as CORS and object lifecycles, you will need to use the AWS Provider.
:::
With terraform installed, create main.tf and copy the content below replacing with your API Token.
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4"
}
}
}
provider "cloudflare" {
api_token = "<YOUR_API_TOKEN>"
}
resource "cloudflare_r2_bucket" "cloudflare-bucket" {
account_id = "<YOUR_ACCOUNT_ID>"
name = "my-tf-test-bucket"
location = "WEUR"
}
You can then use terraform plan to view the changes and terraform apply to apply changes.
aws CLI
import { Render } from "~/components";
With the aws CLI installed, you may run aws configure to configure a new profile. You will be prompted with a series of questions for the new profile's details.
aws configure
AWS Access Key ID [None]: <ACCESS_KEY_ID>
AWS Secret Access Key [None]: <SECRET_ACCESS_KEY>
Default region name [None]: auto
Default output format [None]: json
The region value can be set to auto since it is required by the SDK but not used by R2.
You may then use the aws CLI for any of your normal workflows.
# Provide your Cloudflare account ID
aws s3api list-buckets --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com
# {
# "Buckets": [
# {
# "Name": "my-bucket",
# "CreationDate": "2022-05-18T17:19:59.645000+00:00"
# }
# ],
# "Owner": {
# "DisplayName": "134a5a2c0ba47b38eada4b9c8ead10b6",
# "ID": "134a5a2c0ba47b38eada4b9c8ead10b6"
# }
# }
aws s3api list-objects-v2 --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com --bucket my-bucket
# {
# "Contents": [
# {
# "Key": "ferriswasm.png",
# "LastModified": "2022-05-18T17:20:21.670000+00:00",
# "ETag": "\"eb2b891dc67b81755d2b726d9110af16\"",
# "Size": 87671,
# "StorageClass": "STANDARD"
# }
# ]
# }
Generate presigned URLs
You can also generate presigned links which allow you to share public access to a file temporarily.
# You can pass the --expires-in flag to determine how long the presigned link is valid.
aws s3 presign --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com s3://my-bucket/ferriswasm.png --expires-in 3600
# https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/ferriswasm.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=<signature>
aws-sdk-go
import { Render } from "~/components";
This example uses version 2 of the aws-sdk-go package. You must pass in the R2 configuration credentials when instantiating your S3 service client:
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"log"
)
func main() {
var bucketName = "sdk-example"
// Provide your Cloudflare account ID
var accountId = "<ACCOUNT_ID>"
// Retrieve your S3 API credentials for your R2 bucket via API tokens
// (see: https://developers.cloudflare.com/r2/api/tokens)
var accessKeyId = "<ACCESS_KEY_ID>"
var accessKeySecret = "<SECRET_ACCESS_KEY>"
cfg, err := config.LoadDefaultConfig(context.TODO(),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKeyId, accessKeySecret, "")),
config.WithRegion("auto"), // Required by SDK but not used by R2
)
if err != nil {
log.Fatal(err)
}
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String(fmt.Sprintf("https://%s.r2.cloudflarestorage.com", accountId))
})
listObjectsOutput, err := client.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{
Bucket: &bucketName,
})
if err != nil {
log.Fatal(err)
}
for _, object := range listObjectsOutput.Contents {
obj, _ := json.MarshalIndent(object, "", "\t")
fmt.Println(string(obj))
}
// {
// "ChecksumAlgorithm": null,
// "ETag": "\"eb2b891dc67b81755d2b726d9110af16\"",
// "Key": "ferriswasm.png",
// "LastModified": "2022-05-18T17:20:21.67Z",
// "Owner": null,
// "Size": 87671,
// "StorageClass": "STANDARD"
// }
listBucketsOutput, err := client.ListBuckets(context.TODO(), &s3.ListBucketsInput{})
if err != nil {
log.Fatal(err)
}
for _, object := range listBucketsOutput.Buckets {
obj, _ := json.MarshalIndent(object, "", "\t")
fmt.Println(string(obj))
}
// {
// "CreationDate": "2022-05-18T17:19:59.645Z",
// "Name": "sdk-example"
// }
}
Generate presigned URLs
You can also generate presigned links that can be used to temporarily share public write access to a bucket.
presignClient := s3.NewPresignClient(client)
presignResult, err := presignClient.PresignPutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String("example.txt"),
})
if err != nil {
panic("Couldn't get presigned URL for PutObject")
}
fmt.Printf("Presigned URL For object: %s\n", presignResult.URL)
aws-sdk-java
import { Render } from "~/components";
This example uses version 2 of the aws-sdk-java package. You must pass in the R2 configuration credentials when instantiating your S3 service client.
:::note
You must set chunkedEncodingEnabled(false) in the S3Configuration when building your client. The AWS SDK for Java v2 uses chunked transfer encoding by default for putObject requests, which causes a signature mismatch error (HTTP 403) with R2. Disabling chunked encoding ensures the request signature is calculated correctly.
:::
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.core.sync.RequestBody;
import java.net.URI;
import java.util.List;
/**
* Client for interacting with Cloudflare R2 Storage using AWS SDK S3 compatibility
*/
public class CloudflareR2Client {
private final S3Client s3Client;
/**
* Creates a new CloudflareR2Client with the provided configuration
*/
public CloudflareR2Client(S3Config config) {
this.s3Client = buildS3Client(config);
}
/**
* Configuration class for R2 credentials and endpoint
* - accountId: Your Cloudflare account ID
* - accessKey: Your R2 Access Key ID (see: https://developers.cloudflare.com/r2/api/tokens)
* - secretKey: Your R2 Secret Access Key (see: https://developers.cloudflare.com/r2/api/tokens)
*/
public static class S3Config {
private final String accountId;
private final String accessKey;
private final String secretKey;
private final String endpoint;
public S3Config(String accountId, String accessKey, String secretKey) {
this.accountId = accountId;
this.accessKey = accessKey;
this.secretKey = secretKey;
this.endpoint = String.format("https://%s.r2.cloudflarestorage.com", accountId);
}
public String getAccessKey() { return accessKey; }
public String getSecretKey() { return secretKey; }
public String getEndpoint() { return endpoint; }
}
/**
* Builds and configures the S3 client with R2-specific settings
*/
private static S3Client buildS3Client(S3Config config) {
AwsBasicCredentials credentials = AwsBasicCredentials.create(
config.getAccessKey(),
config.getSecretKey()
);
S3Configuration serviceConfiguration = S3Configuration.builder()
.pathStyleAccessEnabled(true)
.chunkedEncodingEnabled(false)
.build();
return S3Client.builder()
.endpointOverride(URI.create(config.getEndpoint()))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.of("auto")) // Required by SDK but not used by R2
.serviceConfiguration(serviceConfiguration)
.build();
}
/**
* Lists all buckets in the R2 storage
*/
public List<Bucket> listBuckets() {
try {
return s3Client.listBuckets().buckets();
} catch (S3Exception e) {
throw new RuntimeException("Failed to list buckets: " + e.getMessage(), e);
}
}
/**
* Lists all objects in the specified bucket
*/
public List<S3Object> listObjects(String bucketName) {
try {
ListObjectsV2Request request = ListObjectsV2Request.builder()
.bucket(bucketName)
.build();
return s3Client.listObjectsV2(request).contents();
} catch (S3Exception e) {
throw new RuntimeException("Failed to list objects in bucket " + bucketName + ": " + e.getMessage(), e);
}
}
/**
* Uploads an object to the specified bucket
*/
public void putObject(String bucketName, String key, String content) {
try {
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
s3Client.putObject(request, RequestBody.fromString(content));
} catch (S3Exception e) {
throw new RuntimeException("Failed to put object " + key + " in bucket " + bucketName + ": " + e.getMessage(), e);
}
}
public static void main(String[] args) {
S3Config config = new S3Config(
"<ACCOUNT_ID>",
"<ACCESS_KEY_ID>",
"<SECRET_ACCESS_KEY>"
);
CloudflareR2Client r2Client = new CloudflareR2Client(config);
// List buckets
System.out.println("Available buckets:");
r2Client.listBuckets().forEach(bucket ->
System.out.println("* " + bucket.name())
);
// Upload an object to a bucket
String bucketName = "demos";
r2Client.putObject(bucketName, "example.txt", "Hello, R2!");
System.out.println("Uploaded example.txt to bucket '" + bucketName + "'");
// List objects in a specific bucket
System.out.println("\nObjects in bucket '" + bucketName + "':");
r2Client.listObjects(bucketName).forEach(object ->
System.out.printf("* %s (size: %d bytes, modified: %s)%n",
object.key(),
object.size(),
object.lastModified())
);
}
}
Generate presigned URLs
You can also generate presigned links that can be used to temporarily share public write access to a bucket.
// import required packages for presigning
// Rest of the packages are same as above
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
import java.time.Duration;
public class CloudflareR2Client {
private final S3Client s3Client;
private final S3Presigner presigner;
/**
* Creates a new CloudflareR2Client with the provided configuration
*/
public CloudflareR2Client(S3Config config) {
this.s3Client = buildS3Client(config);
this.presigner = buildS3Presigner(config);
}
/**
* Builds and configures the S3 presigner with R2-specific settings
*/
private static S3Presigner buildS3Presigner(S3Config config) {
AwsBasicCredentials credentials = AwsBasicCredentials.create(
config.getAccessKey(),
config.getSecretKey()
);
return S3Presigner.builder()
.endpointOverride(URI.create(config.getEndpoint()))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.of("auto")) // Required by SDK but not used by R2
.serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(true)
.build())
.build();
}
public String generatePresignedUploadUrl(String bucketName, String objectKey, Duration expiration) {
PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
.signatureDuration(expiration)
.putObjectRequest(builder -> builder
.bucket(bucketName)
.key(objectKey)
.build())
.build();
PresignedPutObjectRequest presignedRequest = presigner.presignPutObject(presignRequest);
return presignedRequest.url().toString();
}
// Rest of the methods remains the same
public static void main(String[] args) {
// config the client as before
// Generate a pre-signed upload URL valid for 15 minutes
String uploadUrl = r2Client.generatePresignedUploadUrl(
"demos",
"README.md",
Duration.ofMinutes(15)
);
System.out.println("Pre-signed Upload URL (valid for 15 minutes):");
System.out.println(uploadUrl);
}
}
aws-sdk-js-v3
import { Render, WranglerConfig } from "~/components";
JavaScript or TypeScript users may continue to use the @aws-sdk/client-s3 npm package as per normal. You must pass in the R2 configuration credentials when instantiating your S3 service client.
:::note[Using the SDK in a Worker]
The AWS SDK for JavaScript v3 works in Workers with the nodejs_compat compatibility flag enabled. Operations that parse an XML response, such as ListObjectsV2, CopyObject, DeleteObjects, and multipart uploads, can fail with ReferenceError: DOMParser is not defined when Wrangler bundles the SDK. This happens because the browser field of @aws-sdk/xml-builder swaps in a parser that depends on DOMParser, which the Workers runtime does not provide. To work around this, install @aws-sdk/xml-builder as a direct dependency (for example, npm i @aws-sdk/xml-builder) so that it resolves from your project root, then alias the package to its CommonJS build in your Wrangler configuration file:
compatibility_date = "$today"
compatibility_flags = ["nodejs_compat"]
[alias]
"@aws-sdk/xml-builder" = "@aws-sdk/xml-builder/dist-cjs/index.js"
If you only need object reads and writes from a Worker, the R2 Workers API binding does not require the SDK. aws4fetch is a lighter alternative for S3-compatible requests and presigned URLs. :::
:::note
Currently, you cannot use AWS S3-compatible API while developing locally via wrangler dev.
:::
import {
S3Client,
ListBucketsCommand,
ListObjectsV2Command,
GetObjectCommand,
PutObjectCommand,
} from "@aws-sdk/client-s3";
const S3 = new S3Client({
region: "auto", // Required by SDK but not used by R2
// Provide your Cloudflare account ID
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
// Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
credentials: {
accessKeyId: ACCESS_KEY_ID,
secretAccessKey: SECRET_ACCESS_KEY,
},
});
console.log(await S3.send(new ListBucketsCommand({})));
// {
// '$metadata': {
// httpStatusCode: 200,
// requestId: undefined,
// extendedRequestId: undefined,
// cfId: undefined,
// attempts: 1,
// totalRetryDelay: 0
// },
// Buckets: [
// { Name: 'user-uploads', CreationDate: 2022-04-13T21:23:47.102Z },
// { Name: 'my-bucket', CreationDate: 2022-05-07T02:46:49.218Z }
// ],
// Owner: {
// DisplayName: '...',
// ID: '...'
// }
// }
console.log(await S3.send(new ListObjectsV2Command({ Bucket: "my-bucket" })));
// {
// '$metadata': {
// httpStatusCode: 200,
// requestId: undefined,
// extendedRequestId: undefined,
// cfId: undefined,
// attempts: 1,
// totalRetryDelay: 0
// },
// CommonPrefixes: undefined,
// Contents: [
// {
// Key: 'cat.png',
// LastModified: 2022-05-07T02:50:45.616Z,
// ETag: '"c4da329b38467509049e615c11b0c48a"',
// ChecksumAlgorithm: undefined,
// Size: 751832,
// StorageClass: 'STANDARD',
// Owner: undefined
// },
// {
// Key: 'todos.txt',
// LastModified: 2022-05-07T21:37:17.150Z,
// ETag: '"29d911f495d1ba7cb3a4d7d15e63236a"',
// ChecksumAlgorithm: undefined,
// Size: 279,
// StorageClass: 'STANDARD',
// Owner: undefined
// }
// ],
// ContinuationToken: undefined,
// Delimiter: undefined,
// EncodingType: undefined,
// IsTruncated: false,
// KeyCount: 8,
// MaxKeys: 1000,
// Name: 'my-bucket',
// NextContinuationToken: undefined,
// Prefix: undefined,
// StartAfter: undefined
// }
Generate presigned URLs
You can also generate presigned links that can be used to share public read or write access to a bucket temporarily.
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
// Use the expiresIn property to determine how long the presigned link is valid.
console.log(
await getSignedUrl(
S3,
new GetObjectCommand({ Bucket: "my-bucket", Key: "dog.png" }),
{ expiresIn: 3600 },
),
);
// You can also create links for operations such as PutObject to allow temporary write access to a specific key.
// Specify ContentType to restrict uploads to a specific file type.
console.log(
await getSignedUrl(
S3,
new PutObjectCommand({
Bucket: "my-bucket",
Key: "dog.png",
ContentType: "image/png",
}),
{ expiresIn: 3600 },
),
);
https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/dog.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=<signature>&x-id=GetObject
https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/dog.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Signature=<signature>&x-id=PutObject
You can use the link generated by the PutObject example to upload to the specified bucket and key, until the presigned link expires. When using a presigned URL with ContentType, the client must include a matching Content-Type header in the request.
curl -X PUT "https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/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:
- Restricting Content-Type: Specify the allowed content type in the
PutObjectCommand. The upload will fail if the client sends a differentContent-Typeheader. - 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:
const putUrl = await getSignedUrl(
S3,
new PutObjectCommand({
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/pngheader (enforced by the signature)
aws-sdk-kotlin
import { Render } from "~/components";
This example uses the aws-sdk-kotlin. You must pass in the R2 configuration credentials when instantiating your S3 client:
Basic Usage
import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider
import aws.sdk.kotlin.services.s3.S3Client
import aws.sdk.kotlin.services.s3.listObjects
import aws.sdk.kotlin.services.s3.model.GetObjectRequest
import aws.sdk.kotlin.services.s3.model.PutObjectRequest
import aws.sdk.kotlin.services.s3.presigners.presignGetObject
import aws.sdk.kotlin.services.s3.presigners.presignPutObject
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
import aws.smithy.kotlin.runtime.net.url.Url
import kotlinx.coroutines.runBlocking
import kotlin.time.Duration.Companion.minutes
val ACCOUNT_ID = "<ACCOUNT_ID>"
val ACCESS_KEY = "<ACCESS_KEY>"
val SECRET_KEY = "<SECRET_KEY>"
fun main() = runBlocking {
val r2Client = S3Client.fromEnvironment {
region = "auto" // Required by SDK, but not used by R2
endpointUrl = Url.parse("https://${ACCOUNT_ID}.r2.cloudflarestorage.com")
credentialsProvider = StaticCredentialsProvider(
Credentials(
accessKeyId = ACCESS_KEY,
secretAccessKey = SECRET_KEY
),
)
}
println("Available buckets:")
r2Client.listBuckets().buckets?.forEach { bucket ->
println("* ${bucket.name}")
}
val bucketName = "<BUCKET_NAME>"
println("\nObjects in bucket '${bucketName}':")
r2Client.listObjects { bucket = bucketName }.contents?.forEach {
println("* ${it.key} (size: ${it.size} bytes, modified: ${it.lastModified})")
}
r2Client.close()
}
Available buckets:
* my-bucket-1
* my-bucket-2
Objects in bucket 'my-bucket-1':
* image1.png (size: 253167 bytes, modified: 2026-01-17T11:30:58.896Z)
* image2.png (size: 247027 bytes, modified: 2026-01-17T11:30:57.779Z)
Generate presigned URLs
You can also generate presigned links that can be used to temporarily share public read or write access to a bucket.
val uploadUrl = r2Client.presignPutObject(
input = PutObjectRequest {
bucket = bucketName
key = "README.md"
},
duration = 15.minutes,
).url
println(uploadUrl)
val getUrl = r2Client.presignGetObject(
input = GetObjectRequest {
bucket = bucketName
key = "README.md"
},
duration = 15.minutes,
).url
println(getUrl)
You can use these presigned URLs with any HTTP client. For example, to upload a file using the PUT URL:
curl -X PUT "https://<your-presigned-put-url>" -H "Content-Type: application/octet-stream" --data-binary "@local-file.txt"
To download a file using the GET URL:
curl -X GET "https://<your-presigned-get-url>" -o downloaded-file.txt
aws-sdk-net
import { Render } from "~/components";
This example uses version 3 of the aws-sdk-net package. You must pass in the R2 configuration credentials when instantiating your S3 service client:
Client setup
In this example, you will pass credentials explicitly to the IAmazonS3 initialization. If you wish, use a shared AWS credentials file or the SDK store in-line with other AWS SDKs. Refer to Configure AWS credentials for more details.
private static IAmazonS3 s3Client;
public static void Main(string[] args)
{
// Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
var accessKey = "<ACCESS_KEY_ID>";
var secretKey = "<SECRET_ACCESS_KEY>";
var credentials = new BasicAWSCredentials(accessKey, secretKey);
s3Client = new AmazonS3Client(credentials, new AmazonS3Config
{
// Provide your Cloudflare account ID
ServiceURL = "https://<ACCOUNT_ID>.r2.cloudflarestorage.com",
});
}
List buckets and objects
The ListBucketsAsync and ListObjectsAsync methods can be used to list buckets under your account and the contents of those buckets respectively.
static async Task ListBuckets()
{
var response = await s3Client.ListBucketsAsync();
foreach (var s3Bucket in response.Buckets)
{
Console.WriteLine("{0}", s3Bucket.BucketName);
}
}
sdk-example
my-bucket
static async Task ListObjectsV2()
{
var request = new ListObjectsV2Request
{
BucketName = "my-bucket"
};
var response = await s3Client.ListObjectsV2Async(request);
foreach (var s3Object in response.S3Objects)
{
Console.WriteLine("{0}", s3Object.Key);
}
}
dog.png
cat.png
Upload and retrieve objects
The PutObjectAsync and GetObjectAsync methods can be used to upload objects and download objects from an R2 bucket respectively.
:::caution
DisablePayloadSigning = true and DisableDefaultChecksumValidation = true must be passed as Cloudflare R2 does not currently support the Streaming SigV4 implementation used by AWSSDK.S3.
:::
static async Task PutObject()
{
var request = new PutObjectRequest
{
FilePath = @"/path/file.txt",
BucketName = "my-bucket",
DisablePayloadSigning = true,
DisableDefaultChecksumValidation = true
};
var response = await s3Client.PutObjectAsync(request);
Console.WriteLine("ETag: {0}", response.ETag);
}
ETag: "186a71ee365d9686c3b98b6976e1f196"
static async Task GetObject()
{
var bucket = "my-bucket";
var key = "file.txt";
var response = await s3Client.GetObjectAsync(bucket, key);
Console.WriteLine("ETag: {0}", response.ETag);
}
ETag: "186a71ee365d9686c3b98b6976e1f196"
Generate presigned URLs
The GetPreSignedURL method allows you to sign ahead of time, giving temporary access to a specific operation. In this case, presigning a PutObject request for sdk-example/file.txt.
static string? GeneratePresignedUrl()
{
AWSConfigsS3.UseSignatureVersion4 = true;
var presign = new GetPreSignedUrlRequest
{
BucketName = "my-bucket",
Key = "file.txt",
Verb = HttpVerb.GET,
Expires = DateTime.Now.AddDays(7),
};
var presignedUrl = s3Client.GetPreSignedURL(presign);
Console.WriteLine(presignedUrl);
return presignedUrl;
}
https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/file.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=<signature>
aws-sdk-php
import { Render } from "~/components";
This example uses version 3 of the aws-sdk-php package. You must pass in the R2 configuration credentials when instantiating your S3 service client:
<?php
require 'vendor/aws/aws-autoloader.php';
$bucket_name = "my-bucket";
// Provide your Cloudflare account ID
$account_id = "<ACCOUNT_ID>";
// Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
$access_key_id = "<ACCESS_KEY_ID>";
$access_key_secret = "<SECRET_ACCESS_KEY>";
$credentials = new Aws\Credentials\Credentials($access_key_id, $access_key_secret);
$options = [
'region' => 'auto', // Required by SDK but not used by R2
'endpoint' => "https://$account_id.r2.cloudflarestorage.com",
'version' => 'latest',
'credentials' => $credentials
];
$s3_client = new Aws\S3\S3Client($options);
$contents = $s3_client->listObjectsV2([
'Bucket' => $bucket_name
]);
var_dump($contents['Contents']);
// array(1) {
// [0]=>
// array(5) {
// ["Key"]=>
// string(14) "ferriswasm.png"
// ["LastModified"]=>
// object(Aws\Api\DateTimeResult)#187 (3) {
// ["date"]=>
// string(26) "2022-05-18 17:20:21.670000"
// ["timezone_type"]=>
// int(2)
// ["timezone"]=>
// string(1) "Z"
// }
// ["ETag"]=>
// string(34) ""eb2b891dc67b81755d2b726d9110af16""
// ["Size"]=>
// string(5) "87671"
// ["StorageClass"]=>
// string(8) "STANDARD"
// }
// }
$buckets = $s3_client->listBuckets();
var_dump($buckets['Buckets']);
// array(1) {
// [0]=>
// array(2) {
// ["Name"]=>
// string(11) "my-bucket"
// ["CreationDate"]=>
// object(Aws\Api\DateTimeResult)#212 (3) {
// ["date"]=>
// string(26) "2022-05-18 17:19:59.645000"
// ["timezone_type"]=>
// int(2)
// ["timezone"]=>
// string(1) "Z"
// }
// }
// }
?>
Generate presigned URLs
You can also generate presigned links that can be used to share public read or write access to a bucket temporarily.
$cmd = $s3_client->getCommand('GetObject', [
'Bucket' => $bucket_name,
'Key' => 'ferriswasm.png'
]);
// The second parameter allows you to determine how long the presigned link is valid.
$request = $s3_client->createPresignedRequest($cmd, '+1 hour');
print_r((string)$request->getUri());
// https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/ferriswasm.png?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Signature=<signature>
// You can also create links for operations such as putObject to allow temporary write access to a specific key.
$cmd = $s3_client->getCommand('PutObject', [
'Bucket' => $bucket_name,
'Key' => 'ferriswasm.png'
]);
$request = $s3_client->createPresignedRequest($cmd, '+1 hour');
print_r((string)$request->getUri());
You can use the link generated by the putObject example to upload to the specified bucket and key, until the presigned link expires.
curl -X PUT "https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/ferriswasm.png?X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Signature=<signature>" --data-binary @ferriswasm.png
aws-sdk-ruby
import { Render } from "~/components";
Many Ruby projects also store these credentials in environment variables instead.
Add the following dependency to your Gemfile:
gem "aws-sdk-s3"
Then you can use Ruby to operate on R2 buckets:
require "aws-sdk-s3"
@r2 = Aws::S3::Client.new(
# Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
access_key_id: "#{ACCESS_KEY_ID}",
secret_access_key: "#{SECRET_ACCESS_KEY}",
# Provide your Cloudflare account ID
endpoint: "https://#{ACCOUNT_ID}.r2.cloudflarestorage.com",
region: "auto", # Required by SDK but not used by R2
)
# List all buckets on your account
puts @r2.list_buckets
#=> {
#=> :buckets => [{
#=> :name => "your-bucket",
#=> :creation_date => "…",
#=> }],
#=> :owner => {
#=> :display_name => "…",
#=> :id => "…"
#=> }
#=> }
# List the first 20 items in a bucket
puts @r2.list_objects_v2(bucket: "your-bucket", max_keys: 20)
#=> {
#=> :is_truncated => false,
#=> :name => "your-bucket",
#=> :prefix => nil,
#=> :delimiter => nil,
#=> :max_keys => 20,
#=> :common_prefixes => [],
#=> :encoding_type => nil,
#=> :key_count => 3,
#=> :contents => [
#=> …,
#=> …,
#=> …,
#=> ]
#=> }
aws-sdk-rust
import { Render } from "~/components";
This example uses the aws-sdk-s3 crate from the AWS SDK for Rust. You must pass in the R2 configuration credentials when instantiating your S3 client:
Basic Usage
use aws_sdk_s3 as s3;
use aws_smithy_types::date_time::Format::DateTime;
#[tokio::main]
async fn main() -> Result<(), s3::Error> {
let bucket_name = "sdk-example";
// Provide your Cloudflare account ID
let account_id = "<ACCOUNT_ID>";
// Retrieve your S3 API credentials for your R2 bucket via API tokens
// (see: https://developers.cloudflare.com/r2/api/tokens)
let access_key_id = "<ACCESS_KEY_ID>";
let access_key_secret = "<SECRET_ACCESS_KEY>";
// Configure the client
let config = aws_config::from_env()
.endpoint_url(format!("https://{}.r2.cloudflarestorage.com", account_id))
.credentials_provider(aws_sdk_s3::config::Credentials::new(
access_key_id,
access_key_secret,
None, // session token is not used with R2
None,
"R2",
))
.region("auto") // Required by SDK but not used by R2
.load()
.await;
let client = s3::Client::new(&config);
// List buckets
let list_buckets_output = client.list_buckets().send().await?;
println!("Buckets:");
for bucket in list_buckets_output.buckets() {
println!(" - {}: {}",
bucket.name().unwrap_or_default(),
bucket.creation_date().map_or_else(
|| "Unknown creation date".to_string(),
|date| date.fmt(DateTime).unwrap()
)
);
}
// List objects in a specific bucket
let list_objects_output = client
.list_objects_v2()
.bucket(bucket_name)
.send()
.await?;
println!("\nObjects in {}:", bucket_name);
for object in list_objects_output.contents() {
println!(" - {}: {} bytes, last modified: {}",
object.key().unwrap_or_default(),
object.size().unwrap_or_default(),
object.last_modified().map_or_else(
|| "Unknown".to_string(),
|date| date.fmt(DateTime).unwrap()
)
);
}
Ok(())
}
Upload Objects
To upload an object to R2:
use aws_sdk_s3::primitives::ByteStream;
use std::path::Path;
async fn upload_object(
client: &s3::Client,
bucket: &str,
key: &str,
file_path: &str,
) -> Result<(), s3::Error> {
let body = ByteStream::from_path(Path::new(file_path)).await.unwrap();
client
.put_object()
.bucket(bucket)
.key(key)
.body(body)
.send()
.await?;
println!("Uploaded {} to {}/{}", file_path, bucket, key);
Ok(())
}
Download Objects
To download an object from R2:
use std::fs;
use std::io::Write;
async fn download_object(
client: &s3::Client,
bucket: &str,
key: &str,
output_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let resp = client
.get_object()
.bucket(bucket)
.key(key)
.send()
.await?;
let data = resp.body.collect().await?;
let bytes = data.into_bytes();
let mut file = fs::File::create(output_path)?;
file.write_all(&bytes)?;
println!("Downloaded {}/{} to {}", bucket, key, output_path);
Ok(())
}
Generate Presigned URLs
You can also generate presigned links that can be used to temporarily share public read or write access to a bucket.
use aws_sdk_s3::presigning::PresigningConfig;
use std::time::Duration;
async fn generate_get_presigned_url(
client: &s3::Client,
bucket: &str,
key: &str,
expires_in: Duration,
) -> Result<String, s3::Error> {
let presigning_config = PresigningConfig::expires_in(expires_in)?;
// Generate a presigned URL for GET (download)
let presigned_get_request = client
.get_object()
.bucket(bucket)
.key(key)
.presigned(presigning_config)
.await?;
Ok(presigned_get_request.uri().to_string())
}
async fn generate_upload_presigned_url(
client: &s3::Client,
bucket: &str,
key: &str,
expires_in: Duration,
) -> Result<String, s3::Error> {
let presigning_config = PresigningConfig::expires_in(expires_in)?;
// Generate a presigned URL for PUT (upload)
let presigned_put_request = client
.put_object()
.bucket(bucket)
.key(key)
.presigned(presigning_config)
.await?;
Ok(presigned_put_request.uri().to_string())
}
You can use these presigned URLs with any HTTP client. For example, to upload a file using the PUT URL:
curl -X PUT "https://<your-presigned-put-url>" -H "Content-Type: application/octet-stream" --data-binary "@local-file.txt"
To download a file using the GET URL:
curl -X GET "https://<your-presigned-get-url>" -o downloaded-file.txt
aws4fetch
import { Render } from "~/components";
JavaScript or TypeScript users may continue to use the aws4fetch npm package as per normal. This package uses the fetch and SubtleCrypto APIs which you will be familiar with when working in browsers or with Cloudflare Workers.
aws4fetch is a lightweight option for Workers because it has no Node.js dependencies. You can also use it to generate presigned URLs from a Worker.
You must pass in the R2 configuration credentials when instantiating your S3 service client:
import { AwsClient } from "aws4fetch";
// Provide your Cloudflare account ID
const R2_URL = `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`;
const client = new AwsClient({
// Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
accessKeyId: ACCESS_KEY_ID,
secretAccessKey: SECRET_ACCESS_KEY,
});
const ListBucketsResult = await client.fetch(R2_URL);
console.log(await ListBucketsResult.text());
// <ListAllMyBucketsResult>
// <Buckets>
// <Bucket>
// <CreationDate>2022-04-13T21:23:47.102Z</CreationDate>
// <Name>user-uploads</Name>
// </Bucket>
// <Bucket>
// <CreationDate>2022-05-07T02:46:49.218Z</CreationDate>
// <Name>my-bucket</Name>
// </Bucket>
// </Buckets>
// <Owner>
// <DisplayName>...</DisplayName>
// <ID>...</ID>
// </Owner>
// </ListAllMyBucketsResult>
const ListObjectsV2Result = await client.fetch(
`${R2_URL}/my-bucket?list-type=2`,
);
console.log(await ListObjectsV2Result.text());
// <ListBucketResult>
// <Name>my-bucket</Name>
// <Contents>
// <Key>cat.png</Key>
// <Size>751832</Size>
// <LastModified>2022-05-07T02:50:45.616Z</LastModified>
// <ETag>"c4da329b38467509049e615c11b0c48a"</ETag>
// <StorageClass>STANDARD</StorageClass>
// </Contents>
// <Contents>
// <Key>todos.txt</Key>
// <Size>278</Size>
// <LastModified> 2022-05-07T21:37:17.150Z</LastModified>
// <ETag>"29d911f495d1ba7cb3a4d7d15e63236a"</ETag>
// <StorageClass>STANDARD</StorageClass>
// </Contents>
// <IsTruncated>false</IsTruncated>
// <MaxKeys>1000</MaxKeys>
// <KeyCount>2</KeyCount>
// </ListBucketResult>
Generate presigned URLs
You can also generate presigned links that can be used to share public read or write access to a bucket temporarily.
import { AwsClient } from "aws4fetch";
const client = new AwsClient({
service: "s3", // Required by SDK but not used by R2
region: "auto", // Required by SDK but not used by R2
// Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
accessKeyId: ACCESS_KEY_ID,
secretAccessKey: SECRET_ACCESS_KEY,
});
// Provide your Cloudflare account ID
const R2_URL = `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`;
// Use the `X-Amz-Expires` query param to determine how long the presigned link is valid.
console.log(
(
await client.sign(
new Request(`${R2_URL}/my-bucket/dog.png?X-Amz-Expires=${3600}`),
{
aws: { signQuery: true },
},
)
).url.toString(),
);
// You can also create links for operations such as PutObject to allow temporary write access to a specific key.
// Specify Content-Type header to restrict uploads to a specific file type.
console.log(
(
await client.sign(
new Request(`${R2_URL}/my-bucket/dog.png?X-Amz-Expires=${3600}`, {
method: "PUT",
headers: {
"Content-Type": "image/png",
},
}),
{
aws: { signQuery: true },
},
)
).url.toString(),
);
https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/dog.png?X-Amz-Expires=3600&X-Amz-Date=<timestamp>&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=<credential>&X-Amz-SignedHeaders=host&X-Amz-Signature=<signature>
https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/dog.png?X-Amz-Expires=3600&X-Amz-Date=<timestamp>&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=<credential>&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Signature=<signature>
You can use the link generated by the PutObject example to upload to the specified bucket and key, until the presigned link expires. When using a presigned URL with Content-Type, the client must include a matching Content-Type header in the request.
curl -X PUT "https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/dog.png?X-Amz-Expires=3600&X-Amz-Date=<timestamp>&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=<credential>&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Signature=<signature>" \
-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:
-
Restricting Content-Type: Specify the
Content-Typeheader in the request when signing. The upload will fail if the client sends a differentContent-Typeheader. -
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:
const signedRequest = await client.sign(
new Request(`${R2_URL}/my-bucket/user-upload.png?X-Amz-Expires=${3600}`, {
method: "PUT",
headers: {
"Content-Type": "image/png",
},
}),
{
aws: { signQuery: true },
},
);
const putUrl = signedRequest.url.toString();
When a client uses this presigned URL, they must:
- Make the request from an allowed origin (enforced by CORS)
- Include the
Content-Type: image/pngheader (enforced by the signature)
boto3
import { Render } from "~/components";
Configure boto3 to use your R2 endpoint:
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:
# 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:
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.
# 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:
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:
-
Restricting Content-Type: Specify the allowed content type in the presigned URL parameters. The upload will fail if the client sends a different
Content-Typeheader. -
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:
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/pngheader (enforced by the signature)
Configure custom headers
Some of R2's extensions require setting a specific header when using them in the S3 compatible API. For some functionality you may want to set a request header on an entire category of requests. Other times you may want to configure a different header for each individual request. This page contains some examples on how to do so with boto3 and with aws-sdk-js-v3.
Setting a custom header on all requests
When using certain functionality, like the cf-create-bucket-if-missing header, you may want to set a constant header for all PutObject requests you're making.
Set a header for all requests with boto3
Boto3 has an event system which allows you to modify requests. Here we register a function into the event system which adds our header to every PutObject request being made.
import boto3
client = boto3.resource('s3',
# Provide your Cloudflare account ID
endpoint_url = 'https://<ACCOUNT_ID>.r2.cloudflarestorage.com',
# Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
aws_access_key_id = '<ACCESS_KEY_ID>',
aws_secret_access_key = '<SECRET_ACCESS_KEY>'
)
event_system = client.meta.events
# Define function responsible for adding the header
def add_custom_header(params, **kwargs):
params["headers"]['cf-create-bucket-if-missing'] = 'true'
event_system.register('before-call.s3.PutObject', add_custom_header)
response = client.put_object(Bucket="my-bucket", Key="my_file", Body="file_contents")
print(response)
Set a header for all requests with aws-sdk-js-v3
aws-sdk-js-v3 allows the customization of request behavior through the use of its middleware stack. This example adds a middleware to the client which adds a header to every PutObject request being made.
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { HttpRequest } from "@smithy/core/protocols";
const client = new S3Client({
region: "auto", // Required by SDK but not used by R2
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
// Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
credentials: {
accessKeyId: ACCESS_KEY_ID,
secretAccessKey: SECRET_ACCESS_KEY,
},
});
client.middlewareStack.add(
(next) => async (args) => {
const request = args.request;
if (HttpRequest.isInstance(request)) {
request.headers["cf-create-bucket-if-missing"] = "true";
}
return next(args);
},
{ step: "build", name: "customHeaders" },
);
const command = new PutObjectCommand({
Bucket: "my-bucket",
Key: "my_key",
Body: "my_data",
});
const response = await client.send(command);
console.log(response);
Set a different header on each request
Certain extensions that R2 has provided in the S3 compatible api may require setting a different header on each request. For example, you may want to only want to overwrite an object if its etag matches a certain expected value. This value will likely be different for each object that is being overwritten, which requires the If-Match header to be different with each request you make. This section shows examples of how to accomplish that.
Set a header per request in boto3
To enable us to pass custom headers as an extra argument into the call to client.put_object() we need to register 2 functions into boto3's event system. This is necessary because boto3 performs a parameter validation step which rejects extra method arguments. Since this parameter validation occurs before we can set headers on the request, we first need to move the custom argument into the request context before the parameter validation happens. In a subsequent step we can now actually set the headers based on the information we put in the request context.
import boto3
client = boto3.resource('s3',
# Provide your Cloudflare account ID
endpoint_url = 'https://<ACCOUNT_ID>.r2.cloudflarestorage.com',
# Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
aws_access_key_id = '<ACCESS_KEY_ID>',
aws_secret_access_key = '<SECRET_ACCESS_KEY>'
)
event_system = client.meta.events
# Moves the custom headers from the parameters to the request context
def process_custom_arguments(params, context, **kwargs):
if (custom_headers := params.pop("custom_headers", None)):
context["custom_headers"] = custom_headers
# Here we extract the headers from the request context and actually set them
def add_custom_headers(params, context, **kwargs):
if (custom_headers := context.get("custom_headers")):
params["headers"].update(custom_headers)
event_system.register('before-parameter-build.s3.PutObject', process_custom_arguments)
event_system.register('before-call.s3.PutObject', add_custom_headers)
custom_headers = {'If-Match' : '"29d911f495d1ba7cb3a4d7d15e63236a"'}
# Note that boto3 will throw an exception if the precondition failed. Catch this exception if necessary
response = client.put_object(Bucket="my-bucket", Key="my_key", Body="file_contents", custom_headers=custom_headers)
print(response)
Set a header per request in aws-sdk-js-v3
Here we again configure the header we would like to set by creating a middleware, but this time we add the middleware to the request itself instead of to the whole client.
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { HttpRequest } from "@smithy/core/protocols";
const client = new S3Client({
region: "auto", // Required by SDK but not used by R2
// Provide your Cloudflare account ID
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
// Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
credentials: {
accessKeyId: ACCESS_KEY_ID,
secretAccessKey: SECRET_ACCESS_KEY,
},
});
const command = new PutObjectCommand({
Bucket: "my-bucket",
Key: "my_key",
Body: "my_data",
});
const headers = { "If-Match": '"29d911f495d1ba7cb3a4d7d15e63236a"' };
command.middlewareStack.add(
(next) => (args) => {
const request = args.request;
if (HttpRequest.isInstance(request)) {
Object.assign(request.headers, headers);
}
return next(args);
},
{ step: "build", name: "customHeaders" },
);
const response = await client.send(command);
console.log(response);
S3 SDKs
import { DirectoryListing } from "~/components";
s3mini
import { Render } from "~/components";
s3mini is a zero-dependency, lightweight (~20 KB minified) TypeScript S3 client that uses AWS SigV4 signing. It runs natively on Node.js, Bun, and Cloudflare Workers without polyfills.
Unlike the AWS SDKs, s3mini expects a bucket-scoped endpoint — the bucket name is part of the endpoint URL, so you do not pass a separate bucket parameter to each operation.
Install
npm install s3mini
Node.js / Bun
import { S3mini } from "s3mini";
const s3 = new S3mini({
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
// Bucket-scoped endpoint — include your bucket name in the path
endpoint: `https://${process.env.ACCOUNT_ID}.r2.cloudflarestorage.com/my-bucket`,
region: "auto",
});
// Upload an object
await s3.putObject("hello.txt", "Hello from s3mini!");
// Download an object as a string
const text = await s3.getObject("hello.txt");
console.log(text);
// List objects with a prefix
const objects = await s3.listObjects("/", "hello");
console.log(objects);
// Delete an object
await s3.deleteObject("hello.txt");
Cloudflare Workers
:::note[Prefer R2 bindings inside Workers] When your Worker and R2 bucket live in the same Cloudflare account, R2 bindings give you zero-latency access without managing API credentials. Use the S3 API when you need cross-account access or interoperability with S3-compatible tooling. :::
s3mini works natively in Workers without the nodejs_compat compatibility flag.
import { S3mini } from "s3mini";
interface Env {
R2_ACCESS_KEY_ID: string;
R2_SECRET_ACCESS_KEY: string;
ACCOUNT_ID: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const s3 = new S3mini({
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
endpoint: `https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com/my-bucket`,
region: "auto",
});
const url = new URL(request.url);
const key = url.pathname.slice(1); // strip leading "/"
if (!key) {
return new Response("Missing object key", { status: 400 });
}
switch (request.method) {
case "PUT": {
const data = await request.arrayBuffer();
const contentType =
request.headers.get("content-type") ?? "application/octet-stream";
await s3.putObject(key, new Uint8Array(data), contentType);
return new Response("Created", { status: 201 });
}
case "GET": {
const response = await s3.getObjectResponse(key);
if (!response) {
return new Response("Not Found", { status: 404 });
}
return new Response(response.body, {
headers: {
"content-type":
response.headers.get("content-type") ??
"application/octet-stream",
etag: response.headers.get("etag") ?? "",
},
});
}
case "DELETE": {
await s3.deleteObject(key);
return new Response(null, { status: 204 });
}
default:
return new Response("Method Not Allowed", { status: 405 });
}
},
};
CLI
import { PackageManagers, LinkCard, Render, Tabs, TabItem, Steps } from "~/components";
Manage R2 buckets and objects directly from your terminal. Use CLI tools to automate tasks and manage objects.
| Tool | Best for |
|---|---|
| Wrangler | Single object operations and managing bucket settings with minimal setup |
| rclone | Bulk object operations, migrations, and syncing directories |
| AWS CLI | Existing AWS workflows or familiarity with AWS CLI |
1. Create a bucket
2. Generate API credentials
CLI tools that use the S3 API (AWS CLI, rclone) require an Access Key ID and Secret Access Key. If you are using Wrangler, you can skip this step.
1. In the Cloudflare dashboard, go to **R2**. 2. Select **Manage R2 API tokens**. 3. Select **Create API token**. 4. Choose **Object Read & Write** permission and select the buckets you want to access. 5. Select **Create API Token**. 6. Copy the **Access Key ID** and **Secret Access Key**. Store these securely — you cannot view the secret again.3. Set up a CLI tool
Wrangler is the Cloudflare Workers CLI. It authenticates with your Cloudflare account directly, so no API credentials needed.
1. Install Wrangler:-
Log in to your Cloudflare account:
wrangler login
rclone is ideal for bulk uploads, migrations, and syncing directories.
1. [Install rclone](https://rclone.org/install/) (version 1.59 or later). 2. Configure a new remote:rclone config
- Create new remote by selecting
n. - Name your remote
r2 - Select Amazon S3 Compliant Storage Providers as the storage type.
- Select Cloudflare R2 as the provider.
- Select whether you would like to enter AWS credentials manually, or get it from the runtime environment.
- Enter your Access Key ID and Secret Access Key when prompted.
- Select the region to connect to (optional).
- Provide your S3 API endpoint.
The AWS CLI works with R2 by specifying a custom endpoint.
1. [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) for your operating system. 2. Configure your credentials:aws configure
-
When prompted, enter:
- AWS Access Key ID: Your R2 Access Key ID
- AWS Secret Access Key: Your R2 Secret Access Key
- Default region name:
auto - Default output format:
json(or press Enter to skip)
4. Upload and download objects
(Optional) Create a test file to upload. Run this command in the directory where you plan to run the CLI commands:
echo 'Hello, R2!' > myfile.txt
# Upload myfile.txt to my-bucket
wrangler r2 object put my-bucket/myfile.txt --file ./myfile.txt
# Download myfile.txt and save it as downloaded.txt
wrangler r2 object get my-bucket/myfile.txt --file ./downloaded.txt
Refer to the Wrangler R2 commands for all available operations.
# Upload myfile.txt to my-bucket
rclone copy myfile.txt r2:my-bucket/
# Download myfile.txt from my-bucket to the current directory
rclone copy r2:my-bucket/myfile.txt .
Refer to the rclone documentation for more configuration options.
# Upload myfile.txt to my-bucket
aws s3 cp myfile.txt s3://my-bucket/ --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com
# Download myfile.txt from my-bucket to current directory
aws s3 cp s3://my-bucket/myfile.txt ./ --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com
# List all objects in my-bucket
aws s3 ls s3://my-bucket/ --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com
Refer to the AWS CLI documentation for more examples.
Next steps
Get started
Cloudflare R2 Storage allows developers to store large amounts of unstructured data without the costly egress bandwidth fees associated with typical cloud storage services.
Before you begin
You need a Cloudflare account with an R2 subscription. If you do not have one:
- Go to the Cloudflare Dashboard.
- Select Storage & databases > R2 > Overview
- Complete the checkout flow to add an R2 subscription to your account.
R2 is free to get started with included free monthly usage. You are billed for your usage on a monthly basis. Refer to Pricing for details.
Choose how to access R2
R2 supports multiple access methods, so you can choose the one that fits your use case best:
| Method | Use when |
|---|---|
| Workers API | You are building an application on Cloudflare Workers that needs to read or write from R2 |
| S3 | You want to use S3-compatible SDKs to interact with R2 in your existing applications |
| CLI tools | You want to upload, download, or manage objects from your terminal |
| Dashboard | You want to quickly view and manage buckets and objects in the browser |
Next steps
S3
import { LinkCard, PackageManagers, Render, Tabs, TabItem, Steps } from "~/components";
R2 provides support for a S3-compatible API, which means you can use any S3 SDK, library, or tool to interact with your buckets. If you have existing code that works with S3, you can use it with R2 by changing the endpoint URL.
1. Create a bucket
2. Generate API credentials
To use the S3 API, you need to generate credentials and get an Access Key ID and Secret Access Key:
You also need your S3 API endpoint URL which you can find at the bottom of the Create API Token confirmation page once you have created your token, or on the R2 Overview page:
https://<ACCOUNT_ID>.r2.cloudflarestorage.com
3. Use an AWS SDK
The following examples show how to use Python and JavaScript SDKs. For other languages, refer to S3-compatible SDK examples for Go, Java, PHP, Ruby, and Rust.
-
Install boto3:
pip install boto3 -
Create a test file to upload:
echo 'Hello, R2!' > myfile.txt -
Use your credentials to create an S3 client and interact with your bucket:
import boto3 s3 = boto3.client( service_name='s3', # Provide your R2 endpoint: https://<ACCOUNT_ID>.r2.cloudflarestorage.com endpoint_url='https://<ACCOUNT_ID>.r2.cloudflarestorage.com', # Provide your R2 Access Key ID and Secret Access Key aws_access_key_id='<ACCESS_KEY_ID>', aws_secret_access_key='<SECRET_ACCESS_KEY>', region_name='auto', # Required by boto3, not used by R2 ) # Upload a file s3.upload_file('myfile.txt', 'my-bucket', 'myfile.txt') print('Uploaded myfile.txt') # Download a file s3.download_file('my-bucket', 'myfile.txt', 'downloaded.txt') print('Downloaded to downloaded.txt') # List objects response = s3.list_objects_v2(Bucket='my-bucket') for obj in response.get('Contents', []): print(f"Object: {obj['Key']}") -
Save this as
example.pyand run it:python example.pyUploaded myfile.txt Downloaded to downloaded.txt Object: myfile.txt
Refer to boto3 examples for more operations.
-
Install the @aws-sdk/client-s3 package:
npm install @aws-sdk/client-s3 -
Use your credentials to create an S3 client and interact with your bucket:
import { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, } from "@aws-sdk/client-s3"; const s3 = new S3Client({ region: "auto", // Required by AWS SDK, not used by R2 // Provide your R2 endpoint: https://<ACCOUNT_ID>.r2.cloudflarestorage.com endpoint: "https://<ACCOUNT_ID>.r2.cloudflarestorage.com", credentials: { // Provide your R2 Access Key ID and Secret Access Key accessKeyId: "<ACCESS_KEY_ID>", secretAccessKey: "<SECRET_ACCESS_KEY>", }, }); // Upload a file await s3.send( new PutObjectCommand({ Bucket: "my-bucket", Key: "myfile.txt", Body: "Hello, R2!", }), ); console.log("Uploaded myfile.txt"); // Download a file const response = await s3.send( new GetObjectCommand({ Bucket: "my-bucket", Key: "myfile.txt", }), ); const content = await response.Body.transformToString(); console.log("Downloaded:", content); // List objects const list = await s3.send( new ListObjectsV2Command({ Bucket: "my-bucket", }), ); console.log( "Objects:", list.Contents.map((obj) => obj.Key), ); -
Save this as
example.mjsand run it:node example.mjsUploaded myfile.txt Downloaded: Hello, R2! Objects: [ 'myfile.txt' ]
Refer to AWS SDK for JavaScript examples for more operations.
Next steps
Workers API
import { PackageManagers, WranglerConfig, LinkCard, Render, Tabs, TabItem, Steps } from "~/components";
Workers let you run code at the edge. When you bind an R2 bucket to a Worker, you can read and write objects directly using the Workers API.
1. Create a bucket
2. Create a Worker with an R2 binding
-
Create a new Worker project:
<PackageManagers type="create" pkg="cloudflare@latest" args={"r2-worker"} />
When prompted, select Hello World example and JavaScript (or TypeScript) as your template.
-
Move into the project directory:
cd r2-worker -
Add an R2 binding to your Wrangler configuration file. Replace
my-bucketwith your bucket name:{ "r2_buckets": [ { "binding": "MY_BUCKET", "bucket_name": "my-bucket" } ] } -
(Optional) If you are using TypeScript, regenerate types:
npx wrangler types
3. Read and write objects
Use the binding to interact with your bucket. This example stores and retrieves objects based on the URL path:
export default {
async fetch(request, env) {
// Get the object key from the URL path
// For example: /images/cat.png → images/cat.png
const url = new URL(request.url);
const key = url.pathname.slice(1);
// PUT: Store the request body in R2
if (request.method === "PUT") {
await env.MY_BUCKET.put(key, request.body);
return new Response(`Put ${key} successfully!`);
}
// GET: Retrieve the object from R2
const object = await env.MY_BUCKET.get(key);
if (object === null) {
return new Response("Object not found", { status: 404 });
}
return new Response(object.body);
},
};
export default {
async fetch(request, env): Promise<Response> {
// Get the object key from the URL path
// For example: /images/cat.png → images/cat.png
const url = new URL(request.url);
const key = url.pathname.slice(1);
// PUT: Store the request body in R2
if (request.method === "PUT") {
await env.MY_BUCKET.put(key, request.body);
return new Response(`Put ${key} successfully!`);
}
// GET: Retrieve the object from R2
const object = await env.MY_BUCKET.get(key);
if (object === null) {
return new Response("Object not found", { status: 404 });
}
return new Response(object.body);
},
} satisfies ExportedHandler<Env>;
4. Test and deploy
-
Test your Worker locally:
npx wrangler dev:::note[Local development] By default,
wrangler devuses a local R2 simulation. Objects you store during development exist only on your machine in the.wrangler/statefolder and do not affect your production bucket.To connect to your real R2 bucket during development, add
"remote": trueto your R2 binding in your Wrangler configuration file. Refer to remote bindings for more information. ::: -
Once the dev server is running, test storing and retrieving objects:
# Store an object curl -X PUT http://localhost:8787/my-file.txt -d 'Hello, R2!' # Retrieve the object curl http://localhost:8787/my-file.txt -
Deploy to production:
npx wrangler deploy -
After deploying, Wrangler outputs your Worker's URL (for example,
https://r2-worker.<YOUR_SUBDOMAIN>.workers.dev). Test storing and retrieving objects:# Store an object curl -X PUT https://r2-worker.<YOUR_SUBDOMAIN>.workers.dev/my-file.txt -d 'Hello, R2!' # Retrieve the object curl https://r2-worker.<YOUR_SUBDOMAIN>.workers.dev/my-file.txt
Refer to the Workers R2 API documentation for the complete API reference.
Next steps
Delete objects
import { Tabs, TabItem, DashButton, LinkCard } from "~/components";
You can delete objects from R2 using the dashboard, Workers API, S3 API, or command-line tools. To empty or delete an entire bucket, refer to Delete buckets.
Delete via dashboard
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Select your bucket.
-
(Optional) Select the View prefixes as directories checkbox to view prefixes grouped as folders.
-
Select the objects or folders you want to delete. You can select a mix of both in the same operation.
-
Select Delete.
-
Confirm your choice in the dialog that appears.
To delete all objects in a bucket at once, refer to Empty a bucket.
Delete via Workers API
Use R2 bindings in Workers to delete objects:
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
await env.MY_BUCKET.delete("image.png");
return new Response("Deleted");
},
} satisfies ExportedHandler<Env>;
For complete documentation, refer to Workers API.
Delete via S3 API
Use S3-compatible SDKs to delete objects. You'll need your account ID and R2 API token.
import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3";
const S3 = new S3Client({
region: "auto", // Required by SDK but not used by R2
// Provide your Cloudflare account ID
endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
// Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
credentials: {
accessKeyId: '<ACCESS_KEY_ID>',
secretAccessKey: '<SECRET_ACCESS_KEY>',
},
});
await S3.send(
new DeleteObjectCommand({
Bucket: "my-bucket",
Key: "image.png",
}),
);
import boto3
s3 = boto3.client(
service_name="s3",
# Provide your Cloudflare account ID
endpoint_url=f"https://{ACCOUNT_ID}.r2.cloudflarestorage.com",
# Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
aws_access_key_id=ACCESS_KEY_ID,
aws_secret_access_key=SECRET_ACCESS_KEY,
region_name="auto", # Required by SDK but not used by R2
)
s3.delete_object(Bucket="my-bucket", Key="image.png")
For complete S3 API documentation, refer to S3 API.
Delete via Wrangler
:::caution
Deleting objects from a bucket is irreversible.
:::
Use Wrangler to delete objects. Run the r2 object delete command:
wrangler r2 object delete test-bucket/image.png
Related resources
Download objects
import { Render, Tabs, TabItem, DashButton } from "~/components";
You can download objects from R2 using the dashboard, Workers API, S3 API, or command-line tools.
Download via dashboard
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Select your bucket.
-
Locate the object you want to download.
-
Select ... for the object and click Download.
Download via Workers API
Use R2 bindings in Workers to download objects:
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const object = await env.MY_BUCKET.get("image.png");
if (object === null) {
return new Response("Object not found", { status: 404 });
}
return new Response(object.body);
},
} satisfies ExportedHandler<Env>;
For complete documentation, refer to Workers API.
Download via S3 API
Use S3-compatible SDKs to download objects. You'll need your account ID and R2 API token.
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
const S3 = new S3Client({
region: "auto", // Required by SDK but not used by R2
// Provide your Cloudflare account ID
endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
// Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
credentials: {
accessKeyId: '<ACCESS_KEY_ID>',
secretAccessKey: '<SECRET_ACCESS_KEY>',
},
});
const response = await S3.send(
new GetObjectCommand({
Bucket: "my-bucket",
Key: "image.png",
}),
);
import boto3
s3 = boto3.client(
service_name="s3",
# Provide your Cloudflare account ID
endpoint_url=f"https://{ACCOUNT_ID}.r2.cloudflarestorage.com",
# Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
aws_access_key_id=ACCESS_KEY_ID,
aws_secret_access_key=SECRET_ACCESS_KEY,
region_name="auto", # Required by SDK but not used by R2
)
response = s3.get_object(Bucket="my-bucket", Key="image.png")
image_data = response["Body"].read()
Refer to R2's S3 API documentation for all S3 API methods.
Presigned URLs
For client-side downloads where users download directly from R2, use presigned URLs. Your server generates a temporary download URL that clients can use without exposing your API credentials.
- Your application generates a presigned GET URL using an S3 SDK
- Send the URL to your client
- Client downloads directly from R2 using the presigned URL
For details on generating and using presigned URLs, refer to Presigned URLs.
Download via Wrangler
Use Wrangler to download objects. Run the r2 object get command:
wrangler r2 object get test-bucket/image.png
The file will be downloaded into the current working directory. You can also use the --file flag to set a new name for the object as it is downloaded, and the --pipe flag to pipe the download to standard output (stdout).
Objects
import { DirectoryListing, Render } from "~/components"
Objects are individual files or data that you store in an R2 bucket. Each object is identified by its key, a string like images/photo.png.
Prefixes and folders
R2 uses a flat storage structure. There are no real directories or folders. The / character in an object key is used as a delimiter to group objects by prefix.
The R2 dashboard groups objects that share a common prefix into folders when the View prefixes as directories checkbox is selected. For example, objects with keys logs/jan.csv and logs/feb.csv appear under a logs/ folder. These folders are a visual grouping and do not exist as separate resources in your bucket.
Manage objects
Upload objects
import { Steps, Tabs, TabItem, Render, DashButton, LinkCard, TypeScriptExample, } from "~/components";
There are several ways to upload objects to R2. Which approach you choose depends on the size of your objects and your performance requirements.
Choose an upload method
Single upload (PUT) |
Multipart upload | |
|---|---|---|
| Best for | Small to medium files (under ~100 MB) | Large files, or when you need parallelism and resumability |
| Maximum object size | 5 GiB | 5 TiB (up to 10,000 parts) |
| Part size | N/A | 5 MiB – 5 GiB per part |
| Resumable | No — must restart the entire upload | Yes — only failed parts need to be retried |
| Parallel upload | No | Yes — parts can be uploaded concurrently |
| When to use | Quick, simple uploads of small objects | Video, backups, datasets, or any file where reliability matters |
:::note
Most S3-compatible SDKs and tools (such as rclone) automatically choose multipart upload for large files based on a configurable threshold. You do not typically need to implement multipart logic yourself when using the S3 API.
:::
Upload via dashboard
To upload objects to your bucket from the Cloudflare dashboard:
1. In the Cloudflare dashboard, go to the **R2 object storage** page. 2. Select your bucket. 3. Select **Upload**. 4. Drag and drop your file into the upload area or **select from computer**.You will receive a confirmation message after a successful upload.
Create a folder
You can also create folders from the dashboard by selecting Create folder. This creates a zero-byte object with a key ending in / that acts as a placeholder. For more information on how folders work in R2, refer to Prefixes and folders.
Upload via Workers API
Use R2 bindings in Workers to upload objects server-side. Refer to Use R2 from Workers for instructions on setting up an R2 binding.
Single upload
Use put() to upload an object in a single request. This is the simplest approach for small to medium objects.
export default {
async fetch(request: Request, env: Env): Promise<Response> {
try {
const object = await env.MY_BUCKET.put("image.png", request.body, {
httpMetadata: {
contentType: "image/png",
},
});
if (object === null) {
return new Response("Precondition failed or upload returned null", {
status: 412,
});
}
return Response.json({
key: object.key,
size: object.size,
etag: object.etag,
});
} catch (err) {
return new Response(`Upload failed: ${err}`, { status: 500 });
}
},
} satisfies ExportedHandler<Env>;
Multipart upload
Use createMultipartUpload() and resumeMultipartUpload() for large files or when you need to upload parts in parallel. Each part must be at least 5 MiB (except the last part).
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const key = "large-file.bin";
// Create a new multipart upload
const multipartUpload = await env.MY_BUCKET.createMultipartUpload(key);
try {
// In a real application, these would be actual data chunks.
// Each part except the last must be at least 5 MiB.
const firstChunk = new Uint8Array(5 * 1024 * 1024); // placeholder
const secondChunk = new Uint8Array(1024); // placeholder
const part1 = await multipartUpload.uploadPart(1, firstChunk);
const part2 = await multipartUpload.uploadPart(2, secondChunk);
// Complete the upload with all parts
const object = await multipartUpload.complete([part1, part2]);
return Response.json({
key: object.key,
etag: object.httpEtag,
});
} catch (err) {
// Abort on failure so incomplete uploads do not count against storage
await multipartUpload.abort();
return new Response(`Multipart upload failed: ${err}`, { status: 500 });
}
},
} satisfies ExportedHandler<Env>;
In most cases, the multipart state (the uploadId and uploaded part ETags) is tracked by the client sending requests to your Worker. The following example exposes an HTTP API that a client application can call to create, upload parts for, and complete a multipart upload:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.pathname.slice(1);
const action = url.searchParams.get("action");
if (!key || !action) {
return new Response("Missing key or action", { status: 400 });
}
switch (action) {
// Step 1: Client calls POST /<key>?action=mpu-create
case "mpu-create": {
const upload = await env.MY_BUCKET.createMultipartUpload(key);
return Response.json({ key: upload.key, uploadId: upload.uploadId });
}
// Step 2: Client calls PUT /<key>?action=mpu-uploadpart&uploadId=...&partNumber=...
case "mpu-uploadpart": {
const uploadId = url.searchParams.get("uploadId");
const partNumber = Number(url.searchParams.get("partNumber"));
if (!uploadId || !partNumber || !request.body) {
return new Response("Missing uploadId, partNumber, or body", {
status: 400,
});
}
const upload = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
try {
const part = await upload.uploadPart(partNumber, request.body);
return Response.json(part);
} catch (err) {
return new Response(String(err), { status: 400 });
}
}
// Step 3: Client calls POST /<key>?action=mpu-complete&uploadId=...
case "mpu-complete": {
const uploadId = url.searchParams.get("uploadId");
if (!uploadId) {
return new Response("Missing uploadId", { status: 400 });
}
const upload = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
const body = await request.json<{ parts: R2UploadedPart[] }>();
try {
const object = await upload.complete(body.parts);
return new Response(null, {
headers: { etag: object.httpEtag },
});
} catch (err) {
return new Response(String(err), { status: 400 });
}
}
// Abort an in-progress upload
case "mpu-abort": {
const uploadId = url.searchParams.get("uploadId");
if (!uploadId) {
return new Response("Missing uploadId", { status: 400 });
}
const upload = env.MY_BUCKET.resumeMultipartUpload(key, uploadId);
try {
await upload.abort();
} catch (err) {
return new Response(String(err), { status: 400 });
}
return new Response(null, { status: 204 });
}
default:
return new Response(`Unknown action: ${action}`, { status: 400 });
}
},
} satisfies ExportedHandler<Env>;
For the complete Workers API reference, refer to Workers API reference.
Presigned URLs (Workers)
When you need clients (browsers, mobile apps) to upload directly to R2 without proxying through your Worker, generate a presigned URL server-side and hand it to the client:
import { AwsClient } from "aws4fetch";
interface Env {
R2_ACCESS_KEY_ID: string;
R2_SECRET_ACCESS_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const r2 = new AwsClient({
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
});
// Generate a presigned PUT URL valid for 1 hour
const url = new URL(
"https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/image.png",
);
url.searchParams.set("X-Amz-Expires", "3600");
const signed = await r2.sign(new Request(url, { method: "PUT" }), {
aws: { signQuery: true },
});
// Return the signed URL to the client — they can PUT directly to R2
return Response.json({ url: signed.url });
},
} satisfies ExportedHandler<Env>;
For full presigned URL documentation including GET, PUT, and security best practices, refer to Presigned URLs.
Upload via S3 API
Use S3-compatible SDKs to upload objects. You will need your account ID and R2 API token.
Single upload
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { readFile } from "node:fs/promises";
const S3 = new S3Client({
region: "auto",
endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: "<ACCESS_KEY_ID>",
secretAccessKey: "<SECRET_ACCESS_KEY>",
},
});
const fileContent = await readFile("./image.png");
const response = await S3.send(
new PutObjectCommand({
Bucket: "my-bucket",
Key: "image.png",
Body: fileContent,
ContentType: "image/png",
}),
);
console.log(`Uploaded successfully. ETag: ${response.ETag}`);
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { readFile } from "node:fs/promises";
const S3 = new S3Client({
region: "auto",
endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: "<ACCESS_KEY_ID>",
secretAccessKey: "<SECRET_ACCESS_KEY>",
},
});
const fileContent = await readFile("./image.png");
const response = await S3.send(
new PutObjectCommand({
Bucket: "my-bucket",
Key: "image.png",
Body: fileContent,
ContentType: "image/png",
}),
);
console.log(`Uploaded successfully. ETag: ${response.ETag}`);
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",
)
with open("./image.png", "rb") as f:
response = s3.put_object(
Bucket="my-bucket",
Key="image.png",
Body=f,
ContentType="image/png",
)
print(f"Uploaded successfully. ETag: {response['ETag']}")
Multipart upload
Most S3 SDKs handle multipart uploads automatically when the file exceeds a configurable threshold. The examples below show both automatic (high-level) and manual (low-level) approaches.
Automatic multipart upload
The SDK splits the file and uploads parts in parallel.
import { S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { createReadStream } from "node:fs";
const S3 = new S3Client({
region: "auto",
endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: "<ACCESS_KEY_ID>",
secretAccessKey: "<SECRET_ACCESS_KEY>",
},
});
const upload = new Upload({
client: S3,
params: {
Bucket: "my-bucket",
Key: "large-file.bin",
Body: createReadStream("./large-file.bin"),
},
// Upload parts in parallel (default: 4)
leavePartsOnError: false,
});
upload.on("httpUploadProgress", (progress) => {
console.log(`Uploaded ${progress.loaded ?? 0} bytes`);
});
const result = await upload.done();
console.log(`Upload complete. ETag: ${result.ETag}`);
import { S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { createReadStream } from "node:fs";
const S3 = new S3Client({
region: "auto",
endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: "<ACCESS_KEY_ID>",
secretAccessKey: "<SECRET_ACCESS_KEY>",
},
});
const upload = new Upload({
client: S3,
params: {
Bucket: "my-bucket",
Key: "large-file.bin",
Body: createReadStream("./large-file.bin"),
},
leavePartsOnError: false,
});
upload.on("httpUploadProgress", (progress) => {
console.log(`Uploaded ${progress.loaded ?? 0} bytes`);
});
const result = await upload.done();
console.log(`Upload complete. ETag: ${result.ETag}`);
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",
)
# upload_file automatically uses multipart for large files.
# For better throughput with large objects, use the manual multipart example below.
s3.upload_file(
Filename="./large-file.bin",
Bucket="my-bucket",
Key="large-file.bin",
)
Manual multipart upload
Use the low-level API when you need full control over part sizes or upload order.
import {
S3Client,
CreateMultipartUploadCommand,
UploadPartCommand,
CompleteMultipartUploadCommand,
AbortMultipartUploadCommand,
type CompletedPart,
} from "@aws-sdk/client-s3";
import { createReadStream, statSync } from "node:fs";
const S3 = new S3Client({
region: "auto",
endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: "<ACCESS_KEY_ID>",
secretAccessKey: "<SECRET_ACCESS_KEY>",
},
});
const bucket = "my-bucket";
const key = "large-file.bin";
const partSize = 10 * 1024 * 1024; // 10 MiB per part
// Step 1: Create the multipart upload
const { UploadId } = await S3.send(
new CreateMultipartUploadCommand({ Bucket: bucket, Key: key }),
);
try {
const fileSize = statSync("./large-file.bin").size;
const partCount = Math.ceil(fileSize / partSize);
const parts: CompletedPart[] = [];
// Step 2: Upload each part
for (let i = 0; i < partCount; i++) {
const start = i * partSize;
const end = Math.min(start + partSize, fileSize);
const { ETag } = await S3.send(
new UploadPartCommand({
Bucket: bucket,
Key: key,
UploadId,
PartNumber: i + 1,
Body: createReadStream("./large-file.bin", { start, end: end - 1 }),
ContentLength: end - start,
}),
);
parts.push({ PartNumber: i + 1, ETag });
}
// Step 3: Complete the upload
await S3.send(
new CompleteMultipartUploadCommand({
Bucket: bucket,
Key: key,
UploadId,
MultipartUpload: { Parts: parts },
}),
);
console.log("Multipart upload complete.");
} catch (err) {
// Abort on failure to clean up incomplete parts
try {
await S3.send(
new AbortMultipartUploadCommand({ Bucket: bucket, Key: key, UploadId }),
);
} catch (_abortErr) {
// Best-effort cleanup — the original error is more important
}
throw err;
}
import {
S3Client,
CreateMultipartUploadCommand,
UploadPartCommand,
CompleteMultipartUploadCommand,
AbortMultipartUploadCommand,
} from "@aws-sdk/client-s3";
import { createReadStream, statSync } from "node:fs";
const S3 = new S3Client({
region: "auto",
endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: "<ACCESS_KEY_ID>",
secretAccessKey: "<SECRET_ACCESS_KEY>",
},
});
const bucket = "my-bucket";
const key = "large-file.bin";
const partSize = 10 * 1024 * 1024; // 10 MiB per part
// Step 1: Create the multipart upload
const { UploadId } = await S3.send(
new CreateMultipartUploadCommand({ Bucket: bucket, Key: key }),
);
try {
const fileSize = statSync("./large-file.bin").size;
const partCount = Math.ceil(fileSize / partSize);
const parts = [];
// Step 2: Upload each part
for (let i = 0; i < partCount; i++) {
const start = i * partSize;
const end = Math.min(start + partSize, fileSize);
const { ETag } = await S3.send(
new UploadPartCommand({
Bucket: bucket,
Key: key,
UploadId,
PartNumber: i + 1,
Body: createReadStream("./large-file.bin", { start, end: end - 1 }),
ContentLength: end - start,
}),
);
parts.push({ PartNumber: i + 1, ETag });
}
// Step 3: Complete the upload
await S3.send(
new CompleteMultipartUploadCommand({
Bucket: bucket,
Key: key,
UploadId,
MultipartUpload: { Parts: parts },
}),
);
console.log("Multipart upload complete.");
} catch (err) {
// Abort on failure to clean up incomplete parts
try {
await S3.send(
new AbortMultipartUploadCommand({ Bucket: bucket, Key: key, UploadId }),
);
} catch (_abortErr) {
// Best-effort cleanup — the original error is more important
}
throw err;
}
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 # Number of parallel upload threads
# 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:
# Abort on failure to clean up incomplete parts
if upload_id:
try:
s3.abort_multipart_upload(Bucket=bucket, Key=key, UploadId=upload_id)
except Exception:
pass # Best-effort cleanup — the original error is more important
raise
Presigned URLs (S3 API)
For client-side uploads where users upload directly to R2 without going through your server, generate a presigned PUT URL. Your server creates the URL and the client uploads to it — no API credentials are exposed to the client.
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const S3 = new S3Client({
region: "auto",
endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: "<ACCESS_KEY_ID>",
secretAccessKey: "<SECRET_ACCESS_KEY>",
},
});
const presignedUrl = await getSignedUrl(
S3,
new PutObjectCommand({
Bucket: "my-bucket",
Key: "user-upload.png",
ContentType: "image/png",
}),
{ expiresIn: 3600 }, // Valid for 1 hour
);
console.log(presignedUrl);
// Return presignedUrl to the client
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const S3 = new S3Client({
region: "auto",
endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: "<ACCESS_KEY_ID>",
secretAccessKey: "<SECRET_ACCESS_KEY>",
},
});
const presignedUrl = await getSignedUrl(
S3,
new PutObjectCommand({
Bucket: "my-bucket",
Key: "user-upload.png",
ContentType: "image/png",
}),
{ expiresIn: 3600 }, // Valid for 1 hour
);
console.log(presignedUrl);
// Return presignedUrl to the client
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",
)
presigned_url = s3.generate_presigned_url(
"put_object",
Params={
"Bucket": "my-bucket",
"Key": "user-upload.png",
"ContentType": "image/png",
},
ExpiresIn=3600, # Valid for 1 hour
)
print(presigned_url)
# Return presigned_url to the client
For full presigned URL documentation, refer to Presigned URLs.
Refer to R2's S3 API documentation for all supported S3 API methods.
Upload via CLI
Rclone
Rclone is a command-line tool for managing files on cloud storage. Rclone works well for uploading multiple files from your local machine or copying data from other cloud storage providers.
To use rclone, install it onto your machine using their official documentation - Install rclone.
Upload files with the rclone copy command:
# Upload a single file
rclone copy /path/to/local/image.png r2:bucket_name
# Upload everything in a directory
rclone copy /path/to/local/folder r2:bucket_name
Verify the upload with rclone ls:
rclone ls r2:bucket_name
For more information, refer to our rclone example.
Wrangler
:::note
Wrangler supports uploading files up to 315 MB and only allows one object at a time. For large files or bulk uploads, use rclone or another S3-compatible tool.
:::
Use Wrangler to upload objects. Run the r2 object put command:
wrangler r2 object put test-bucket/image.png --file=image.png
You can set the Content-Type (MIME type), Content-Disposition, Cache-Control and other HTTP header metadata through optional flags.
Multipart upload details
Part size limits
- Minimum part size: 5 MiB (except for the last part)
- Maximum part size: 5 GiB
- Maximum number of parts: 10,000
- All parts except the last must be the same size
Incomplete upload lifecycles
Incomplete multipart uploads are automatically aborted after 7 days by default. You can change this by configuring a custom lifecycle policy.
ETags
For example, if a two-part upload has part ETags bce6bf66aeb76c7040fdd5f4eccb78e6 and 8165449fc15bbf43d3b674595cbcc406, the completed object's ETag will be f77dc0eecdebcd774a2a22cb393ad2ff-2.
Related resources
Audit Logs
Audit logs provide a comprehensive summary of changes made within your Cloudflare account, including those made to R2 buckets. This functionality is available on all plan types, free of charge, and is always enabled.
Viewing audit logs
To view audit logs for your R2 buckets, go to the Audit logs page.
For more information on how to access and use audit logs, refer to Review audit logs.
Logged operations
The following configuration actions are logged:
| Operation | Description |
|---|---|
| CreateBucket | Creation of a new bucket. |
| DeleteBucket | Deletion of an existing bucket. |
| AddCustomDomain | Addition of a custom domain to a bucket. |
| RemoveCustomDomain | Removal of a custom domain from a bucket. |
| ChangeBucketVisibility | Change to the managed public access (r2.dev) settings of a bucket. |
| PutBucketStorageClass | Change to the default storage class of a bucket. |
| PutBucketLifecycleConfiguration | Change to the object lifecycle configuration of a bucket. |
| DeleteBucketLifecycleConfiguration | Deletion of the object lifecycle configuration for a bucket. |
| PutBucketCors | Change to the CORS configuration for a bucket. |
| DeleteBucketCors | Deletion of the CORS configuration for a bucket. |
:::note
Audit Logs do not include data access operations, such as GetObject and PutObject. To record supported object operations with response status codes below 400, use R2 Data Access Logs.
Data Access Logs also include supported requests to public R2 buckets through r2.dev or custom domains.
:::
Example log entry
Below is an example of an audit log entry showing the creation of a new bucket:
{
"action": { "info": "CreateBucket", "result": true, "type": "create" },
"actor": {
"email": "<ACTOR_EMAIL>",
"id": "3f7b730e625b975bc1231234cfbec091",
"ip": "fe32:43ed:12b5:526::1d2:13",
"type": "user"
},
"id": "5eaeb6be-1234-406a-87ab-1971adc1234c",
"interface": "API",
"metadata": { "zone_name": "r2.cloudflarestorage.com" },
"newValue": "",
"newValueJson": {},
"oldValue": "",
"oldValueJson": {},
"owner": { "id": "1234d848c0b9e484dfc37ec392b5fa8a" },
"resource": { "id": "my-bucket", "type": "r2.bucket" },
"when": "2024-07-15T16:32:52.412Z"
}
Event subscriptions
import { Render } from "~/components";
Available R2 events
Available Super Slurper events
Limits
import { Render } from "~/components";
| Feature | Limit |
|---|---|
| Data storage per bucket | Unlimited |
| Number of objects per bucket | Unlimited |
| Maximum number of buckets per account | Unlimited, with a default limit of 100,000. Each bucket above 100,000 costs $0.10 per month. |
| Maximum rate of bucket management operations per bucket [^1] | 50 per second |
| Number of custom domains per bucket | 100 |
| Object key length | 1,024 bytes |
| Object metadata size | 8,192 bytes |
| Object size | 5 TiB per object [^2] |
| Maximum upload size [^3] | 5 GiB (single-part) / 4.995 TiB (multi-part) [^4] |
| Maximum upload parts | 10,000 |
| Maximum concurrent writes to the same object name (key) | 1 per second [^5] |
[^1]: Bucket management operations include creating, deleting, listing, and configuring buckets. This limit does not apply to reading or writing objects to a bucket.
[^2]: The object size limit is 5 GiB less than 5 TiB, so 4.995 TiB.
[^3]: Max upload size applies to uploading a file via one request, uploading a part of a multipart upload, or copying into a part of a multipart upload. If you have a Worker, its inbound request size is constrained by Workers request limits. The max upload size limit does not apply to subrequests.
[^4]: The max upload size is 5 MiB less than 5 GiB, so 4.995 GiB.
[^5]: Concurrent writes to the same object name (key) at a higher rate return HTTP 429 (rate limited) responses.
Limits specified in MiB (mebibyte), GiB (gibibyte), or TiB (tebibyte) are storage units of measurement based on base-2. 1 GiB (gibibyte) is equivalent to 230 bytes (or 10243 bytes). This is distinct from 1 GB (gigabyte), which is 109 bytes (or 10003 bytes).
Rate limiting on managed public buckets through r2.dev
Managed public bucket access through an r2.dev subdomain is not intended for production usage and has a variable rate limit applied to it. The r2.dev endpoint for your bucket is designed to enable testing.
- If you exceed the rate limit (hundreds of requests/second), requests to your
r2.devendpoint will be temporarily throttled and you will receive a429 Too Many Requestsresponse. - Bandwidth (throughput) may also be throttled when using the
r2.devendpoint.
For production use cases, connect a custom domain to your bucket. Custom domains allow you to serve content from a domain you control (for example, assets.example.com), configure fine-grained caching, set up redirect and rewrite rules, mutate content via Cloudflare Workers, and get detailed URL-level analytics for content served from your R2 bucket.
Cloudflare REST API
The Cloudflare REST API is rate limited to 1,200 requests per five minutes across all R2 REST API operations on your account.
For high-throughput object operations, use the S3-compatible API or Workers API instead. The Cloudflare REST API is best suited for lower-volume management and configuration operations.
Metrics and analytics
R2 exposes analytics for requests, storage, and bandwidth usage across your buckets.
The metrics displayed for a bucket in the Cloudflare dashboard are queried from Cloudflare's GraphQL Analytics API. You can access the metrics programmatically via GraphQL or HTTP client.
Metrics
R2 has three datasets:
| Dataset | GraphQL Dataset Name | Description |
|---|---|---|
| Operations | r2OperationsAdaptiveGroups |
This dataset consists of the operations taken on a bucket within an account. |
| Storage | r2StorageAdaptiveGroups |
This dataset consists of the storage of a bucket within an account. |
| Bandwidth | r2BandwidthUsageAdaptiveGroups |
Dataset contains uploaded and downloaded bytes for buckets within an account. |
Metrics can be queried for a maximum range of 31 days. These datasets require an accountTag filter with your Cloudflare account ID.
:::caution[Querying buckets with jurisdiction restriction] In your account, you may have two buckets of the same name, one with a specified jurisdiction, and one without.
Therefore, if you want to query metrics about a bucket which has a specified jurisdiction, you must include the jurisdiction followed by an underscore before the bucket name. For example: eu_bucket-name. This ensures you query the correct bucket.
:::
Operations dataset
| Field | Description |
|---|---|
| actionType | The name of the operation performed. |
| actionStatus | The status of the operation. Can be success, userError, or internalError. |
| bucketName | The bucket this operation was performed on if applicable. For buckets with a jurisdiction specified, you must include the jurisdiction followed by an underscore before the bucket name. For example: eu_your-bucket-name |
| objectName | The object this operation was performed on if applicable. |
| responseStatusCode | The http status code returned by this operation. |
| datetime | The time of the request. |
Storage dataset
| Field | Description |
|---|---|
| bucketName | The bucket this storage value is for. For buckets with a jurisdiction specified, you must include the jurisdiction followed by an underscore before the bucket name. For example: eu_your-bucket-name |
| payloadSize | The size of the objects in the bucket. |
| metadataSize | The size of the metadata of the objects in the bucket. |
| objectCount | The number of objects in the bucket. |
| uploadCount | The number of pending multipart uploads in the bucket. |
| datetime | The time that this storage value represents. |
Bandwidth dataset
The dataset excludes bandwidth transfers smaller than 100 KiB. This threshold applies to transfer size, not object size.
You can filter by the following fields:
| Field | Description |
|---|---|
bucketName |
The R2 bucket identifier, including the jurisdiction prefix if applicable. |
date |
The transfer timestamp truncated to the start of the day. |
datetimeHour |
The transfer timestamp truncated to the start of the hour. |
datetimeFifteenMinutes |
The transfer timestamp rounded down to the nearest quarter hour. |
datetimeFiveMinutes |
The transfer timestamp truncated to the start of the five-minute interval. |
datetimeMinute |
The transfer timestamp truncated to the start of the minute. |
datetime |
The raw transfer timestamp. |
The following fields can be summed:
| Field | Description |
|---|---|
bytesDownload |
Downloaded bytes. |
bytesUpload |
Uploaded bytes. |
View via the dashboard
Per-bucket analytics for R2 are available in the Cloudflare dashboard. To view current and historical metrics for a bucket:
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Select your bucket.
-
Select the Metrics tab.
You can optionally select a time window to query. This defaults to the last 24 hours.
Query via the GraphQL API
You can programmatically query analytics for your R2 buckets via the GraphQL Analytics API. This API queries the same dataset as the Cloudflare dashboard, and supports GraphQL introspection.
Examples
Operations
To query the volume of each operation type on a bucket for a given time period you can run a query as such
query R2VolumeExample(
$accountTag: string!
$startDate: Time
$endDate: Time
$bucketName: string
) {
viewer {
accounts(filter: { accountTag: $accountTag }) {
r2OperationsAdaptiveGroups(
limit: 10000
filter: {
datetime_geq: $startDate
datetime_leq: $endDate
bucketName: $bucketName
}
) {
sum {
requests
}
dimensions {
actionType
}
}
}
}
}
The bucketName field can be removed to get an account level overview of operations. The volume of operations can be broken down even further by adding more dimensions to the query.
Storage
To query the storage of a bucket over a given time period you can run a query as such.
query R2StorageExample(
$accountTag: string!
$startDate: Time
$endDate: Time
$bucketName: string
) {
viewer {
accounts(filter: { accountTag: $accountTag }) {
r2StorageAdaptiveGroups(
limit: 10000
filter: {
datetime_geq: $startDate
datetime_leq: $endDate
bucketName: $bucketName
}
orderBy: [datetime_DESC]
) {
max {
objectCount
uploadCount
payloadSize
metadataSize
}
dimensions {
datetime
}
}
}
}
}
Bandwidth
To query hourly uploads and downloads for a bucket. Results contain byte totals, not transfer rates, and excluded transfers smaller than 100 KiB.
query R2BandwidthExample(
$accountTag: string!
$startDate: Time!
$endDate: Time!
$bucketName: string!
) {
viewer {
accounts(filter: { accountTag: $accountTag }) {
r2BandwidthUsageAdaptiveGroups(
limit: 1000
filter: {
datetime_geq: $startDate
datetime_lt: $endDate
bucketName: $bucketName
}
orderBy: [datetimeHour_ASC]
) {
sum {
bytesUpload
bytesDownload
}
dimensions {
datetimeHour
}
}
}
}
}
Release notes
import { ProductReleaseNotes } from "~/components";
{/* */}
Troubleshooting
import { FileTree } from "~/components";
Troubleshooting 403 / CORS issues with R2
If you are encountering a CORS error despite setting up everything correctly, you may follow this troubleshooting guide to help you.
If you see a 401/403 error above the CORS error in your browser console, you are dealing with a different issue (not CORS related).
If you do have a CORS issue, refer to Resolving CORS issues.
If you are using a custom domain
- Open developer tools on your browser.
- Go to the Network tab and find the failing request. You may need to reload the page, as requests are only logged after developer tools have been opened.
- Check the response headers for the following two headers:
cf-cache-statuscf-mitigated
If you have a cf-mitigated header
Your request was blocked by one of your WAF rules. Inspect your Security Events to identify the cause of the block.
If you do not have a cf-cache-status header
Your request was blocked by Hotlink Protection.
Edit your Hotlink Protection settings using a Configuration Rule, or disable it completely.
If you are using the S3 API
Your request may be incorrectly signed. You may obtain a better error message by trying the request over curl.
Refer to the working S3 signing examples on the Examples page.
If it is actually CORS
Here are some common issues with CORS configurations:
ExposeHeadersis missing headers likeETagAllowedHeadersis missing headers likeAuthorizationorContent-TypeAllowedMethodsis missing methods likePOST/PUT
Object-level API tokens fail against the REST API
If you use an R2 API token created with the Object Read & Write or Object Read only permissions against the Cloudflare REST API (api.cloudflare.com), object requests fail to authenticate and return one of the following:
- When the token applies to all buckets:
{"code":10002,"message":"Unauthorized"}(HTTP 401). - When the token is scoped to specific buckets:
{"code":10000,"message":"Authentication error"}(HTTP 403).
Object-level tokens are only supported by the S3-compatible API, which authenticates with AWS Signature Version 4 (SigV4).
To resolve this:
- To keep using an Object-level token, make object requests through the S3-compatible API instead of the REST API. The S3-compatible API is also better suited for object operations: the REST API is rate limited.
- To use the REST API, authenticate with an Admin Read & Write or Admin Read only token. Admin tokens grant account-wide access rather than bucket-scoped access.
HTTP 5XX Errors and capacity limitations of Cloudflare R2
When you encounter an HTTP 5XX error, it is usually a sign that your Cloudflare R2 bucket has been overwhelmed by too many concurrent requests. These errors can trigger bucket-wide read and write locks, affecting the performance of all ongoing operations.
To avoid these disruptions, it is important to implement strategies for managing request volume.
Here are some mitigations you can employ:
Monitor concurrent requests
Track the number of concurrent requests to your bucket. If a client encounters a 5XX error, ensure that it retries the operation and communicates with other clients. By coordinating, clients can collectively slow down, reducing the request rate and maintaining a more stable flow of successful operations.
If your users are directly uploading to the bucket (for example, using the S3 or Workers API), you may not be able to monitor or enforce a concurrency limit. In that case, we recommend bucket sharding.
Bucket sharding
For higher capacity at the cost of added complexity, consider bucket sharding. This approach distributes reads and writes across multiple buckets, reducing the load on any single bucket. While sharding cannot prevent a single hot object from exhausting capacity, it can mitigate the overall impact and improve system resilience.
Objects named This object is unnamed
In the Cloudflare dashboard, you can choose to view objects with / in the name as folders by selecting View prefixes as directories.
For example, an object named example/object will be displayed as below.
Object names which end with / will cause the Cloudflare dashboard to render the object as a folder with an unnamed object inside.
For example, uploading an object named example/ into an R2 bucket will be displayed as below.
Consistency model
This page details R2's consistency model, including where R2 is strongly, globally consistent and which operations this applies to.
R2 can be described as "strongly consistent", especially in comparison to other distributed object storage systems. This strong consistency ensures that operations against R2 see the latest (accurate) state: clients should be able to observe the effects of any write, update and/or delete operation immediately, globally.
Terminology
In the context of R2, strong consistency and eventual consistency have the following meanings:
- Strongly consistent - The effect of an operation will be observed globally, immediately, by all clients. Clients will not observe 'stale' (inconsistent) state.
- Eventually consistent - Clients may not see the effect of an operation immediately. The state may take a some time (typically seconds to a minute) to propagate globally.
Operations and Consistency
Operations against R2 buckets and objects adhere to the following consistency guarantees:
| Action | Consistency |
|---|---|
| Read-after-write: Write (upload) an object, then read it | Strongly consistent: readers will immediately see the latest object globally |
| Metadata: Update an object's metadata | Strongly consistent: readers will immediately see the updated metadata globally |
| Deletion: Delete an object | Strongly consistent: reads to that object will immediately return a "does not exist" error |
| Object listing: List the objects in a bucket | Strongly consistent: the list operation will list all objects at that point in time |
| IAM: Adding/removing R2 Storage permissions | Eventually consistent: A new or updated API key may take up to a minute to have permissions reflected globally |
Additional notes:
- In the event two clients are writing (
PUTorDELETE) to the same key, the last writer to complete "wins". - When performing a multipart upload, read-after-write consistency continues to apply once all parts have been successfully uploaded. In the case the same part is uploaded (in error) from multiple writers, the last write will win.
- Copying an object within the same bucket also follows the same read-after-write consistency that writing a new object would. The "copied" object is immediately readable by all clients once the copy operation completes.
- To delete an R2 bucket, it must be completely empty before deletion is allowed. If you attempt to delete a bucket that still contains objects, you will receive an error such as:
The bucket you tried to delete (X) is not empty (account Y)orBucket X cannot be deleted because it isn’t empty.For instructions on emptying and deleting a bucket, refer to Delete buckets.
Conditional writes
R2 evaluates conditional write headers (for example If-Match and If-None-Match on PutObject) atomically, as part of the same write that commits the object:
- The condition is always evaluated against the committed state of the bucket at the moment the write commits, never against an older snapshot.
- Competing conditional writes to the same key are serialized. When two or more clients concurrently issue
PutObjectwithIf-None-Match: *for a previously absent key, at most one request succeeds and every other request fails with a412 PreconditionFailederror. It is therefore safe to rely onIf-None-Match: *for "create-once" semantics: two concurrent conditional writes can never both observe success.
For S3 API usage, refer to Conditional operations in PutObject.
Negative reads and in-flight writes
A strongly consistent read (HeadObject or GetObject) that returns NoSuchKey (404) tells you that the object did not exist at the moment the read was processed. This absence is not final with respect to an earlier PutObject whose outcome is still unknown: a request that was still in flight — for example, a client that lost its connection before receiving the response — may still commit after the negative read has returned. Reads are never stale, but they do not wait for or cancel earlier writes.
If you need to treat a negative read as proof that a write failed, first make sure the original write has definitively completed (either succeeded or returned a definitive error) before trusting the read. Alternatively, retry the write with If-None-Match: *: it fails with 412 PreconditionFailed if the ambiguous write did in fact commit.
Caching
:::note
By default, Cloudflare's cache will cache common, cacheable status codes automatically per our cache documentation.
:::
When connecting a custom domain to an R2 bucket and enabling caching for objects served from that bucket, the consistency model is necessarily relaxed when accessing content via a domain with caching enabled.
Specifically, you should expect:
- An object you delete from R2, but that is still cached, will still be available. You should purge the cache after deleting objects if you need that delete to be reflected.
- By default, Cloudflare’s cache will cache HTTP 404 (Not Found) responses automatically. If you upload an object to that same path, the cache may continue to return HTTP 404s until the cache TTL (Time to Live) expires and the new object is fetched from R2 or the cache is purged.
- An object for a given key is overwritten with a new object: the old (previous) object will continue to be served to clients until the cache TTL expires (or the object is evicted) or the cache is purged.
The cache does not affect access via Worker API bindings or the S3 API, as these operations are made directly against the bucket and do not transit through the cache.
Data location
import { WranglerConfig, DashButton } from "~/components";
Learn how the location of data stored in R2 is determined and about the different available inputs that control the physical location where objects in your buckets are stored.
Automatic (recommended)
When you create a new bucket, the data location is set to Automatic by default. Currently, this option chooses a bucket location in the closest available region to the create bucket request based on the location of the caller.
Location Hints
Location Hints are optional parameters you can provide during bucket creation to indicate the primary geographical location you expect data will be accessed from.
Using Location Hints can be a good choice when you expect the majority of access to data in a bucket to come from a different location than where the create bucket request originates. Keep in mind Location Hints are a best effort and not a guarantee, and they should only be used as a way to optimize performance by placing regularly updated content closer to users.
Set hints via the Cloudflare dashboard
You can choose to automatically create your bucket in the closest available region based on your location or choose a specific location from the list.
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Select Create bucket.
-
Enter a name for the bucket.
-
Under Location, leave None selected for automatic selection or choose a region from the list.
-
Select Create bucket to complete the bucket creation process.
Set hints via the S3 API
You can set the Location Hint via the LocationConstraint parameter using the S3 API:
await S3.send(
new CreateBucketCommand({
Bucket: "YOUR_BUCKET_NAME",
CreateBucketConfiguration: {
LocationConstraint: "WNAM",
},
}),
);
Refer to Examples for additional examples from other S3 SDKs.
Available hints
The following hint locations are supported:
| Hint | Hint description |
|---|---|
| wnam | Western North America |
| enam | Eastern North America |
| weur | Western Europe |
| eeur | Eastern Europe |
| apac | Asia-Pacific |
| oc | Oceania |
Additional considerations
Location Hints are only honored the first time a bucket with a given name is created. If you delete and recreate a bucket with the same name, the original bucket’s location will be used.
Jurisdictional Restrictions
Jurisdictional Restrictions guarantee objects in a bucket are stored within a specific jurisdiction.
Use Jurisdictional Restrictions when you need to ensure data is stored and processed within a jurisdiction to meet data residency requirements, including local regulations such as the GDPR or FedRAMP.
Set jurisdiction via the Cloudflare dashboard
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Select Create bucket.
-
Enter a name for the bucket.
-
Under Location, select Specify jurisdiction and choose a jurisdiction from the list.
-
Select Create bucket to complete the bucket creation process.
Using jurisdictions from Workers
To access R2 buckets that belong to a jurisdiction from Workers, you will need to specify the jurisdiction as well as the bucket name as part of your bindings in your Wrangler configuration file:
{
"r2_buckets": [
{
"binding": "MY_BUCKET",
"bucket_name": "<YOUR_BUCKET_NAME>",
"jurisdiction": "<JURISDICTION>"
}
]
}
For more information on getting started, refer to Use R2 from Workers.
Using jurisdictions with the S3 API
When interacting with R2 resources that belong to a defined jurisdiction with the S3 API or existing S3-compatible SDKs, you must specify the jurisdiction in your S3 endpoint:
https://<ACCOUNT_ID>.<JURISDICTION>.r2.cloudflarestorage.com
You can use your jurisdiction-specific endpoint for any supported S3 API operations. When using a jurisdiction endpoint, you will not be able to access R2 resources outside of that jurisdiction.
The example below shows how to create an R2 bucket in the eu jurisdiction using the @aws-sdk/client-s3 package for JavaScript.
import { S3Client, CreateBucketCommand } from "@aws-sdk/client-s3";
const S3 = new S3Client({
endpoint: "https://<account_id>.eu.r2.cloudflarestorage.com",
credentials: {
accessKeyId: "<access_key_id>",
secretAccessKey: "<access_key_secret>",
},
region: "auto",
});
await S3.send(
new CreateBucketCommand({
Bucket: "YOUR_BUCKET_NAME",
}),
);
Refer to Examples for additional examples from other S3 SDKs.
Available jurisdictions
The following jurisdictions are supported:
| Jurisdiction | Jurisdiction description |
|---|---|
| eu | European Union |
| fedramp | FedRAMP |
| us | United States |
:::note
Cloudflare Enterprise customers may contact their account team or Cloudflare Support to get access to the FedRAMP jurisdiction. :::
Limitations
The following services do not interact with R2 resources with assigned jurisdictions:
- Logpush. As a workaround to this limitation, you can set up a Logpush job using an S3-compatible endpoint to store logs in an R2 bucket in the jurisdiction of your choice.
Additional considerations
Once an R2 bucket is created, the jurisdiction cannot be changed.
Data security
This page details the data security properties of R2, including encryption-at-rest (EAR), encryption-in-transit (EIT), and Cloudflare's compliance certifications.
Encryption at Rest
All objects stored in R2, including their metadata, are encrypted at rest. Encryption and decryption are automatic, do not require user configuration to enable, and do not impact the effective performance of R2.
Encryption keys are managed by Cloudflare and securely stored in the same key management systems we use for managing encrypted data across Cloudflare internally.
Objects are encrypted using AES-256, a widely tested, highly performant and industry-standard encryption algorithm. R2 uses GCM (Galois/Counter Mode) as its preferred mode.
Encryption in Transit
Data transfer between a client and R2 is secured using the same Transport Layer Security (TLS/SSL) supported on all Cloudflare domains.
Access over plaintext HTTP (without TLS/SSL) can be disabled by connecting a custom domain to your R2 bucket and enabling Always Use HTTPS.
:::note
R2 custom domains use Cloudflare for SaaS certificates and cannot be customized. Even if you have Advanced Certificate Manager, the advanced certificate will not be used due to certificate prioritization.
:::
Compliance
To learn more about Cloudflare's adherence to industry-standard security compliance certifications, visit the Cloudflare Trust Hub.
Durability
R2 is designed to provide 99.999999999% (eleven 9s) of annual durability. This means that if you store 10,000,000 objects on R2, you can expect to lose an object once every 10,000 years on average.
How R2 achieves eleven-nines durability
R2's durability is built on multiple layers of redundancy and data protection:
-
Replication: When you upload an object, R2 stores multiple "copies" of that object through either full replication and/or erasure coding. This ensures that the full or partial failure of any individual disk does not result in data loss. Erasure coding distributes parts of the object across multiple disks, ensuring that even if some disks fail, the object can still be reconstructed from a subset of the available parts, preventing hardware failure or physical impacts to data centers (such as fire or floods) from causing data loss.
-
Hardware redundancy: Storage clusters are comprised of hardware distributed across several data centers within a geographic region. This physical distribution ensures that localized failures—such as power outages, network disruptions, or hardware malfunctions at a single facility—do not result in data loss.
-
Synchronous writes: R2 returns an
HTTP 200 (OK)for a write via API or otherwise indicates success only when data has been persisted to disk. We do not rely on asynchronous replication to support underlying durability guarantees. This is critical to R2’s consistency guarantees and mitigates the chance of a client receiving a successful API response without the underlying metadata and storage infrastructure having persisted the change.
Considerations
- Durability is not a guarantee of data availability. It is a measure of the likelihood of data loss.
- R2 provides an availability SLA of 99.9%
- Durability does not prevent intentional or accidental deletion of data. Use bucket locks and/or bucket-scoped API tokens to limit access to data.
- Durability is also distinct from consistency, which describes how reads and writes are reflected in the system's state (e.g. eventual consistency vs. strong consistency).
Reference
import { DirectoryListing } from "~/components";
Unicode interoperability
R2 is built on top of Workers and supports Unicode natively. One nuance of Unicode that is often overlooked is the issue of filename interoperability due to Unicode equivalence.
Based on feedback from our users, we have chosen to NFC-normalize key names before storing by default. This means that Héllo and Héllo, for example, are the same object in R2 but different objects in other storage providers. Although Héllo and Héllo may be different character byte sequences, they are rendered the same.
R2 preserves the encoding for display though. When you list the objects, you will get back the last encoding you uploaded with.
There are still some platform-specific differences to consider:
- Windows and macOS filenames are case-insensitive while R2 and Linux are not.
- Windows console support for Unicode can be error-prone. Make sure to run
chcp 65001before using command-line tools or use Cygwin if your object names appear to be incorrect. - Linux allows distinct files that are unicode-equivalent because filenames are byte streams. Unicode-equivalent filenames on Linux will point to the same R2 object.
If it is important for you to be able to bypass the unicode equivalence and use byte-oriented key names, contact your Cloudflare account team.
Wrangler commands
import { Render } from "~/components";
Snowflake
import { Render } from "~/components";
This page details which R2 location or jurisdiction is recommended based on your Snowflake region.
You have the following inputs to control the physical location where objects in your R2 buckets are stored (for more information refer to data location):
- Location hints: Specify a geophrical area (for example, Asia-Pacific or Western Europe). R2 makes a best effort to place your bucket in or near that location to optimize performance. You can confirm bucket placement after creation by navigating to the Settings tab of your bucket and referring to the Bucket details section.
- Jurisdictions: Enforce that data is both stored and processed within a specific jurisdiction (for example, US, EU, or FedRAMP environments). Use jurisdictions when you need to ensure data is stored and processed within a jurisdiction to meet data residency requirements, including local regulations such as the GDPR or FedRAMP.
North and South America (Commercial)
| Snowflake region name | Cloud | Region ID | Recommended R2 location |
|---|---|---|---|
| Canada (Central) | AWS | ca-central-1 |
Location hint: enam |
| South America (Sao Paulo) | AWS | sa-east-1 |
Location hint: enam |
| US West (Oregon) | AWS | us-west-2 |
Jurisdiction: us or hint: wnam |
| US East (Ohio) | AWS | us-east-2 |
Jurisdiction: us or hint: enam |
| US East (N. Virginia) | AWS | us-east-1 |
Jurisdiction: us or hint: enam |
| US Central1 (Iowa) | GCP | us-central1 |
Jurisdiction: us or hint: enam |
| US East4 (N. Virginia) | GCP | us-east4 |
Jurisdiction: us or hint: enam |
| Canada Central (Toronto) | Azure | canadacentral |
Location hint: enam |
| Central US (Iowa) | Azure | centralus |
Jurisdiction: us or hint: enam |
| East US 2 (Virginia) | Azure | eastus2 |
Jurisdiction: us or hint: enam |
| Mexico Central (Mexico City) | Azure | mexicocentral |
Location hint: wnam |
| South Central US (Texas) | Azure | southcentralus |
Jurisdiction: us or hint: enam |
| West US 2 (Washington) | Azure | westus2 |
Jurisdiction: us or hint: wnam |
U.S. Government
| Snowflake region name | Cloud | Region ID | Recommended R2 location |
|---|---|---|---|
| US Gov East 1 | AWS | us-gov-east-1 |
Jurisdiction: fedramp |
| US Gov West 1 | AWS | us-gov-west-1 |
Jurisdiction: fedramp |
| US Gov Virginia | Azure | usgovvirginia |
Jurisdiction: fedramp |
:::note
Cloudflare Enterprise customers may contact their account team or Cloudflare Support to get access to the FedRAMP jurisdiction. :::
Europe and Middle East
| Snowflake region name | Cloud | Region ID | Recommended R2 location |
|---|---|---|---|
| EU (Frankfurt) | AWS | eu-central-1 |
Jurisdiction: eu or hint: weur/eeur |
| EU (Zurich) | AWS | eu-central-2 |
Jurisdiction: eu or hint: weur/eeur |
| EU (Stockholm) | AWS | eu-north-1 |
Jurisdiction: eu or hint: weur/eeur |
| EU (Ireland) | AWS | eu-west-1 |
Jurisdiction: eu or hint: weur/eeur |
| Europe (London) | AWS | eu-west-2 |
Jurisdiction: eu or hint: weur/eeur |
| EU (Paris) | AWS | eu-west-3 |
Jurisdiction: eu or hint: weur/eeur |
| Middle East Central2 (Dammam) | GCP | me-central2 |
Location hint: weur/eeur |
| Europe West2 (London) | GCP | europe-west-2 |
Jurisdiction: eu or hint: weur/eeur |
| Europe West3 (Frankfurt) | GCP | europe-west-3 |
Jurisdiction: eu or hint: weur/eeur |
| Europe West4 (Netherlands) | GCP | europe-west-4 |
Jurisdiction: eu or hint: weur/eeur |
| North Europe (Ireland) | Azure | northeurope |
Jurisdiction: eu or hint: weur/eeur |
| Switzerland North (Zurich) | Azure | switzerlandnorth |
Jurisdiction: eu or hint: weur/eeur |
| West Europe (Netherlands) | Azure | westeurope |
Jurisdiction: eu or hint: weur/eeur |
| UAE North (Dubai) | Azure | uaenorth |
Location hint: weur/eeur |
| UK South (London) | Azure | uksouth |
Jurisdiction: eu or hint: weur/eeur |
Asia Pacific and China
| Snowflake region name | Cloud | Region ID | Recommended R2 location |
|---|---|---|---|
| Asia Pacific (Tokyo) | AWS | ap-northeast-1 |
Location hint: apac |
| Asia Pacific (Seoul) | AWS | ap-northeast-2 |
Location hint: apac |
| Asia Pacific (Osaka) | AWS | ap-northeast-3 |
Location hint: apac |
| Asia Pacific (Mumbai) | AWS | ap-south-1 |
Location hint: apac |
| Asia Pacific (Singapore) | AWS | ap-southeast-1 |
Location hint: apac |
| Asia Pacific (Sydney) | AWS | ap-southeast-2 |
Location hint: oc |
| Asia Pacific (Jakarta) | AWS | ap-southeast-3 |
Location hint: apac |
| China (Ningxia) | AWS | cn-northwest-1 |
Location hint: apac |
| Australia East (New South Wales) | Azure | australiaeast |
Location hint: oc |
| Central India (Pune) | Azure | centralindia |
Location hint: apac |
| Japan East (Tokyo) | Azure | japaneast |
Location hint: apac |
| Southeast Asia (Singapore) | Azure | southeastasia |
Location hint: apac |
Protect an R2 Bucket with Cloudflare Access
import { Render } from "~/components";
You can secure access to R2 buckets using Cloudflare Access.
Access allows you to only allow specific users, groups or applications within your organization to access objects within a bucket, or specific sub-paths, based on policies you define.
:::note
For providing secure access to bucket objects for anonymous users, we recommend using pre-signed URLs instead.
Pre-signed URLs do not require users to be a member of your organization and enable programmatic application directly.
:::
1. Create a bucket
If you have an existing R2 bucket, you can skip this step.
You will need to create an R2 bucket. Follow the R2 get started guide to create a bucket before returning to this guide.
2. Create an Access application
Within the Zero Trust section of the Cloudflare Dashboard, you will need to create an Access application and a policy to restrict access to your R2 bucket.
If you have not configured Cloudflare Access before, we recommend:
- Configuring an identity provider first to enable Access to use your organization's single-sign on (SSO) provider as an authentication method.
To create an Access application for your R2 bucket:
-
In the Cloudflare dashboard, go to Zero Trust > Access controls > Applications and select Create new application.
-
Select Self-hosted and private.
-
Select Add public hostname and enter the application domain. The Domain must be a domain hosted on Cloudflare, and the Subdomain part of the custom domain you will connect to your R2 bucket. For example, if you want to serve files from
behind-access.example.comandexample.comis a domain within your Cloudflare account, then enterbehind-accessin the subdomain field and selectexample.comfrom the Domain list. -
Add Access policies to control who can connect to your application. This should be an Allow policy so that users can access objects within the bucket behind this Access application.
:::note Ensure that your policies only allow the users within your organization that need access to this R2 bucket. :::
-
Follow the remaining self-hosted application creation steps to publish the application.
3. Connect a custom domain
:::caution
You should create an Access application before connecting a custom domain to your bucket, as connecting a custom domain will otherwise make your bucket public by default.
:::
You will need to connect a custom domain to your bucket in order to configure it as an Access application. Make sure the custom domain is the same domain you entered when configuring your Access policy.
4. Test your Access policy
Visit the custom domain you connected to your R2 bucket, which should present a Cloudflare Access authentication page with your selected identity provider(s) and/or authentication methods.
For example, if you connected Google and/or GitHub identity providers, you can log in with those providers. If the login is successful and you pass the Access policies configured in this guide, you will be able to access (read/download) objects within the R2 bucket.
If you cannot authenticate or receive a block page after authenticating, check that you have an Access policy configured within your Access application that explicitly allows the group your user account is associated with.
Next steps
- Learn more about Access applications and how to configure them.
- Understand how to use pre-signed URLs to issue time-limited and prefix-restricted access to objects for users not within your organization.
- Review the documentation on using API tokens to authenticate against R2 buckets.
Tutorials
import { GlossaryTooltip, ListTutorials } from "~/components";
View tutorials to help you get started with R2.
Mastodon
Mastodon is a popular fediverse software. This guide will explain how to configure R2 to be the object storage for a self hosted Mastodon instance, for either a new instance or an existing instance.
Set up a new instance
You can set up a self hosted Mastodon instance in multiple ways. Refer to the official documentation for more details. When you reach the Configuring your environment step in the Mastodon documentation after installation, refer to the procedures below for the next steps.
1. Determine the hostname to access files
Different from the default hostname of your Mastodon instance, object storage for files requires a unique hostname. As an example, if you set up your Mastodon's hostname to be mastodon.example.com, you can use mastodon-files.example.com or files.example.com for accessing files. This means that when visiting your instance on mastodon.example.com, whenever there are media attached to a post such as an image or a video, the file will be served under the hostname determined at this step, such as mastodon-files.example.com.
:::note
If you move from R2 to another S3 compatible service later on, you can continue using the same hostname determined in this step. We do not recommend changing the hostname after the instance has been running to avoid breaking historical file references. In such a scenario, Bulk Redirects can be used to instruct requests reaching the previous hostname to refer to the new hostname.
:::
2. Create and set up an R2 bucket
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
Select Create bucket.
-
Enter your bucket name and then select Create bucket. This name is internal when setting up your Mastodon instance and is not publicly accessible.
-
Once the bucket is created, navigate to the Settings tab of this bucket and copy the value of S3 API.
-
From the Settings tab, select Connect Domain and enter the hostname from step 1.
-
Navigate back to the R2's overview page and select Manage R2 API Tokens.
-
Select Create API token.
-
Name your token
Mastodonby selecting the pencil icon next to the API name and grant it the Edit permission. Select Create API Token to finalize token creation. -
Copy the values of Access Key ID and Secret Access Key.
3. Configure R2 for Mastodon
While configuring your Mastodon instance based on the official configuration file, replace the File storage section with the following details.
S3_ENABLED=true
S3_ALIAS_HOST={{mastodon-files.example.com}} # Change to the hostname determined in step 1
S3_BUCKET={{your-bucket-name}} # Change to the bucket name set in step 2
S3_ENDPOINT=https://{{unique-id}}.r2.cloudflarestorage.com/ # Change the {{unique-id}} to the part of S3 API retrieved in step 2
AWS_ACCESS_KEY_ID={{your-access-key-id}} # Change to the Access Key ID retrieved in step 2
AWS_SECRET_ACCESS_KEY={{your-secret-access-key}} # Change to the Secret Access Key retrieved in step 2
S3_PROTOCOL=https
S3_PERMISSION=
Leave S3_PERMISSION empty. This prevents Mastodon from sending ACL headers, which R2 does not support.
After configuration, you can run your instance. After the instance is running, upload a media attachment and verify the attachment is retrieved from the hostname set above. When navigating back to the bucket's page in R2, you should see the following structure.

Migrate to R2
If you already have an instance running, you can migrate the media files to R2 and benefit from no egress cost.
1. Set up an R2 bucket and start file migration
- (Optional) To minimize the number of migrated files, you can use the Mastodon admin CLI to clean up unused files.
- Set up an R2 bucket ready for file migration by following steps 1 and 2 from Setting up a new instance section above.
- Migrate all the media files to R2. Refer to the examples provided to connect various providers together. If you currently host these media files locally, you can use
rcloneto upload these local files to R2.
2. (Optional) Set up file path redirects
While the file migration is in progress, which may take a while, you can prepare file path redirect settings.
If you had the media files hosted locally, you will likely need to set up redirects. By default, media files hosted locally would have a path similar to https://mastodon.example.com/cache/..., which needs to be redirected to a path similar to https://mastodon-files.example.com/cache/... after the R2 bucket is up and running alongside your Mastodon instance. If you already use another S3 compatible object storage service and would like to keep the same hostname, you do not need to set up redirects.
Bulk Redirects are available for all plans. Refer to Create Bulk Redirects in the dashboard for more information.

3. Verify bucket and redirects
Depending on your migration plan, you can verify if the bucket is accessible publicly and the redirects work correctly. To verify, open an existing uploaded media file with a path like https://mastodon.example.com/cache/... and replace the hostname from mastodon.example.com to mastodon-files.example.com and visit the new path. If the file opened correctly, proceed to the final step.
4. Finalize migration
Your instance may be still running during migration, and during migration, you likely have new media files created either through direct uploads or fetched from other federated instances. To upload only the newly created files, you can use a program like rclone. Note that when re-running the sync program, all existing files will be checked using at least Class B operations.
Once all the files are synced, you can restart your Mastodon instance with the new object storage configuration as mentioned in step 3 of Set up a new instance.
Postman
Postman is an API platform that makes interacting with APIs easier. This guide will explain how to use Postman to make authenticated R2 requests to create a bucket, upload a new object, and then retrieve the object. The R2 Postman collection includes a complete list of operations supported by the platform.
1. Purchase R2
This guide assumes that you have made a Cloudflare account and purchased R2.
2. Explore R2 in Postman
Explore R2's publicly available Postman collection. The collection is organized into a Buckets folder for bucket-level operations and an Objects folder for object-level operations. Operations in the Objects > Upload folder allow for adding new objects to R2.
3. Configure your R2 credentials
In the Postman dashboard, select the Cloudflare R2 collection and navigate to the Variables tab. In Variables, you can set variables within the R2 collection. They will be used to authenticate and interact with the R2 platform. Remember to always select Save after updating a variable.
To execute basic operations, you must set the account-id, r2-access-key-id, and r2-secret-access-key variables in the Postman dashboard > Variables.
To do this:
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
In R2, under Manage R2 API Tokens on the right side of the dashboard, copy your Cloudflare account ID.
-
Go back to the Postman dashboard.
-
Set the CURRENT VALUE of
account-idto your Cloudflare account ID and select Save.
Next, generate an R2 API token:
-
In the Cloudflare dashboard, go to the R2 object storage page.
-
On the right hand sidebar, select Manage R2 API Tokens.
-
Select Create API token.
-
Name your token Postman by selecting the pencil icon next to the API name and grant it the Edit permission.
Guard this token and the Access Key ID and Secret Access Key closely. You will not be able to review these values again after finishing this step. Anyone with this information can fully interact with all of your buckets.
After you have created your API token in the Cloudflare dashboard:
- Go to the Postman dashboard > Variables.
- Copy
Access Key IDvalue from the Cloudflare dashboard and paste it into Postman’sr2-access-key-idvariable value and select Save. - Copy the
Secret Access Keyvalue from the Cloudflare dashboard and paste it into Postman’sr2-secret-access-keyvariable value and select Save.
By now, you should have account-id, r2-secret-access-key, and r2-access-key-id set in Postman.
To verify the token:
- In the Postman dashboard, select the Cloudflare R2 folder dropdown arrow > Buckets folder dropdown arrow >
GETListBuckets. - Select Send.
The Postman collection uses AWS SigV4 authentication to complete the handshake.
You should see a 200 OK response with a list of existing buckets. If you receive an error, ensure your R2 subscription is active and Postman variables are saved correctly.
4. Create a bucket
In the Postman dashboard:
- Go to Variables.
- Set the
r2-bucketvariable value as the name of your R2 bucket and select Save. - Select the Cloudflare R2 folder dropdown arrow > Buckets folder dropdown arrow >
PUTCreateBucket and select Send.
You should see a 200 OK response. If you run the ListBuckets request again, your bucket will appear in the list of results.
5. Add an object
You will now add an object to your bucket:
- Go to Variables in the Postman dashboard.
- Set
r2-objecttocat-pic.jpgand select Save. - Select Cloudflare R2 folder dropdown arrow > Objects folder dropdown arrow > Multipart folder dropdown arrow >
PUTPutObject and select Send. - Go to Body and choose binary before attaching your cat picture.
- Select Send to add the cat picture to your R2 bucket.
After a few seconds, you should receive a 200 OK response.
6. Get an object
It only takes a few more more clicks to download our cat friend using the GetObject request.
- Select the Cloudflare R2 folder dropdown arrow > Objects folder dropdown arrow >
GETGetObject. - Select Send.
The R2 team will keep this collection up to date as we expand R2 features set. You can explore the rest of the R2 Postman collection by experimenting with other operations.
Use event notification to summarize PDF files on upload
import { Render, PackageManagers, Details, WranglerConfig } from "~/components";
In this tutorial, you will learn how to use event notifications to process a PDF file when it is uploaded to an R2 bucket. You will use Workers AI to summarize the PDF and store the summary as a text file in the same bucket.
Prerequisites
To continue, you will need:
- A Cloudflare account with access to R2.
- Have an existing R2 bucket. Refer to Get started tutorial for R2.
- Install
Node.js.
For supported Node.js versions, refer to the
[Wrangler system requirements](/workers/wrangler/install-and-update/#install-wrangler).
1. Create a new project
You will create a new Worker project that will use Static Assets to serve the front-end of your application. A user can upload a PDF file using this front-end, which will then be processed by your Worker.
Create a new Worker project by running the following commands:
<PackageManagers type="create" pkg="cloudflare@latest" args={"pdf-summarizer"} />
<Render
file="c3-post-run-steps-hello-world"
product="workers"
params={{
type: "Worker only",
lang: "TypeScript",
}}
/>
cd pdf-summarizer
2. Create the front-end
Using Static Assets, you can serve the front-end of your application from your Worker. To use Static Assets, you need to add the required bindings to your Wrangler file.
{
"assets": {
"directory": "public"
}
}
Next, create a public directory and add an index.html file. The index.html file should contain the following HTML code:
Select to view the HTML code
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PDF Summarizer</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
background-color: #fefefe;
}
.content {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.upload-container {
background-color: #f0f0f0;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.upload-button {
background-color: #4caf50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.upload-button:hover {
background-color: #45a049;
}
footer {
background-color: #f0f0f0;
color: white;
text-align: center;
padding: 10px;
width: 100%;
}
footer a {
color: #333;
text-decoration: none;
margin: 0 10px;
}
footer a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="content">
<div class="upload-container">
<h2>Upload PDF File</h2>
<form id="uploadForm" onsubmit="return handleSubmit(event)">
<input
type="file"
id="pdfFile"
name="pdfFile"
accept=".pdf"
required
/>
<button type="submit" id="uploadButton" class="upload-button">
Upload
</button>
</form>
</div>
</div>
<footer>
<a
href="https://developers.cloudflare.com/r2/buckets/event-notifications/"
target="_blank"
>R2 Event Notification</a
>
<a
href="https://developers.cloudflare.com/queues/get-started/#3-create-a-queue"
target="_blank"
>Cloudflare Queues</a
>
<a href="https://developers.cloudflare.com/workers-ai/" target="_blank"
>Workers AI</a
>
<a
href="https://github.com/harshil1712/pdf-summarizer-r2-event-notification"
target="_blank"
>GitHub Repo</a
>
</footer>
<script>
handleSubmit = async (event) => {
event.preventDefault();
// Disable the upload button and show a loading message
const uploadButton = document.getElementById("uploadButton");
uploadButton.disabled = true;
uploadButton.textContent = "Uploading...";
// get form data
const formData = new FormData(event.target);
const file = formData.get("pdfFile");
if (file) {
// call /api/upload endpoint and send the file
await fetch("/api/upload", {
method: "POST",
body: formData,
});
event.target.reset();
} else {
console.log("No file selected");
}
uploadButton.disabled = false;
uploadButton.textContent = "Upload";
};
</script>
</body>
</html>
To view the front-end of your application, run the following command and navigate to the URL displayed in the terminal:
npm run dev
⛅️ wrangler 3.80.2
-------------------
⎔ Starting local server...
[wrangler:inf] Ready on http://localhost:8787
╭───────────────────────────╮
│ [b] open a browser │
│ [d] open devtools │
│ [l] turn off local mode │
│ [c] clear console │
│ [x] to exit │
╰───────────────────────────╯
When you open the URL in your browser, you will see that there is a file upload form. If you try uploading a file, you will notice that the file is not uploaded to the server. This is because the front-end is not connected to the back-end. In the next step, you will update your Worker that will handle the file upload.
3. Handle file upload
To handle the file upload, you will first need to add the R2 binding. In the Wrangler file, add the following code:
{
"r2_buckets": [
{
"binding": "MY_BUCKET",
"bucket_name": "<R2_BUCKET_NAME>"
}
]
}
Replace <R2_BUCKET_NAME> with the name of your R2 bucket.
Next, update the src/index.ts file. The src/index.ts file should contain the following code:
export default {
async fetch(request, env, ctx): Promise<Response> {
// Get the pathname from the request
const pathname = new URL(request.url).pathname;
if (pathname === "/api/upload" && request.method === "POST") {
// Get the file from the request
const formData = await request.formData();
const file = formData.get("pdfFile") as File;
// Upload the file to Cloudflare R2
const upload = await env.MY_BUCKET.put(file.name, file);
return new Response("File uploaded successfully", { status: 200 });
}
return new Response("incorrect route", { status: 404 });
},
} satisfies ExportedHandler<Env>;
The above code does the following:
- Check if the request is a POST request to the
/api/uploadendpoint. If it is, it gets the file from the request and uploads it to Cloudflare R2 using the Workers API. - If the request is not a POST request to the
/api/uploadendpoint, it returns a 404 response.
Since the Worker code is written in TypeScript, you should run the following command to add the necessary type definitions. While this is not required, it will help you avoid errors.
npm run cf-typegen
You can restart the developer server to test the changes:
npm run dev
4. Create a queue
Event notifications capture changes to data in your R2 bucket. You will need to create a new queue pdf-summarize to receive notifications:
npx wrangler queues create pdf-summarizer
Add the binding to the Wrangler file:
{
"queues": {
"consumers": [
{
"queue": "pdf-summarizer"
}
]
}
}
5. Handle event notifications
Now that you have a queue to receive event notifications, you need to update the Worker to handle the event notifications. You will need to add a Queue handler that will extract the textual content from the PDF, use Workers AI to summarize the content, and then save it in the R2 bucket.
Update the src/index.ts file to add the Queue handler:
export default {
async fetch(request, env, ctx): Promise<Response> {
// No changes in the fetch handler
},
async queue(batch, env) {
for (let message of batch.messages) {
console.log(`Processing the file: ${message.body.object.key}`);
}
},
} satisfies ExportedHandler<Env>;
The above code does the following:
- The
queuehandler is called when a new message is added to the queue. It loops through the messages in the batch and logs the name of the file.
For now the queue handler is not doing anything. In the next steps, you will update the queue handler to extract the textual content from the PDF, use Workers AI to summarize the content, and then add it to the bucket.
6. Extract the textual content from the PDF
To extract the textual content from the PDF, the Worker will use the unpdf library. The unpdf library provides utilities to work with PDF files.
Install the unpdf library by running the following command:
Update the src/index.ts file to import the required modules from the unpdf library:
import { extractText, getDocumentProxy } from "unpdf";
Next, update the queue handler to extract the textual content from the PDF:
async queue(batch, env) {
for(let message of batch.messages) {
console.log(`Processing file: ${message.body.object.key}`);
// Get the file from the R2 bucket
const file = await env.MY_BUCKET.get(message.body.object.key);
if (!file) {
console.error(`File not found: ${message.body.object.key}`);
continue;
}
// Extract the textual content from the PDF
const buffer = await file.arrayBuffer();
const document = await getDocumentProxy(new Uint8Array(buffer));
const {text} = await extractText(document, {mergePages: true});
console.log(`Extracted text: ${text.substring(0, 100)}...`);
}
}
The above code does the following:
- The
queuehandler gets the file from the R2 bucket. - The
queuehandler extracts the textual content from the PDF using theunpdflibrary. - The
queuehandler logs the textual content.
7. Use Workers AI to summarize the content
To use Workers AI, you will need to add the Workers AI binding to the Wrangler file. The Wrangler file should contain the following code:
{
"ai": {
"binding": "AI"
}
}
Execute the following command to add the AI type definition:
npm run cf-typegen
Update the src/index.ts file to use Workers AI to summarize the content:
async queue(batch, env) {
for(let message of batch.messages) {
// Extract the textual content from the PDF
const {text} = await extractText(document, {mergePages: true});
console.log(`Extracted text: ${text.substring(0, 100)}...`);
// Use Workers AI to summarize the content
const result: AiSummarizationOutput = await env.AI.run(
"@cf/facebook/bart-large-cnn",
{
input_text: text,
}
);
const summary = result.summary;
console.log(`Summary: ${summary.substring(0, 100)}...`);
}
}
The queue handler now uses Workers AI to summarize the content.
8. Add the summary to the R2 bucket
Now that you have the summary, you need to add it to the R2 bucket. Update the src/index.ts file to add the summary to the R2 bucket:
async queue(batch, env) {
for(let message of batch.messages) {
// Extract the textual content from the PDF
// ...
// Use Workers AI to summarize the content
// ...
// Add the summary to the R2 bucket
const upload = await env.MY_BUCKET.put(`${message.body.object.key}-summary.txt`, summary, {
httpMetadata: {
contentType: 'text/plain',
},
});
console.log(`Summary added to the R2 bucket: ${upload.key}`);
}
}
The queue handler now adds the summary to the R2 bucket as a text file.
9. Enable event notifications
Your queue handler is ready to handle incoming event notification messages. You need to enable event notifications with the wrangler r2 bucket notification create command for your bucket. The following command creates an event notification for the object-create event type for the pdf suffix:
npx wrangler r2 bucket notification create <R2_BUCKET_NAME> --event-type object-create --queue pdf-summarizer --suffix "pdf"
Replace <R2_BUCKET_NAME> with the name of your R2 bucket.
An event notification is created for the pdf suffix. When a new file with the pdf suffix is uploaded to the R2 bucket, the pdf-summarizer queue is triggered.
10. Deploy your Worker
To deploy your Worker, run the wrangler deploy command:
npx wrangler deploy
In the output of the wrangler deploy command, copy the URL. This is the URL of your deployed application.
11. Test
To test the application, navigate to the URL of your deployed application and upload a PDF file. Alternatively, you can use the Cloudflare dashboard to upload a PDF file.
To view the logs, you can use the wrangler tail command.
npx wrangler tail
You will see the logs in your terminal. You can also navigate to the Cloudflare dashboard and view the logs in the Workers Logs section.
If you check your R2 bucket, you will see the summary file.
Conclusion
In this tutorial, you learned how to use R2 event notifications to process an object on upload. You created an application to upload a PDF file, and created a consumer Worker that creates a summary of the PDF file. You also learned how to use Workers AI to summarize the content of the PDF file, and upload the summary to the R2 bucket.
You can use the same approach to process other types of files, such as images, videos, and audio files. You can also use the same approach to process other types of events, such as object deletion, and object update.
If you want to view the code for this tutorial, you can find it on GitHub.
Log and store upload events in R2 with event notifications
import { Render, PackageManagers, WranglerConfig } from "~/components";
This example provides a step-by-step guide on using event notifications to capture and store R2 upload logs in a separate bucket.
1. Install Wrangler
To begin, refer to Install/Update Wrangler to install Wrangler, the Cloudflare Developer Platform CLI.
2. Create R2 buckets
You will need to create two R2 buckets:
example-upload-bucket: When new objects are uploaded to this bucket, your consumer Worker will write logs.example-log-sink-bucket: Upload logs fromexample-upload-bucketwill be written to this bucket.
To create the buckets, run the following Wrangler commands:
npx wrangler r2 bucket create example-upload-bucket
npx wrangler r2 bucket create example-log-sink-bucket
3. Create a queue
Event notifications capture changes to data in example-upload-bucket. You will need to create a new queue to receive notifications:
npx wrangler queues create example-event-notification-queue
4. Create a Worker
Before you enable event notifications for example-upload-bucket, you need to create a consumer Worker to receive the notifications.
Create a new Worker with C3 (create-cloudflare CLI). C3 is a command-line tool designed to help you set up and deploy new applications, including Workers, to Cloudflare.
<PackageManagers type="create" pkg="cloudflare@latest" args={"consumer-worker"} />
<Render file="c3-post-run-steps-hello-world" product="workers" params={{ type: "Worker only", lang: "TypeScript", }} />
Then, move into your newly created directory:
cd consumer-worker
5. Configure your Worker
In your Worker project's [Wrangler configuration file](/workers/wrangler/configuration/), add a queue consumer and R2 bucket binding. The queues consumer bindings will register your Worker as a consumer of your future event notifications and the R2 bucket bindings will allow your Worker to access your R2 bucket.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "event-notification-writer",
"main": "src/index.ts",
"upload_source_maps": true,
"compatibility_date": "$today",
"compatibility_flags": [
"nodejs_compat"
],
"queues": {
"consumers": [
{
"queue": "example-event-notification-queue",
"max_batch_size": 100,
"max_batch_timeout": 5
}
]
},
"r2_buckets": [
{
"binding": "LOG_SINK",
"bucket_name": "example-log-sink-bucket"
}
]
}
6. Write event notification messages to R2
Add a queue handler to src/index.ts to handle writing batches of notifications to our log sink bucket (you do not need a fetch handler):
export interface Env {
LOG_SINK: R2Bucket;
}
export default {
async queue(batch, env): Promise<void> {
const batchId = new Date().toISOString().replace(/[:.]/g, "-");
const fileName = `upload-logs-${batchId}.json`;
// Serialize the entire batch of messages to JSON
const fileContent = new TextEncoder().encode(
JSON.stringify(batch.messages),
);
// Write the batch of messages to R2
await env.LOG_SINK.put(fileName, fileContent, {
httpMetadata: {
contentType: "application/json",
},
});
},
} satisfies ExportedHandler<Env>;
7. Deploy your Worker
To deploy your consumer Worker, run the wrangler deploy command:
npx wrangler deploy
8. Enable event notifications
Now that you have your consumer Worker ready to handle incoming event notification messages, you need to enable event notifications with the wrangler r2 bucket notification create command for example-upload-bucket:
npx wrangler r2 bucket notification create example-upload-bucket --event-type object-create --queue example-event-notification-queue
9. Test
Now you can test the full end-to-end flow by uploading an object to example-upload-bucket in the Cloudflare dashboard. After you have uploaded an object, logs will appear in example-log-sink-bucket in a few seconds.