Skip to content

Latest commit

 

History

History
166 lines (125 loc) · 5.6 KB

File metadata and controls

166 lines (125 loc) · 5.6 KB

R2 — Object Storage

What it is

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.

When to use

  • 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.

Wrangler configuration

[[r2_buckets]]
binding = "IMAGES"
bucket_name = "inspection-images"

Or in wrangler.jsonc:

{
  "r2_buckets": [
    { "binding": "IMAGES", "bucket_name": "inspection-images" }
  ]
}

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.

Worker-side reads and writes

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");
  },
};

Reads and metadata

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 size cap

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.

Inspecting from the CLI

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 first

When 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 eyJrZXkiOiJyZWNlaXB0cy8wMDk5In0

wdl 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.

Cleanup after worker deletion

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 prompts

Confirm with the user before adding --yes. Object deletion is irreversible.

Anti-patterns

  • ❌ 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 --yes to wdl r2 objects delete proactively. head or list first, and confirm with the user.
  • ❌ Assuming worker deletion cleans up R2. It does not — R2 outlives the worker.

End-to-end example

../examples/inspection-demo — R2 + D1 + KV + assets combined in one project.

Related

  • kv.md — small key-value storage.
  • d1.md — relational data.
  • assets.md — static files served via CDN.