Blog20 min read
Object storage on Cloudflare R2: list, get, put, delete
Deep guide to Cloudflare R2 on Workers - bindings, CRUD APIs, presigned uploads, lifecycle, caching, SQL-over-R2, and the R2 Storage experiment.
Cloudflare R2 is S3-compatible object storage that pairs naturally with Workers: store blobs next to your compute and avoid classic egress fees when Workers read the bucket. Typical uses include user uploads, generated screenshots, model artifacts, and caches for heavier experiment outputs.
This guide covers the CRUD mental model, binding setup, auth, lifecycle, adjacent products (KV, D1, Cache API, R2 SQL), and the R2 Storage experiment.
Cloudflare products in this pattern
| Product | Role | Docs |
|---|---|---|
| R2 | Object storage buckets | Product home |
| R2 Workers API | put / get / list / delete from Workers | Binding API |
| Workers | HTTP facade + validation | Runtime |
| Workers bindings | r2_buckets in wrangler | Config |
| Presigned URLs | Browser-direct uploads | S3 API |
| R2 lifecycle | Expire temp objects | Lifecycle |
| D1 | Structured metadata about objects | SQL |
| KV | Tiny pointers / hot flags | Key-value |
| Cache API | Short-TTL HTTP cache in front of gets | Cache |
When R2 is the right store
| Need | Prefer | Why |
|---|---|---|
| Files, images, PDFs, archives | R2 | Object semantics + size |
| Structured rows / SQL | D1 (playground) | Queries, joins |
| Small string configs | KV (KV Notes) | Ultra-simple get/put |
| Strongly consistent per-entity state | Durable Objects | Single-threaded entity |
Do not force multi-MB blobs into KV. Do not force ad-hoc JSON files into R2 when D1 fits.
Worker + R2 binding pattern
Declare the bucket in wrangler.json (see R2 bindings), type it in env.d.ts, then call the binding:
await env.BUCKET.put(key, body, {
httpMetadata: { contentType: "image/png" },
});
const object = await env.BUCKET.get(key);
if (object === null) {
// 404 with stable code
}Official reference: Workers API reference. The R2 Storage experiment wraps these calls behind HTTP so you can explore keys from curl before folding helpers into your app.
Suggested HTTP surface
| Method | Path | Action |
|---|---|---|
PUT | /objects/:key | Upload body |
GET | /objects/:key | Download object |
DELETE | /objects/:key | Delete |
GET | /objects | List with prefix/cursor |
Validate keys (no path traversal, length limits, charset). Return 404 with a stable code when missing. Stream large bodies when possible instead of buffering entire objects in memory.
Presigned uploads for browsers
For large browser uploads, prefer direct-to-R2 with presigned URLs so the Worker does not proxy gigabytes. The Presigned R2 Upload experiment shows that flow.
Pattern:
- Authenticated Worker mints a short-lived upload URL
- Browser
PUTs the file to R2 - Worker records metadata in D1 if needed
Production hardening checklist
- Auth - never leave public put/delete open
- Content-Type - persist
httpMetadataon put (Workers API) - Size limits - reject oversized bodies early in the Worker
- Prefixes - namespace per tenant (
tenantId/…) - Lifecycle - object lifecycles for temp objects
- SQL over objects - analytics-style queries via R2 SQL Query when applicable
- Hot reads - optional Cache API in front (Edge Cache)
Pair R2 with Browser Rendering or Workers AI: generate a screenshot or audio file, then put for reuse.
S3 compatibility notes
R2’s S3-compatible API matters when you use existing SDKs, rclone, or tooling outside Workers. Inside Workers, prefer the native binding for lower friction. Know which path you are on so credentials and endpoints stay correct.
Deploy the experiment
- Create an R2 bucket (get started)
- Deploy R2 Storage with the bucket binding
- Exercise put → get → list → delete
- Add Presigned R2 Upload if browsers upload large files
- Copy
src/libhelpers into your product Worker
You leave with a tested binding usage pattern and clear error responses - the boring foundation most edge apps need before fancier storage features.
Production checklist
- Create buckets per environment (dev/staging/prod).
- Namespace keys by tenant:
tenantId/objectId. - Never expose anonymous put/delete.
- Prefer presigned URLs for large browser uploads.
- Set lifecycle rules for temp objects.
- Store relational metadata in D1, not only object keys.
- Decide CORS carefully when browsing directly to R2.
FAQ
R2 vs KV for files?
KV is for small values with eventual consistency. Multi-MB files belong in R2.
Do Workers pay egress to read R2?
A major R2 value proposition is no egress fees for many common Cloudflare data-transfer paths - confirm current pricing on the R2 pricing page before budgeting.
Related experiments
These deployable references implement the patterns in this article. Open the docs for API details, then Deploy or clone the source.
- R2 Storage API - Upload and serve files with Cloudflare R2 - private and public bucket patterns, object CRUD, and edge file storage reference
- D1 SQL Playground - Read-only SQL playground over a seeded Cloudflare D1 database with safe SELECT validation - learn D1 at the edge
- KV Notes - Simple note CRUD with Workers KV at the edge - minimal key-value storage reference for Cloudflare Workers
- R2 SQL Query - Query Apache Iceberg tables via the Cloudflare R2 SQL HTTP API - analytics SQL over object storage from a Worker
- Presigned R2 Upload - Generate presigned PUT URLs for direct browser-to-R2 uploads with aws4fetch - secure client uploads without Worker proxying
- Edge Cache - Fetch and cache URLs with the Workers Cache API at the edge - Cache API reference pattern for Cloudflare Workers