Skip to content

Cloudflare R2 vs AWS S3 -- Comparison and Migration

DodaTech 6 min read

In this tutorial, you will learn how Cloudflare R2 compares to AWS S3 across pricing, performance, API compatibility, and global distribution, along with a step-by-step Migration Strategy. Understanding these differences is important because choosing the wrong object storage provider can lead to unexpected costs, performance issues, or Migration headaches later. A real-world example is a video platform that reduced its monthly cloud bill by 60 percent by moving from S3 to R2 due to the elimination of egress fees.

Pricing Comparison

The most significant difference between R2 and S3 is egress pricing. AWS S3 charges $0.01 to $0.09 per GB for data transfer out to the internet, which can exceed storage costs for read-heavy workloads. R2 charges zero egress fees. Storage pricing is comparable: R2 charges approximately $0.015 per GB per month for standard storage, while S3 standard tier charges $0.023 per GB per month. Operation costs (GET, PUT, DELETE) are also similar between the two services.

Feature Comparison Table

flowchart LR
    subgraph R2
        R1[Zero egress fees]
        R2[Global edge network]
        R3[S3 API compatible]
        R4[Workers integration]
    end
    subgraph S3
        S1[Egress fees]
        S2[Regional storage]
        S3[Native S3 API]
        S4[Event notifications]
        S5[Object Lock Compliance]
    end
    subgraph Shared
        C1[AES-256 encryption]
        C2[Multipart upload]
        C3[Versioning]
        C4[Lifecycle policies]
    end

    R2 --- C1
    S3 --- C1
    R2 --- C2
    S3 --- C4

    style R2 fill:#f90,color:#fff
    style S3 fill:#f90,color:#fff
    style Shared fill:#2ecc71,color:#fff

R2 lacks S3 Event Notifications (SQS, Lambda triggers) and S3 Object Lock (write-once-read-many Compliance). For most standard object storage use cases -- static assets, backups, user uploads -- R2 provides equivalent functionality at lower cost.

API Compatibility Check

// Same code works for both S3 and R2 with endpoint change
import { S3Client, ListBucketsCommand } from '@aws-sdk/client-s3';

async function listBuckets(endpoint, credentials) {
  const client = new S3Client({
    region: 'auto',  // R2 uses 'auto'; S3 uses region like 'us-east-1'
    endpoint,
    credentials
  });

  const { Buckets } = await client.send(new ListBucketsCommand({}));
  return Buckets.map(b => ({ name: b.Name, created: b.CreationDate }));
}

// Usage with R2:
// const r2Result = await listBuckets(
//   'https://<account>.r2.cloudflarestorage.com',
//   { accessKeyId: '...', secretAccessKey: '...' }
// );
// Expected R2 output:
// [{name: "assets", created: "2026-01-15T10:00:00Z"}, {name: "backups", created: "2026-03-20T14:30:00Z"}]

// Usage with S3:
// const s3Result = await listBuckets(
//   'https://s3.us-east-1.amazonaws.com',
//   { accessKeyId: '...', secretAccessKey: '...' }
// );
// Expected S3 output:
// [{name: "my-s3-bucket", created: "2025-11-01T08:00:00Z"}]

The S3 SDK works with both providers by changing the endpoint URL. R2 uses region auto and requires the Cloudflare account ID in the endpoint. S3 requires the specific region endpoint.

Performance Comparison

// Benchmark latency from a Worker to both providers
export default {
  async fetch(request, env) {
    const testKey = 'benchmark-' + Date.now() + '.txt';
    const testData = new TextEncoder().encode('performance test');

    async function testR2() {
      const start = Date.now();
      await env.R2_BUCKET.put(testKey, testData);
      await env.R2_BUCKET.get(testKey);
      await env.R2_BUCKET.delete(testKey);
      return Date.now() - start;
    }

    async function testS3(endpoint) {
      const client = new S3Client({
        region: 'auto',
        endpoint,
        credentials: { accessKeyId: '...', secretAccessKey: '...' }
      });
      const start = Date.now();
      await client.send(new PutObjectCommand({ Bucket: 'bench', Key: testKey, Body: testData }));
      await client.send(new GetObjectCommand({ Bucket: 'bench', Key: testKey }));
      await client.send(new DeleteObjectCommand({ Bucket: 'bench', Key: testKey }));
      return Date.now() - start;
    }

    const r2Time = await testR2();
    // Expected R2 time: 50-150ms (from Worker in same network)
    // Expected S3 time: 200-600ms (from Worker to external region)

    return new Response(JSON.stringify({
      r2: r2Time + 'ms',
      note: 'R2 is faster from Workers because data stays within Cloudflare network'
    }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

R2 operations from Workers are significantly faster because data stays within Cloudflare's network. S3 requests from Workers require an internet round trip to the S3 regional endpoint, typically adding 100-500ms of latency per operation.

Migrating from S3 to R2

# Use rclone to migrate data from S3 to R2
rclone config  # Set up S3 and R2 remotes

# Copy all objects from S3 bucket to R2 bucket
rclone copy s3:my-bucket r2:my-bucket --progress --checksum

# Expected output:
# Transferred: 15.234 GiB / 15.234 GiB, 100%, 45.23 MiB/s
# Errors: 0
# Checks: 1502 / 1502

Rclone supports both S3 and R2 natively. The --checksum flag verifies data integrity by comparing MD5 hashes. For live Migration, run a final incremental sync after the initial copy to transfer any objects that changed.

Updating Application Configuration

// Environment-aware storage client selection
const config = {
  provider: process.env.STORAGE_PROVIDER || 'r2',
  r2: {
    endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
    bucket: process.env.R2_BUCKET,
    credentials: {
      accessKeyId: process.env.R2_ACCESS_KEY,
      secretAccessKey: process.env.R2_SECRET_KEY
    }
  },
  s3: {
    endpoint: `https://s3.${process.env.AWS_REGION}.amazonaws.com`,
    bucket: process.env.S3_BUCKET,
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
    }
  }
};

function createStorageClient() {
  const cfg = config[config.provider];
  return new S3Client({
    region: config.provider === 'r2' ? 'auto' : process.env.AWS_REGION,
    endpoint: cfg.endpoint,
    credentials: cfg.credentials
  });
}

// Expected: Creates S3Client that works with either R2 or S3 based on STORAGE_PROVIDER env var

Abstracting storage behind a configuration flag lets you switch providers without code changes. Test the R2 endpoint with a staging bucket before migrating production traffic.

Common Errors and Troubleshooting

Egress Cost Shock on S3

After migrating to R2, the first invoice shows zero egress charges. Track your S3 egress spend before migrating to calculate savings. Use the AWS Cost Explorer to identify the most expensive buckets.

Region Mismatch

R2 uses a single global namespace with region: 'auto'. S3 requires a specific region. When migrating, remove region-specific logic from your application and use auto for R2.

Missing Features After Migration

Applications relying on S3 Event Notifications (S3 to SQS or Lambda) or S3 Object Lock will not work on R2. Replace event notifications with Workers Queues or scheduled Workers pollers.

Incompatible ACLs

R2 does not support S3 Access Control Lists (ACLs). Use bucket policies or Worker-level authorization instead. Rclone Migration does not transfer ACLs, so verify permissions after Migration.

Bucket Policy Differences

R2 bucket policies are similar to S3 bucket policies but have different condition keys and resource formats. Review the R2 policy documentation and rewrite existing S3 policies before Migration.

Practice Questions

  1. What is the single biggest cost difference between R2 and S3?
  2. Why are R2 operations faster than S3 operations from a Worker?
  3. Which tool is commonly used for migrating data from S3 to R2?

FAQ

Does R2 support S3 Object Lock for compliance requirements?

No. R2 does not currently support S3 Object Lock (WORM model). For Compliance workloads requiring write-once-read-many guarantees, S3 Object Lock or a dedicated Compliance storage solution is necessary. Cloudflare has indicated Object Lock is on the roadmap.

Can I use S3 bucket policies with R2?

R2 supports bucket policies but with a different set of condition keys and actions compared to S3. Review the R2 policy syntax in the Cloudflare documentation before migrating. Simple policies (allow read, allow write from specific IPs) are straightforward to convert.

What happens to existing S3 SDK code when migrating to R2?

Most S3 SDK operations work with R2 by changing only the endpoint URL and region. Operations like PutObject, GetObject, DeleteObject, ListObjects, and multipart upload are fully compatible. Features like ACLs, Object Lock, and event notifications are not supported and require code changes.

Summary

Cloudflare R2 and AWS S3 share core object storage features but differ significantly in pricing and ecosystem. R2 eliminates egress fees and provides faster access from Workers, while S3 offers advanced features like Object Lock and event notifications. Migration is straightforward using rclone or the S3 SDK with endpoint changes. For read-heavy workloads and Workers-based applications, R2 offers substantial cost savings and performance benefits.

This guide is brought to you by the developers of Cloudflare, Amazon Web Services, and Durga Antivirus Pro at DodaTech.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro