R2-compatible object storage
Object storage bound to env.NAME, with the R2 binding API, multipart uploads and direct transfer URLs.
Object storage with the R2 binding API. Each object’s bytes live in their own file on disk.
Configuration
"r2_buckets": ["MEDIA"]
Bare names are auto-provisioned on deploy; see Storage resources.
Usage
env.MEDIA.put("path/file.txt", body, { httpMetadata: { contentType: "text/plain" } });
const obj = env.MEDIA.get("path/file.txt"); // null when missing
return obj.toResponse(); // serves the bytes unchanged
env.MEDIA.head("path/file.txt"); // metadata only
env.MEDIA.delete("path/file.txt");
const { objects, truncated, cursor } = env.MEDIA.list({ prefix: "path/", limit: 100 });
From compatibility date 2026-10-26 R2 calls return Promises, as on Workers; before it they are synchronous. await works at both.
Methods
| Method | Signature | Returns |
|---|---|---|
put |
(key, body, { httpMetadata?, customMetadata? }) |
void |
get |
(key) |
an object, or null |
head |
(key) |
the object’s metadata without the body, or null |
delete |
(key) |
void |
list |
({ prefix?, limit?, cursor? }) |
{ objects, truncated, cursor }. limit defaults to and is capped at 1000. |
createMultipartUpload |
(key, { httpMetadata?, customMetadata? }) |
a multipart upload |
resumeMultipartUpload |
(key, uploadId) |
a multipart upload |
createUploadUrl |
(key, { maxBytes?, expiresIn?, sha256?, httpMetadata?, customMetadata? }) |
{ url, expiresAt } |
createDownloadUrl |
(key, { expiresIn? }) |
{ url, expiresAt } |
An object has key, size, etag, httpEtag (quoted, for an ETag header), uploaded, httpMetadata and customMetadata. From get, it also has body, text(), json() and toResponse(init?). toResponse sets content-type from httpMetadata and etag from httpEtag unless init.headers sets them.
Large files
put and get hold the whole object in memory, and a request body is capped at 1 MiB by default. For anything larger, use one of these:
Direct transfer URLs. createUploadUrl and createDownloadUrl return a short-lived URL on your own hostname. The client sends the file straight to it with PUT, or fetches it with GET or HEAD (byte ranges supported), and the bytes never pass through your handler.
fetch(request) {
const { url } = env.MEDIA.createUploadUrl("uploads/video.mp4", { maxBytes: 500 * 1024 * 1024 });
return Response.json({ uploadTo: url });
}
- Each URL works once and expires after
expiresInseconds: 900 by default, between 30 and 3600. - An upload is capped at
maxBytes: 100 MiB by default, 5 GiB at most. - Pass
sha256(lowercase hex) to have the upload rejected when its contents do not match.
Multipart uploads. Send a large object in parts, each within the request body limit:
const upload = env.MEDIA.createMultipartUpload("big.bin");
const part = upload.uploadPart(1, chunk); // part numbers 1 to 10000
upload.complete([part]); // or upload.abort()
Store upload.key and upload.uploadId to continue later with resumeMultipartUpload. Unfinished uploads are deleted after seven days.