R2 — Buckets of Objects
Hoplink users now want to shorten links to files: a PDF menu, a holiday photo, a slide deck. KV is the wrong home — it is tuned for small values read everywhere, not for megabytes of binary data. R2 is Cloudflare's object storage: it holds files of any size, speaks the same API as Amazon S3, and charges nothing for data going out (no egress fees), which matters when a popular file is downloaded thousands of times.
The rule
A bucket holds objects; an object is a key, a body and metadata.
- Bucket — a named container, such as
hop-files. You create it withwrangler r2 bucket create hop-filesand bind it asFILES. - Key — the object's name, like
priya/menu.pdf. There are no real folders; slashes are just characters that make keys look like paths. - Body — the bytes, handled as a stream.
- Metadata —
httpMetadata(content type, cache rules) and your owncustomMetadata.
// upload: stream the request body straight into R2
await env.FILES.put("priya/menu.pdf", request.body, {
httpMetadata: { contentType: "application/pdf" },
customMetadata: { owner: "priya" },
});
// download
const object = await env.FILES.get("priya/menu.pdf");
if (object === null) return new Response("Not found", { status: 404 });
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("etag", object.httpEtag);
return new Response(object.body, { headers });The other bucket operations:
head(key)— metadata only, no body. Cheap for "does it exist and how big is it?"delete(key)— accepts one key or an array of keys.list({ prefix: "priya/", cursor })— returnsobjects, atruncatedflag and acursorfor the next page. Adddelimiter: "/"to get folder-like groupings.
Unlike KV, R2 is strongly consistent: once put finishes, every reader everywhere sees the new object.
The shortcut
To choose between KV and R2, ask two questions: How big? How is it used? Small values looked up by name on every request → KV. Files, anything large, anything a user downloads → R2. Often you use both: KV (or D1) maps the short code to an R2 key, and R2 holds the bytes.
Look up the code
env.LINKS.get("menu")returnsfile:priya/menu.pdf— a pointer, not the file.
Trap: forgetting contentType on upload. The file is stored fine, but browsers then download a mystery blob instead of displaying the image or PDF.
Drills
Answer fast — trust the rule.
What does env.FILES.get("missing.pdf") return when the object does not exist?
You only need to know whether a file exists and its size. Which call is cheapest?
Why is priya/menu.pdf not really inside a folder called priya?
Right after a successful put, a reader on another continent requests the same key. What do they get?
Remember
- A bucket holds objects; an object is a key, a body and metadata.
- Shortcut: small lookups → KV; files and large blobs → R2; often both together.
- Trap: uploading without a content type, so browsers cannot display the file.
