Skip to main content
Upload images and files to R2 with configurable private and public access. Objects in the private bucket are only accessible through the Worker, while public bucket objects get a direct URL.

Features

  • Private bucket: Objects only accessible via Worker API (GET /object?key=...)
  • Public bucket: Objects get a public URL (e.g. https://pub-xxx.r2.dev/key) when uploaded with public=true
  • List operations with pagination, prefix filtering, and directory-style delimiters
  • Custom metadata support via X-Custom-Metadata-* headers
  • Content-Type handling for serving images and other media

Setup

1

Create R2 buckets

In the Cloudflare dashboardR2Create bucket:
  1. Create r2-storage-bucket (private - default settings)
  2. Create r2-storage-public (for public files)
2

Enable public access

For the public bucket only:
  1. Go to R2 → select r2-storage-publicSettings
  2. Under Public Development URL, click Enable
  3. Type allow when prompted and confirm
  4. Copy the public bucket URL (looks like https://pub-xxxxx.r2.dev)
You can also use a custom domain instead of the r2.dev URL
3

Configure wrangler.json

Update your wrangler.json with the bucket bindings:
{
  "r2_buckets": [
    {
      "binding": "BUCKET",
      "bucket_name": "r2-storage-bucket"
    },
    {
      "binding": "PUBLIC_BUCKET",
      "bucket_name": "r2-storage-public"
    }
  ],
  "vars": {
    "PUBLIC_BUCKET_URL": "https://pub-xxxxx.r2.dev"
  }
}
Replace https://pub-xxxxx.r2.dev with your actual public bucket URL.
4

Run locally

cd experiments/r2-storage
npm install
npm run dev -- --remote
Use --remote to connect to real R2 buckets. The public URL won’t work with local buckets.

API Reference

List objects

GET /list

List objects from a bucket with optional filtering and pagination.

Query Parameters

public
boolean
default:"false"
Set to true or 1 to list from the public bucket. Omit for private bucket.
prefix
string
Filter objects by key prefix (e.g., images/ to list only keys starting with “images/”)
limit
number
default:"1000"
Maximum number of keys to return (1-1000)
cursor
string
Pagination cursor from a previous response (when truncated: true)
delimiter
string
Delimiter for directory-style listing (e.g., / to group by directory)

Response

objects
array
Array of object metadata
truncated
boolean
Whether there are more results available
cursor
string
Cursor for next page (only present when truncated: true)

Example

# List all objects from private bucket
curl "https://your-worker.workers.dev/list"

# List public objects with prefix
curl "https://your-worker.workers.dev/list?public=true&prefix=images/"

# Paginated listing
curl "https://your-worker.workers.dev/list?limit=10&cursor=eyJrZXkiOi4uLn0"
{
  "objects": [
    {
      "key": "images/photo.png",
      "size": 102400,
      "etag": "abc123def456",
      "uploaded": "2025-03-10T12:34:56.789Z"
    }
  ],
  "truncated": false
}

Get object

GET /object

Download an object from R2. Returns the raw file with appropriate Content-Type.

Query Parameters

key
string
required
Object key to retrieve
public
boolean
default:"false"
Set to true or 1 to get from the public bucket

Headers

Range
string
Optional byte range for partial content (e.g., bytes=0-1023)

Response

Returns the raw object body with headers:
  • Content-Type: From object’s HTTP metadata
  • ETag: Object version identifier
  • Content-Length: Size in bytes

Example

# Download private object
curl "https://your-worker.workers.dev/object?key=private/document.pdf" -o document.pdf

# Download public object
curl "https://your-worker.workers.dev/object?key=images/photo.png&public=true" -o photo.png

# Get partial content
curl -H "Range: bytes=0-1023" "https://your-worker.workers.dev/object?key=video.mp4"

Get object metadata

HEAD /object

Get object metadata without downloading the body.

Query Parameters

key
string
required
Object key to check
public
boolean
default:"false"
Set to true or 1 to check the public bucket

Response

key
string
Object key
size
number
Size in bytes
etag
string
Entity tag
uploaded
string
ISO 8601 timestamp
customMetadata
object
Custom metadata if present
httpMetadata
object
HTTP metadata including contentType

Example

curl -X HEAD "https://your-worker.workers.dev/object?key=photo.png&public=true"
{
  "key": "photo.png",
  "size": 102400,
  "etag": "abc123def456",
  "uploaded": "2025-03-10T12:34:56.789Z",
  "httpMetadata": {
    "contentType": "image/png"
  }
}

Upload object

PUT /object

Upload an object to R2. Send raw bytes in the request body.

Query Parameters

key
string
required
Object key (path) for the upload
public
boolean
default:"false"
Set to true or 1 to upload to the public bucket and get a public URL

Headers

Content-Type
string
MIME type of the file (e.g., image/png, application/pdf)
X-Custom-Metadata-*
string
Custom metadata headers (e.g., X-Custom-Metadata-Author: John)

Request Body

Raw bytes of the file to upload.

Response

key
string
Object key that was uploaded
uploaded
boolean
Always true on success
url
string
Public URL (only present for public bucket uploads when PUBLIC_BUCKET_URL is configured)

Example

# Upload to private bucket
curl -X PUT "https://your-worker.workers.dev/object?key=documents/report.pdf" \
  -H "Content-Type: application/pdf" \
  --data-binary @report.pdf

# Upload image to public bucket
curl -X PUT "https://your-worker.workers.dev/object?key=images/photo.png&public=true" \
  -H "Content-Type: image/png" \
  --data-binary @photo.png

# Upload with custom metadata
curl -X PUT "https://your-worker.workers.dev/object?key=files/data.json&public=true" \
  -H "Content-Type: application/json" \
  -H "X-Custom-Metadata-Author: Alice" \
  -H "X-Custom-Metadata-Version: 1.0" \
  --data-binary @data.json
{
  "key": "documents/report.pdf",
  "uploaded": true
}
The url in the response is a direct public link. You can share it or use it in <img> tags without going through the Worker.

Delete object

DELETE /object

Delete an object from R2.

Query Parameters

key
string
required
Object key to delete
public
boolean
default:"false"
Set to true or 1 to delete from the public bucket

Response

key
string
Object key that was deleted
deleted
boolean
Always true on success

Example

# Delete from private bucket
curl -X DELETE "https://your-worker.workers.dev/object?key=temp/old-file.txt"

# Delete from public bucket
curl -X DELETE "https://your-worker.workers.dev/object?key=images/old-photo.png&public=true"
{
  "key": "temp/old-file.txt",
  "deleted": true
}

Use Cases

Image hosting

Upload images to the public bucket and use the returned URL in your app:
# Upload
curl -X PUT "https://your-worker.workers.dev/object?key=avatars/user123.jpg&public=true" \
  -H "Content-Type: image/jpeg" \
  --data-binary @avatar.jpg

# Response: { "url": "https://pub-xxxxx.r2.dev/avatars/user123.jpg", ... }
# Use URL directly: <img src="https://pub-xxxxx.r2.dev/avatars/user123.jpg" />

Private file storage

Store documents that should only be accessible through your Worker:
# Upload
curl -X PUT "https://your-worker.workers.dev/object?key=user-data/123/invoice.pdf" \
  -H "Content-Type: application/pdf" \
  --data-binary @invoice.pdf

# Download (with auth check in your Worker)
curl "https://your-worker.workers.dev/object?key=user-data/123/invoice.pdf" \
  -H "Authorization: Bearer token"

Organized file structure

Use prefixes and delimiters for directory-like organization:
# List all images
curl "https://your-worker.workers.dev/list?prefix=images/&public=true"

# List top-level "directories" only
curl "https://your-worker.workers.dev/list?delimiter=/&public=true"

Environment Variables

Bindings

BUCKET
R2Bucket
Private R2 bucket binding (name: r2-storage-bucket)
PUBLIC_BUCKET
R2Bucket
Public R2 bucket binding (name: r2-storage-public)

Deploy

Deploy to Cloudflare Workers
1

Create buckets

Create both R2 buckets and enable public access on the public one (see Setup above)
2

Deploy Worker

Click the deploy button or run:
npm run deploy
3

Configure variables

In Workers & Pages → your Worker → SettingsVariables:
  • Add PUBLIC_BUCKET_URL = your public bucket URL (e.g., https://pub-xxxxx.r2.dev)
4

Update wrangler.json

Ensure bucket_name values match your actual bucket names in the dashboard

Error Responses

All errors return JSON with this structure:
{
  "error": "Error message",
  "code": "ERROR_CODE"
}
error
string
Human-readable error message
code
string
Error code: INVALID_QUERY, NOT_FOUND, MISSING_PARAM, INTERNAL_ERROR

Source Code

View the complete source code on GitHub: r2-storage

Cloudflare Features

  • Workers: Serverless compute at the edge
  • R2: Object storage with zero egress fees
  • Public buckets: Direct public access to objects
  • Hono: Lightweight web framework