Namespace-level object storage, accessed from a worker through env.<BUCKET>
with head / get / put / delete / list. Use it for large blobs (images,
PDFs, archives), or for anything larger than — and less suited to — KV.
bucket_name is a platform-local virtual bucket name, not a Cloudflare
account-level bucket. Deleting a worker does not delete R2 data.
- Files (images, attachments, PDFs).
- Anything where a single blob is larger than a few KB.
- Content served for download via CDN — but for static/build artifacts, check assets.md first.
Prefer KV for small key-value lookups — see kv.md. Use D1 for relational data — see d1.md.
[[r2_buckets]]
binding = "IMAGES"
bucket_name = "inspection-images"Or in wrangler.jsonc:
R2 buckets are lazy — no pre-creation needed. The binding works on first use; the bucket appears once the worker writes its first object.
Custom metadata keys are normalized to lowercase on read, following HTTP header
semantics. head() exposes HTTP metadata and custom metadata, so for
authorization it is treated like reading the object body.
export default {
async fetch(request, env, ctx) {
// PUT
await env.IMAGES.put("receipts/2026-04-29.pdf", request.body, {
httpMetadata: { contentType: "application/pdf" },
});
// GET — returns null when not found
const obj = await env.IMAGES.get("receipts/2026-04-29.pdf");
if (!obj) return new Response("not found", { status: 404 });
return new Response(obj.body, {
headers: { "Content-Type": obj.httpMetadata?.contentType ?? "application/octet-stream" },
});
// RANGE GET
const preview = await env.IMAGES.get("receipts/2026-04-29.pdf", {
range: { offset: 0, length: 1024 },
});
// Conditional GET / HEAD
const fresh = await env.IMAGES.get("receipts/2026-04-29.pdf", {
onlyIf: { etagDoesNotMatch: obj.etag },
});
// HEAD — metadata only, no body download
const meta = await env.IMAGES.head("receipts/2026-04-29.pdf");
// LIST — paginated; including metadata reads each object's HEAD
const page = await env.IMAGES.list({ prefix: "receipts/", limit: 100 });
const pageWithMeta = await env.IMAGES.list({
prefix: "receipts/",
include: ["httpMetadata", "customMetadata"],
});
// DELETE
await env.IMAGES.delete("receipts/2026-04-29.pdf");
},
};get() supports the common range GET shapes (offset / length / suffix /
raw header) and conditional reads. onlyIf on get() / head() supports
ETag conditions and upload-time conditions; put(..., { onlyIf }) currently
supports only ETag conditions and returns null when the precondition fails.
list({ include: ["httpMetadata", "customMetadata"] }) issues extra HEAD
requests to hydrate metadata for the listed objects, under a concurrency cap.
For large scans, do not enable the metadata include by default; use it only when
the list results genuinely need metadata for display or decisions.
put(stream, ...) buffers the stream first, then sends a single S3 PUT, with a
25 MiB cap. The platform does not support multipart upload yet; exceeding
the cap fails instead of falling back to chunked upload.
wdl r2 buckets list
wdl r2 objects list <bucket> [--prefix <p>] [--delimiter <d>] [--limit <n>] [--cursor <c>]
wdl r2 objects head <bucket> <key> # metadata only, no body download
wdl r2 objects get <bucket> <key> --out file # download
wdl r2 objects delete <bucket> <key> --yes # destructive — confirm firstWhen the list is truncated, the output includes Next cursor: <c>; pass it to
the next --cursor to keep paging (wdl r2 buckets list also supports
--cursor / --limit, capped at 1000):
wdl r2 objects list receipts --limit 100
# ...
# Next cursor: eyJrZXkiOiJyZWNlaXB0cy8wMDk5In0
wdl r2 objects list receipts --limit 100 --cursor eyJrZXkiOiJyZWNlaXB0cy8wMDk5In0wdl r2 objects head is a read-only inspection tool — confirm the target object
is correct before any destructive operation. For a missing object, control
follows HTTP HEAD semantics and returns an empty 404; the CLI shows the status
code, with no JSON error body to parse.
Deleting a worker does not delete R2 data. R2 objects remain after
wdl delete worker <name>. To clean up:
wdl r2 objects list <bucket> # confirm what exists first
wdl r2 objects delete <bucket> <key> # delete one at a time, each promptsConfirm with the user before adding --yes. Object deletion is irreversible.
- ❌ Using R2 for small key-value data (single-string flags and the like). KV is cheaper for both reads and writes — see kv.md.
- ❌ Expecting
put(stream, ...)to handle blobs over 25 MiB. The platform currently rejects them; multipart upload is not supported yet. - ❌ Adding
--yestowdl r2 objects deleteproactively.headorlistfirst, and confirm with the user. - ❌ Assuming worker deletion cleans up R2. It does not — R2 outlives the worker.
../examples/inspection-demo — R2 + D1 + KV + assets combined in one project.
{ "r2_buckets": [ { "binding": "IMAGES", "bucket_name": "inspection-images" } ] }