Cloudflare Workers and R2: File Upload and Delivery
Cloudflare R2 stores objects, such as images, uploads and backups, in buckets. It has an S3-compatible API, and a Worker can also use a bucket directly through a binding. A bucket is private until you decide otherwise, so each way in is a choice you make.
This template draws the three common ones side by side: uploads and downloads that pass through a Worker, direct uploads to the bucket with a presigned URL, and public downloads from a custom domain with Cloudflare's cache in front. Most apps start with one of them and add the others when a need appears.
Scroll sideways to see the whole diagram
Start from this diagram and edit it on your own board.
By continuing, you agree to the Terms of Service and Privacy Policy, including sending images of your strokes, diagram labels and similar data to providers in the United States (Cloudflare, Inc. and TypeSafe AI, Inc.) for AI conversion.
What each part does
- Browser
- Where files come from and go to. It either sends a file to the Worker, or asks the Worker for a presigned URL and uploads straight to the bucket. Public files are fetched from the custom domain.
- Worker
- Holds the bucket binding (an r2_buckets entry with a binding name and a bucket_name) and decides who may do what. env.MY_BUCKET.put(key, request.body) streams an upload into R2, and get(key) returns null when the key does not exist. The same Worker signs presigned URLs.
- R2 API token
- An access key ID and secret access key for the S3-compatible API, created in the dashboard. The Worker uses them, kept as secrets, to sign presigned URLs. They never go to the browser.
- R2 bucket
- Holds the objects. It can be reached through the Worker binding, through the S3 API at <account id>.r2.cloudflarestorage.com (which is what presigned URLs use), and, if you make it public, through a custom domain.
- Custom domain and cache
- A hostname you control that is connected to the bucket to make it public. Because requests pass through Cloudflare on that domain, you can use Cloudflare Cache, WAF custom rules and access controls in front of the bucket. The bucket's contents are not listed at the root of the domain.
How a file moves
- Upload through the Worker: the browser sends the file to the Worker. After checking who is calling, the Worker streams the request body into the bucket with put(key, request.body).
- Direct upload: the browser asks the Worker for a URL. The Worker signs a PUT request with the R2 API token (AWS Signature Version 4, which needs no call to R2) and returns the URL. The browser then uploads to it directly. The expiry can be anything from 1 second to 7 days, and naming a ContentType when signing restricts the upload to that type.
- Private download: the Worker calls get(key). If the result is null it answers 404, and otherwise it returns the object's body with its stored HTTP metadata and ETag. A presigned GET URL is the alternative when the browser should download straight from R2.
- Public download: the browser requests the file on the custom domain. Cloudflare answers from its cache when it can, and reads the object from the bucket when it cannot.
When to use it
- User-uploaded images and documents, where large files should not pass through your code.
- Serving static assets, downloads or media from a bucket under your own domain.
- Time-limited download links for exports and backups.
Common variations
Restrict who can read a public bucket
Put Cloudflare Access in front of the custom domain for files that only your team should see, or use WAF token authentication to hand out access tokens. Turn off the r2.dev address first: while it is on, the bucket stays public through it. Presigned URLs do not work on custom domains.
Use r2.dev only to try things out
A Cloudflare-managed r2.dev address makes a bucket public without a domain, but the documentation describes it for non-production use. Cache, WAF rules and access controls need a custom domain.
Cache every file type
By default only certain file types are cached on a custom domain. To cache all files in the bucket, set a Cache Everything rule. Smart Tiered Cache can put one upper-tier data center near the bucket.
Handle large files
A Worker has a memory limit of 128 MB, so stream bodies instead of loading them. For very large files, use R2's multipart upload API from the Worker, or presigned URLs so the file never passes through it.
Make it yours
Rename the bucket and the binding, then keep only the upload path you need. Behind a login the Worker route is usually enough, and large public uploads usually want the presigned one.
Opens this diagram as a board you can edit.
By continuing, you agree to the Terms of Service and Privacy Policy, including sending images of your strokes, diagram labels and similar data to providers in the United States (Cloudflare, Inc. and TypeSafe AI, Inc.) for AI conversion.