| name | r2 |
| description | Write Cloudflare R2 object-storage code for uploads, downloads, metadata, streaming, signed access patterns, large files, generated exports, public assets, and D1/R2 metadata separation. Use when Workers handle blobs or egress-sensitive storage.
|
| compatibility | Cloudflare Workers TypeScript projects using Wrangler; verify current Cloudflare APIs, limits, and pricing before production use. |
| metadata | {"source":"Architecting on Cloudflare plus official Cloudflare Developer Platform docs","generated":"2026-04-28"} |
R2
Use this skill when code needs object/blob storage. R2 stores bytes; keep queryable metadata in D1 or Durable Object state.
Best uses
- User uploads.
- Images, video, audio, attachments.
- Generated exports/reports.
- Backups and archives.
- Parsed document chunks for RAG pipelines.
- Public or signed downloads.
Key design
Use predictable, tenant-safe keys that avoid collisions and make cleanup possible.
tenants/{tenantId}/uploads/{yyyy}/{mm}/{objectId}-{safeFilename}
tenants/{tenantId}/exports/{jobId}.csv
documents/{documentId}/chunks/{chunkIndex}.txt
Do not expose raw keys to users unless the key contains no sensitive information and authorization is still enforced.
Upload pattern
export interface Env {
BUCKET: R2Bucket;
DB: D1Database;
}
export async function upload(request: Request, env: Env, tenantId: string) {
const contentType = request.headers.get("content-type") ?? "application/octet-stream";
const objectId = crypto.randomUUID();
const key = `tenants/${tenantId}/uploads/${objectId}`;
await env.BUCKET.put(key, request.body, {
httpMetadata: { contentType },
customMetadata: { tenantId }
});
await env.DB.prepare(
`INSERT INTO files (id, tenant_id, r2_key, content_type, created_at)
VALUES (?, ?, ?, ?, ?)`
).bind(objectId, tenantId, key, contentType, new Date().toISOString()).run();
return Response.json({ id: objectId });
}
Download pattern
export async function download(env: Env, tenantId: string, fileId: string) {
const row = await env.DB.prepare(
"SELECT r2_key, content_type FROM files WHERE tenant_id = ? AND id = ?"
).bind(tenantId, fileId).first<{ r2_key: string; content_type: string }>();
if (!row) return new Response("Not found", { status: 404 });
const object = await env.BUCKET.get(row.r2_key);
if (!object) return new Response("Not found", { status: 404 });
return new Response(object.body, {
headers: {
"Content-Type": object.httpMetadata?.contentType ?? row.content_type,
"ETag": object.httpEtag
}
});
}
Metadata rules
- R2 custom metadata is useful for object-local hints.
- D1 is better for querying, ownership, ACLs, status, and indexes.
- Durable Objects are better for live coordination around an upload/session.
- KV can cache public metadata but should not be the source of truth.
Safety checks
- Enforce auth before reading/writing objects.
- Limit accepted content types and sizes before processing.
- Stream large data; do not buffer entire files in memory.
- Generate canonical keys server-side.
- Consider malware scanning or content moderation for user uploads where appropriate.
- Delete metadata and object bytes together through a workflow or idempotent cleanup job.
Anti-patterns
- Listing R2 keys as the only authorization mechanism.
- Keeping all file metadata only in object custom metadata.
- Reading large objects into memory before returning them.
- Using D1 or KV for large file contents.