Skip to main content
R2 provides object storage with an S3-compatible API. Store and retrieve large files, images, videos, and other objects without egress fees.

Overview

R2 buckets provide:
  • S3-compatible object storage
  • HTTP metadata and custom metadata
  • Multipart uploads for large files
  • Conditional operations (etag matching)
  • Range requests for partial downloads
  • Object lifecycle management
Implementation: src/workerd/api/r2-bucket.h and r2-bucket.c++

Accessing R2 buckets

R2 buckets are bound to your worker in the environment:
export default {
  async fetch(request, env) {
    // Access via env binding
    const object = await env.MY_BUCKET.get('file.txt');
    return new Response(object.body);
  }
};

Uploading objects

Put object

Upload an object:
// Upload text
await env.MY_BUCKET.put('file.txt', 'Hello World');

// Upload binary data
const buffer = new Uint8Array([1, 2, 3, 4]);
await env.MY_BUCKET.put('data.bin', buffer);

// Upload from a stream
const response = await fetch('https://example.com/image.png');
await env.MY_BUCKET.put('image.png', response.body);

Put with options

Set HTTP metadata and custom metadata:
await env.MY_BUCKET.put('document.pdf', pdfData, {
  httpMetadata: {
    contentType: 'application/pdf',
    contentLanguage: 'en-US',
    contentDisposition: 'attachment; filename="document.pdf"',
    contentEncoding: 'gzip',
    cacheControl: 'public, max-age=3600',
    cacheExpiry: new Date(Date.now() + 86400000)
  },
  customMetadata: {
    author: 'Alice',
    department: 'Engineering',
    version: '1.0'
  }
});
Source: src/workerd/api/tests/r2-test.js:8

Put with checksums

Provide checksums for integrity verification:
await env.MY_BUCKET.put('file.txt', data, {
  md5: 'base64-encoded-md5',
  sha1: sha1Buffer,
  sha256: sha256Buffer
});

Downloading objects

Get object

Retrieve an object:
const object = await env.MY_BUCKET.get('file.txt');

if (object === null) {
  return new Response('Not found', { status: 404 });
}

// Object properties
console.log('Key:', object.key);
console.log('Size:', object.size);
console.log('ETag:', object.etag);
console.log('Uploaded:', object.uploaded);

// Get body as text
const text = await object.text();

// Or get body as stream
return new Response(object.body);
Source: src/workerd/api/tests/r2-test.js:88

Get object body types

R2 objects support multiple body reading methods:
const object = await env.MY_BUCKET.get('data');

// Read as text
const text = await object.text();

// Read as JSON
const json = await object.json();

// Read as ArrayBuffer
const buffer = await object.arrayBuffer();

// Read as Blob
const blob = await object.blob();

// Access as ReadableStream
const stream = object.body;

Get with options

Use conditional requests and range requests:
// Conditional get (only if etag matches)
const object = await env.MY_BUCKET.get('file.txt', {
  onlyIf: {
    etagMatches: '"abc123"'
  }
});

// Range request (get bytes 0-999)
const object = await env.MY_BUCKET.get('large-file.bin', {
  range: {
    offset: 0,
    length: 1000
  }
});

// Get with suffix (last 1000 bytes)
const object = await env.MY_BUCKET.get('log.txt', {
  range: {
    suffix: 1000
  }
});
Source: src/workerd/api/r2-bucket.h:78

Object metadata

HTTP metadata

Access HTTP metadata:
const object = await env.MY_BUCKET.get('file.txt');

console.log(object.httpMetadata.contentType);
console.log(object.httpMetadata.contentLanguage);
console.log(object.httpMetadata.contentDisposition);
console.log(object.httpMetadata.contentEncoding);
console.log(object.httpMetadata.cacheControl);
console.log(object.httpMetadata.cacheExpiry);

Custom metadata

Access custom metadata:
const object = await env.MY_BUCKET.get('document.pdf');

const author = object.customMetadata.author;
const version = object.customMetadata.version;

Checksums

Verify object integrity:
const object = await env.MY_BUCKET.get('file.txt');

if (object.checksums.md5) {
  console.log('MD5:', object.checksums.md5);
}
if (object.checksums.sha256) {
  console.log('SHA-256:', object.checksums.sha256);
}
Source: src/workerd/api/r2-bucket.h:122

Head object

Get object metadata without downloading the body:
const object = await env.MY_BUCKET.head('file.txt');

if (object === null) {
  return new Response('Not found', { status: 404 });
}

console.log('Size:', object.size);
console.log('ETag:', object.etag);
console.log('Content-Type:', object.httpMetadata.contentType);
// Note: object.body is null for head requests

Deleting objects

Delete single object

await env.MY_BUCKET.delete('file.txt');

Delete multiple objects

const keys = ['file1.txt', 'file2.txt', 'file3.txt'];
await env.MY_BUCKET.delete(keys);

Listing objects

List objects in a bucket:
// List all objects
const listed = await env.MY_BUCKET.list();

for (const object of listed.objects) {
  console.log('Key:', object.key);
  console.log('Size:', object.size);
  console.log('Uploaded:', object.uploaded);
}

// Check if more results available
if (listed.truncated) {
  console.log('More objects available');
}

List with options

Filter and paginate list results:
const listed = await env.MY_BUCKET.list({
  prefix: 'images/',     // Only objects starting with 'images/'
  delimiter: '/',        // Treat '/' as directory separator
  limit: 100,           // Maximum objects to return
  cursor: 'abc123',     // Pagination cursor
  include: ['httpMetadata', 'customMetadata']
});

// Process objects
for (const object of listed.objects) {
  console.log(object.key, object.httpMetadata?.contentType);
}

// Process common prefixes (directories)
for (const prefix of listed.delimitedPrefixes) {
  console.log('Directory:', prefix);
}

// Get next page
if (listed.truncated) {
  const nextPage = await env.MY_BUCKET.list({
    cursor: listed.cursor
  });
}

Multipart uploads

Upload large files in parts:
// Start multipart upload
const upload = await env.MY_BUCKET.createMultipartUpload('large-file.bin', {
  httpMetadata: {
    contentType: 'application/octet-stream'
  }
});

const uploadId = upload.uploadId;
const parts = [];

// Upload parts (minimum 5 MB each, except the last part)
for (let i = 0; i < chunks.length; i++) {
  const part = await upload.uploadPart(i + 1, chunks[i]);
  parts.push({
    partNumber: i + 1,
    etag: part.etag
  });
}

// Complete the upload
const object = await upload.complete(parts);
console.log('Upload complete:', object.key);

Resume multipart upload

Resume an existing upload:
const upload = env.MY_BUCKET.resumeMultipartUpload(
  'large-file.bin',
  uploadId
);

// Continue uploading parts...

Abort multipart upload

Cancel an upload:
await upload.abort();

Conditional operations

Conditional put

Only put if conditions are met:
try {
  await env.MY_BUCKET.put('file.txt', 'new content', {
    onlyIf: {
      etagMatches: '"old-etag"'  // Only update if etag matches
    }
  });
} catch (error) {
  console.log('Condition not met');
}

Conditional get

Only get if conditions are met:
const object = await env.MY_BUCKET.get('file.txt', {
  onlyIf: {
    etagDoesNotMatch: '"cached-etag"',  // Only get if modified
    uploadedAfter: new Date('2024-01-01')
  }
});

if (object === null) {
  console.log('Condition not met');
}
Source: src/workerd/api/r2-bucket.h:91

Patterns

Serve files with proper headers

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const key = url.pathname.slice(1);
    
    const object = await env.BUCKET.get(key);
    
    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 });
  }
};

Upload with validation

async function uploadFile(bucket, key, data) {
  // Calculate checksum
  const hash = await crypto.subtle.digest('SHA-256', data);
  const sha256 = new Uint8Array(hash);
  
  // Upload with checksum
  await bucket.put(key, data, {
    sha256,
    httpMetadata: {
      contentType: 'application/octet-stream'
    },
    customMetadata: {
      uploadedAt: new Date().toISOString()
    }
  });
}

Download with range support

export default {
  async fetch(request, env) {
    const range = request.headers.get('Range');
    const key = new URL(request.url).pathname.slice(1);
    
    if (range) {
      // Parse range header (simplified)
      const [start, end] = range
        .replace('bytes=', '')
        .split('-')
        .map(Number);
      
      const object = await env.BUCKET.get(key, {
        range: { offset: start, length: end - start + 1 }
      });
      
      if (object === null) {
        return new Response('Not found', { status: 404 });
      }
      
      return new Response(object.body, {
        status: 206,
        headers: {
          'Content-Range': `bytes ${start}-${end}/${object.size}`,
          'Content-Length': object.size.toString()
        }
      });
    }
    
    const object = await env.BUCKET.get(key);
    return new Response(object.body);
  }
};

Best practices

Always set content type for better client handling:
await env.BUCKET.put('image.png', imageData, {
  httpMetadata: {
    contentType: 'image/png'
  }
});
Verify uploads with checksums:
const hash = await crypto.subtle.digest('SHA-256', data);
await env.BUCKET.put(key, data, {
  sha256: new Uint8Array(hash)
});
Upload large files (> 100 MB) in parts:
const upload = await env.BUCKET.createMultipartUpload(key);
// Upload in 10 MB chunks
Check if objects exist without downloading:
const object = await env.BUCKET.head(key);
if (object === null) {
  // Object doesn't exist
}

Implementation details

R2 is implemented in:
  • src/workerd/api/r2-bucket.h / .c++ - Main R2 API (2000+ lines)
  • src/workerd/api/r2-multipart.h / .c++ - Multipart upload support
  • src/workerd/api/r2-admin.h / .c++ - Admin operations
  • src/workerd/api/r2-rpc.h / .c++ - RPC client
The R2 API communicates with R2 storage through HTTP client connections. Definition from src/workerd/api/r2-bucket.h:42:
class R2Bucket: public jsg::Object {
  struct GetOptions {
    jsg::Optional<kj::OneOf<Conditional, jsg::Ref<Headers>>> onlyIf;
    jsg::Optional<kj::OneOf<Range, jsg::Ref<Headers>>> range;
  };

  jsg::Promise<kj::Maybe<jsg::Ref<R2Object>>> get(
    kj::String key,
    jsg::Optional<GetOptions> options);

  jsg::Promise<kj::Maybe<jsg::Ref<R2Object>>> put(
    kj::String key,
    Body body,
    jsg::Optional<PutOptions> options);
};

Build docs developers (and LLMs) love