Skip to main content

Chapter 13: R2: Object Storage Without Egress Fees

How much can we actually save with R2, and when does it matter?


Every cloud provider charges you to retrieve your own data; store a terabyte, read it ten times, and you pay for ten terabytes of egress. These costs accumulate quietly until the bill arrives, and by then the architecture is locked in. R2 eliminates egress fees entirely.

Zero egress isn't a discount; it's a different economic model that enables serving the same large file to millions of users, building data interchange systems without transfer costs, and creating backups you can actually afford to restore. Understanding when these economics matter determines whether R2 fits your architecture.

Why zero egress exists

Cloudflare's business model inverts traditional cloud economics: AWS profits from data at rest and in transit, whereas Cloudflare profits from requests flowing through their network, selling security, performance, and compute on that traffic. Zero egress isn't generosity; it's incentive alignment.

Zero egress isn't a promotional rate; it's structural. Every R2 request flows through Cloudflare's network, potentially invoking Workers, hitting cache, and passing through their WAF, making R2 a funnel for the services Cloudflare actually monetises.

This model explains when R2 makes strategic sense: architectures routing traffic through Cloudflare benefit most. Using R2 purely as a storage backend from AWS infrastructure gets the egress savings but misses the ecosystem integration that makes R2 compelling.

The economics in practice

Scenario (monthly)S3 CostR2 CostSavings
1 TB storage, 10 TB egress~$740~$1598%
10 TB storage, 100 TB egress~$7,200~$15098%
100 TB storage, 10 TB egress~$3,100~$1,50052%
1 TB storage, 100 GB egress~$30~$1550%

R2's advantage compounds with read frequency. Store once, serve a million times, and the economics transform from marginal to decisive. R2 wins decisively when egress exceeds 30% of total storage costs. Below that threshold, migration complexity may exceed savings. Calculate your actual egress-to-storage ratio before committing.

Content delivery illustrates the extreme case. A popular image served to a million users generates a million units of egress from a single stored object. Traditional cloud pricing makes popularity expensive; R2 inverts this. The same logic applies to API responses with large payloads, data interchange between systems, and public datasets.

A concrete example

Consider a video learning platform serving course content globally. Each course contains 20 hours of video at 2 GB per hour (40GB per course). With 500 courses and 50,000 monthly active users each watching an average of 3 courses, monthly egress reaches roughly 6 PB.

On S3 with CloudFront, even with volume discounts, this egress runs approximately $50,000 per month. The storage itself costs perhaps $500. Every decision filters through "how do we reduce data transfer?" Teams implement aggressive caching, lower video quality, and limit concurrent streams; all to manage egress costs rather than improve user experience.

On R2, the same workload costs roughly $600 per month total. The 99% cost reduction isn't the interesting part. The architecture becomes unconstrained. Higher quality video? No incremental cost. Preview clips for non-subscribers? No egress penalty. When data movement is free, you stop designing systems that avoid moving data.

The principle scales down: a SaaS platform serving 10 GB of reports monthly saves hundreds; a media company serving 100 TB saves tens of thousands. The threshold question isn't "will we save money" but "is the saving large enough to justify migration?"

Operations pricing: the cost that replaces egress

Zero egress doesn't mean zero cost beyond storage. R2 charges for operations in two classes: Class A operations (writes and mutations such as PutObject, CopyObject, ListObjects) cost $4.50 per million requests, while Class B operations (reads such as GetObject, HeadObject) cost $0.36 per million requests. Delete operations are free. For comparison, S3 charges $5.00 per million PUT requests and $0.40 per million GET requests, so R2 is marginally cheaper per operation as well as eliminating egress entirely.

R2's free tier absorbs most light workloads: 10 GB of storage, 1 million Class A operations, and 10 million Class B operations per month at no cost. A small application storing a few gigabytes and serving a few million reads per month may pay nothing at all.

For most architectures, operations costs are negligible compared to storage. But for very high-frequency access patterns, they can become the dominant expense. Consider a CDN-replacement pattern serving 300 million objects per month from R2: after the free tier, that's 290 million billable Class B operations at $0.36 per million, totalling roughly $104 per month. The storage for those objects might cost $15. When your read-to-store ratio reaches hundreds or thousands to one, operations costs dwarf everything else. This is the same pattern that makes S3 request pricing accumulate for small-object workloads, but without the compounding effect of egress on top.

The architectural implication: if you're designing a system that serves the same objects millions of times, Cloudflare's CDN cache sits in front of R2 and absorbs repeat requests. Cache hits don't count as R2 operations. The combination of R2 storage with aggressive edge caching keeps operations costs low even for extremely popular content. Architect for cacheability, and operations pricing rarely matters.

The broader hidden comparison

Egress isn't the only S3 cost that accumulates silently beyond request pricing. Glacier retrieval carries both retrieval fees and per-gigabyte charges. Cross-region replication incurs transfer costs. R2 sidesteps some of these, though R2's Infrequent Access tier carries its own retrieval fees.

R2 trades S3's feature depth for pricing simplicity: two storage classes rather than S3's extensive tiering hierarchy, no true archival tiers for data accessed once per year, and no Object Lock for compliance-grade immutability. If you need those features, no amount of egress savings makes migration worthwhile.

When to stay with S3

Zero egress doesn't automatically justify migration. The question isn't "can you save money" but "is the saving worth managing two storage systems?"

Ecosystem integration often outweighs egress savings. S3 connects natively to AWS Backup, AWS Analytics, CloudWatch metrics, IAM policies, and dozens of other services. A 50% egress reduction that requires reimplementing your backup strategy isn't a good trade.

Internal AWS transfers are often free. Data moving between S3 and other AWS services in the same region frequently has zero or discounted egress. If your S3 buckets primarily serve Lambda functions and EC2 instances rather than external users, R2's egress advantage evaporates.

Operational complexity has real costs. Two storage systems mean two sets of credentials, two monitoring configurations, two failure modes, two services in incident runbooks. For workloads under $500/month in egress, this complexity tax probably exceeds any savings.

Compliance requirements may mandate specific providers. Regulatory or contractual obligations sometimes specify particular cloud providers or require features like Object Lock.

True archival storage is cheaper elsewhere. For terabytes of data accessed once per year, S3 Glacier Deep Archive at $0.00099/GB-month beats R2's Infrequent Access at $0.01/GB-month, even accounting for retrieval costs and egress fees. R2's Infrequent Access is for data accessed less frequently, not almost never.

The decision threshold is that if egress costs are under 20% of your total storage spend and you're deeply integrated with AWS services, migration probably isn't worth it. If egress exceeds 50%, or you're building new systems without AWS dependencies, R2 deserves serious consideration.

S3 compatibility

R2 implements the S3 API, not a "similar" API but the actual protocol that existing S3 SDKs speak. Point your AWS SDK at R2's endpoint, provide R2 credentials, and most operations work identically.

This compatibility covers core operations constituting 90% of S3 usage: GetObject, PutObject, DeleteObject, HeadObject, ListObjectsV2, CopyObject, multipart uploads, presigned URLs, conditional requests, and custom metadata. For typical object storage patterns, R2 is a drop-in replacement.

S3 Compatibility Gaps

S3 compatibility doesn't mean feature parity. R2 lacks certified WORM compliance (Object Lock), S3 Select, deep archival tiers, and S3 Inventory. Verify your required features are supported before committing to migration.

S3 compatibility doesn't mean feature parity. R2 lacks certified WORM compliance, S3 Select (in-place queries), deep archival tiers, and S3 Inventory. Check feature compatibility before assuming migration is possible.

The gaps are real but specific:

Certified WORM compliance. S3's Object Lock provides write-once-read-many storage with compliance mode (truly immutable, not even AWS can delete), governance mode, and legal hold capabilities; financial services firms need SEC 17a-4 certification, and healthcare and legal sectors often require similar immutability guarantees. R2 offers Bucket Locks, which provide retention policies preventing deletion and overwriting for specified periods or indefinitely, making them useful for preventing accidental deletion and enforcing basic retention. However, they lack compliance-mode immutability, legal hold, and regulatory certifications, and rules can be removed by administrators. If your compliance requirements mandate certified WORM storage, S3 Object Lock remains necessary.

S3 Select lets you query object contents directly, running SQL against CSV or JSON files without downloading them. If you're not already using it on S3, its absence won't affect you.

S3 Inventory provides automated reports of bucket contents and metadata for audits or compliance. The alternative is custom inventory using listing APIs, which works but requires you to build and maintain it.

Glacier and deep archive tiers provide extremely cheap storage for data accessed rarely. R2's Infrequent Access tier serves a different purpose: reducing costs for data accessed occasionally but not frequently. For petabytes with annual access patterns, S3's archival tiers remain cheaper despite egress fees.

The binding model difference

Through the S3 API, R2 is external storage with credentials in configuration, network round-trips, and SDK initialisation overhead. Through Workers bindings, R2 is part of your compute environment.

wrangler.toml - R2 bucket binding
[[r2_buckets]]
binding = "STORAGE"
bucket_name = "my-bucket"

This configuration makes R2 available as env.STORAGE in your Worker. No credentials in code, no connection management, no SDK initialisation. The binding appears as a typed object with methods for storage operations.

Bindings enable streaming patterns where objects flow directly from R2 through your Worker to the response without loading into memory. A Worker with 128 MB of memory can serve a 5 GB video because the content streams through rather than accumulating. R2 becomes less "storage your code calls" and more "storage woven into your execution environment".

This integration is R2's strongest advantage for Cloudflare-centric architectures. If you're using Workers, R2 binding access is simpler, faster, and more natural than S3 SDK access to any storage provider. If you're accessing R2 purely through the S3 API from non-Cloudflare infrastructure, you're missing the deeper integration that distinguishes R2 from "S3 with different pricing."

Access patterns

Three patterns serve R2 content. Start with Worker-mediated access; deviate to presigned URLs for large transfers or public buckets for genuinely public static content.

Worker-mediated access: the default

Every request passes through a Worker that fetches from R2 and returns the response. You can authenticate, transform, log, rate-limit, or apply any logic before serving content. The Worker sees every request and controls every response.

The costs are real but usually acceptable. Every request consumes Worker invocations and CPU time, but Workers are cheap and fast. Request body limits apply (100 MB for uploads on the default plan), so large file uploads need a different approach.

Worker-mediated access fails when uploads exceed request body limits or when you're serving high-volume static content where per-request compute adds cost without adding value. For everything else, it's the right default because it preserves your options. You can always optimise later by moving specific paths to presigned URLs; you can't easily add authentication to content you've already exposed publicly.

Presigned urls: the escape hatch for large transfers

Generate time-limited URLs granting direct access to R2, bypassing Workers entirely. A Worker handles the authorisation decision and URL generation; the actual data transfer happens directly between client and R2.

This pattern exists primarily for large file transfers. Upload presigned URLs let clients send files directly to R2 without streaming through your Worker: no 100 MB limit, no Worker CPU consumption during transfer, upload speed limited only by client bandwidth. Download presigned URLs serve large files without Worker involvement.

The tradeoff is control. Once you've issued a presigned URL, you can't revoke it before expiration. You can't transform content on the fly. You can't log individual accesses, only URL generation.

Use presigned URLs when Worker limits constrain you (uploads over 100 MB) or when per-request Worker costs matter (high-volume large file downloads). Keep expiration times short: minutes for uploads, and hours at most for downloads.

User-generated content upload pattern

For applications accepting user uploads, combine presigned URLs with event-driven processing:

UGC upload flow with presigned URLs
// 1. Client requests upload permission
export async function handleUploadRequest(request: Request, env: Env) {
const { filename, contentType } = await request.json();
const key = `uploads/${crypto.randomUUID()}/${filename}`;

// Generate presigned PUT URL
const uploadUrl = await generatePresignedUrl(env.STORAGE, key, {
method: "PUT",
expiresIn: 300, // 5 minutes
headers: { "Content-Type": contentType }
});

return Response.json({ uploadUrl, key });
}

// 2. Client uploads directly to R2 using presigned URL
// 3. R2 event notification triggers processing queue
// 4. Queue consumer handles validation, thumbnails, moderation

The Worker acts purely as an authorisation gateway. It validates the user has permission to upload, generates time-limited credentials, and returns immediately. The actual data transfer happens directly between client and R2, eliminating the Worker from the data path entirely.

Event notifications then trigger asynchronous processing: virus scanning, image resizing, content moderation through Workers AI. The upload completes immediately from the user's perspective; processing happens in the background.

This pattern scales elegantly. Upload capacity is limited only by R2, not by Worker request body limits or CPU time. Processing scales independently through queue concurrency settings. A spike in uploads doesn't overwhelm your processing capacity; the queue buffers the work.

Local uploads: improving performance for global users

When clients upload data from a different region than your bucket, object data must travel across potentially significant geographic distance. A user in Singapore uploading to a bucket in Europe experiences this latency on every upload. Local Uploads addresses this by writing object data to storage infrastructure near the client, then asynchronously replicating it to your bucket. The object is immediately accessible and remains strongly consistent throughout the process.

The performance improvement is substantial. Cloudflare reports up to 75% reduction in time to last byte for upload requests when Local Uploads is enabled. For applications where upload responsiveness affects user experience (file sharing, media uploads, collaborative tools), this is worth enabling.

The trade-off is jurisdictional. Local Uploads cannot be used with buckets that have jurisdictional restrictions because it requires temporarily routing data through locations outside the bucket's region. If your compliance requirements mandate that data never touch infrastructure outside a specific jurisdiction, even transiently, you cannot use this feature.

There's also a subtle consistency consideration. When Local Uploads places data near the uploader, reads from near the uploader's region are fast immediately. However, reads from near the bucket's region may experience cross-region latency until replication completes. For most applications this is invisible, but if your workflow requires immediate read-after-write from a location near the bucket rather than near the uploader, design accordingly.

Local Uploads is free to enable and has no additional cost. It's a configuration toggle in bucket settings, not an architectural change. For applications with globally distributed users and upload-sensitive workflows, enable it unless jurisdictional restrictions prevent it.

Public bucket access: for genuinely public content

Configure the entire bucket for public access, making all objects retrievable without authentication. This is the simplest pattern for content that is unconditionally public: static assets, software releases, public documentation.

The tradeoff is absolute, making everything in the bucket public with no per-request authentication.

Use public buckets for static assets served to all users (CSS, JavaScript, marketing images), public downloads (open-source releases, public datasets), and CDN origin scenarios where all content is intentionally public. Avoid using them for user-generated content, even if "usually" public, because the moment you need to make one object private, the architecture requires reworking.

R2 as CDN origin

For architects coming from AWS, R2 integrates naturally with Cloudflare's CDN. Using S3 as a CloudFront origin requires configuring two services: S3 bucket policies, CloudFront distribution settings, origin access controls, cache behaviours. The configuration spans multiple consoles and mental models.

R2 behind Cloudflare's CDN is the default behaviour, not an integration to configure. Public bucket content is automatically served through Cloudflare's edge network. Worker-mediated content benefits from Cloudflare's caching when you set appropriate cache headers.

This simplification compounds with zero egress. Cache misses on CloudFront hit S3 and incur egress charges; cache misses through Cloudflare hit R2 and incur only operations charges. You can cache more aggressively without the anxiety that cache misses will inflate your bill.

Image optimisation pattern

For image-heavy applications, store only original images in R2. Transformed variants (thumbnails, responsive sizes, format conversions) generate on-demand through Cloudflare Image Resizing and cache at the edge.

https://example.com/cdn-cgi/image/width=400,quality=80/images/product.jpg

The pattern eliminates variant storage entirely. No need to generate and store thumbnails during upload. No lifecycle rules managing variant proliferation. No synchronisation between originals and derivatives. Store once at maximum quality; transformations happen at request time.

The economics work because transformations cache. The first request for a 400px thumbnail generates it and caches the result. Subsequent requests serve from cache. You pay for transformation once per unique variant per cache lifetime, not once per request.

Transform Rules enable migration from other image CDNs. If you're using Imgix or Cloudinary URL patterns, Transform Rules can rewrite those URLs to Cloudflare's format without application-level changes. This enables zero-downtime migration where you cut over DNS and existing URLs continue working.

Event-driven processing

R2 can notify Workers when objects change, enabling reactive architectures without polling. Configure notification rules that filter by action, prefix, and suffix; matching events flow to a Queue that triggers your processing Worker. The integration with Queues (Chapter 8) provides automatic retry semantics and dead-letter handling.

This pattern suits workflows where upload and processing can be decoupled. Image uploads trigger thumbnail generation. Document uploads trigger text extraction. File arrivals trigger validation or virus scanning. The upload completes immediately; processing happens asynchronously. Lifecycle-triggered deletions can also generate notifications for audit logging or cleanup workflows.

The decision comes down to one question: must processing complete before you can confirm the upload succeeded? If yes, use synchronous validation. If processing can happen after upload confirmation, event-driven patterns reduce latency and let you scale processing independently.

Event notifications have characteristics you must design around. Events aren't instantaneous, aren't guaranteed in order, and are at-least-once. If processing order matters, your handler must enforce it.

Event Handlers Must Be Idempotent

R2 event notifications are at-least-once. The same event may be delivered multiple times. Design handlers to process duplicate notifications safely.

Handlers must be idempotent. R2 event notifications are at-least-once. The same upload may trigger multiple notifications. Handlers that aren't idempotent (charging payments twice, sending duplicate emails, appending duplicate records) will create production incidents. Design for duplicate events from day one.

Storage classes and lifecycle rules

R2 offers two storage classes: Standard and Infrequent Access. Standard costs $0.015/GB-month. Infrequent Access costs $0.01/GB-month but adds a retrieval fee of $0.01/GB, doubled operations costs (Class A rises from $4.50 to $9.00 per million; Class B from $0.36 to $0.90 per million), and a 30-day minimum storage duration.

The crossover point is straightforward: if monthly retrieval volume exceeds stored volume, Standard wins. For data accessed less than once per month (old logs, historical records, long-tail user content), Infrequent Access reduces your bill.

Lifecycle rules automate transitions and handle expiration. Configure rules that move objects from Standard to Infrequent Access after a specified period and delete objects after retention expires. Rules operate at the bucket level with prefix filtering, so different object hierarchies can have different policies.

wrangler.toml - Logs bucket binding
# Example: transition logs to Infrequent Access after 30 days, delete after 90
[[r2_buckets]]
binding = "LOGS"
bucket_name = "application-logs"

The lifecycle configuration is set via the dashboard or API, not wrangler.toml. The binding just connects your Worker to the bucket; lifecycle rules operate independently.

One constraint: objects in Infrequent Access cannot transition back to Standard via lifecycle rules. If cold data becomes hot, you'll need to copy the object (creating a new one in Standard) rather than transition it in place. If you're uncertain whether data will stay cold, the retrieval fees might offset the storage savings.

When infrequent access makes sense

Infrequent Access works well for data with predictable low-access patterns: application logs older than a month, user-generated content with long-tail access (old photos, archived documents), backup data that exists for disaster recovery but hopefully never gets restored.

Infrequent Access works poorly for data with unpredictable access spikes or many small objects. The per-operation cost increase (roughly double) can outweigh storage savings when you're storing many small files rather than fewer large ones.

What r2's tiers don't provide

R2's Infrequent Access is not a Glacier equivalent. S3 Glacier and Glacier Deep Archive offer storage at $0.004/GB-month and $0.00099/GB-month respectively. The tradeoff is retrieval time: Glacier takes minutes to hours; Deep Archive takes up to 12 hours. R2 Infrequent Access retrieves instantly, just with a per-GB fee.

For truly archival data (compliance records retained for seven years, disaster recovery backups tested annually), S3's archival tiers will be significantly cheaper than R2 even accounting for egress fees. R2's Infrequent Access targets data that's accessed infrequently but needs immediate availability.

If your workload includes petabytes of genuinely cold data alongside terabytes of active data, a hybrid approach makes sense: R2 for active and occasionally-accessed data, S3 Glacier or GCS Archive for truly cold data where per-GB storage cost dominates.

Metadata and queryability

R2 objects have metadata (content type, custom key-value pairs, timestamps) but R2 has no query capability. You can list objects by prefix, but you can't ask "find all objects uploaded by user X" without iterating through listings. This is fundamental to object storage, not specific to R2.

For simple access patterns, prefix-based organisation suffices. Store user uploads at users/{userId}/uploads/{filename} and listing by prefix gives you a user's files efficiently.

For complex access patterns, separate metadata from content. Store the file in R2 with a generated key (a UUID avoids conflicts and leaking information). Store metadata in D1: the R2 key, original filename, owner, upload timestamp, file size, content type, and application-specific attributes. Query D1 to find objects; fetch from R2 to retrieve content.

D1 gives you full SQL queryability over file metadata: find all PDFs uploaded last month, find the largest files per user, find documents matching a full-text search. R2 gives you cheap, scalable blob storage with zero egress.

The decision point is query complexity. If your access patterns map cleanly to prefix-based listing, skip the D1 layer. If you need queries that cross dimensions (files by date range, files by type, files matching search terms), add D1 metadata from the start.

What R2 enables

Zero egress makes certain patterns practical that were cost-prohibitive before.

Iterative processing becomes cheap. Re-read source data at each stage of a multi-step pipeline without accumulating transfer costs. Design pipelines for clarity and correctness, not to minimise reads.

Backups become accessible. Traditional cloud backup economics are asymmetric: cheap to write, expensive to restore. Restoring a 10 TB backup to verify it works costs $900 in S3 egress. R2 makes backup restoration the same cost as backup creation. You might actually test your disaster recovery.

Data interchange stops accumulating costs. A file moving through five services generates five egress charges elsewhere. Zero egress enables architectures with more data sharing and less defensive data duplication.

Generosity becomes affordable. Free tiers, preview content, and public datasets cost the same as serving paying customers. You can be more generous with trials and public goods because serving them doesn't drain your budget.

What R2 gives up

R2 trades feature depth for pricing simplicity, lacking the deep archival tiers that S3 provides. S3's Glacier Deep Archive stores data at $0.00099/GB-month, ten times cheaper than R2's Infrequent Access, making archival tiers remain cheapest even with egress fees for genuinely cold data accessed once per year or less.

No certified WORM compliance. R2's Bucket Locks provide retention policies that prevent deletion and overwriting, which is useful for basic retention requirements, but compliance scenarios requiring certified immutable storage cannot rely on R2 alone. S3's Object Lock in compliance mode is truly immutable with SEC 17a-4 certification, whereas R2's Bucket Locks can be modified by administrators. Financial services firms with regulatory requirements, healthcare organisations with specific HIPAA interpretations, and legal departments with litigation hold obligations should verify whether Bucket Locks satisfy their compliance needs or whether certified WORM storage remains necessary.

Limited geographic control. You can specify jurisdiction hints for data sovereignty, but you can't select specific regions the way you can with S3. If regulations require data in specific locations with audit trails proving it, verify R2's jurisdictional controls satisfy your requirements.

Smaller ecosystem. Fewer third-party tools integrate directly with R2. S3 API compatibility means many work with configuration changes, but the ecosystem remains smaller than S3's.

Comparing with other providers

This comparison helps you decide whether R2 fits your workload, not declare a universal winner. Each service optimises for different constraints.

AspectR2S3Azure BlobGCS
EgressFree$0.09/GB$0.087/GB$0.12/GB
Storage (standard)$0.015/GB$0.023/GB$0.018/GB$0.020/GB
Storage (infrequent)$0.01/GB$0.0125/GB$0.01/GB$0.01/GB
Storage (archive)N/A$0.00099/GB$0.00099/GB$0.0012/GB
Write operations$4.50/M$5.00/M$5.40/M$5.00/M
Read operations$0.36/M$0.40/M$0.40/M$0.40/M
Max object5 TB5 TB190.7 TB5 TB
S3 compatibleYesNativeNoPartial
Storage tiers26+44
WORM/Object LockPartial*YesYesYes
Lifecycle rulesYesYesYesYes

*R2's Bucket Locks provide retention policies but lack compliance-mode immutability, legal hold, and SEC 17a-4 certification.

R2 wins for egress-heavy workloads and architectures routing traffic through Cloudflare. S3 wins for feature completeness, ecosystem integration, and deep AWS dependencies. Azure Blob wins when you're committed to Azure's platform. GCS wins for Google Cloud integration.

The choice often isn't exclusive. Many architectures use R2 for egress-heavy content delivery while keeping S3 for workloads requiring archival tiers or certified WORM compliance.

R2 as multi-cloud data hub

Zero egress fundamentally changes multi-cloud economics. Position R2 as a central data interchange layer where data flows in from any cloud and out to any consumer without accumulating transfer costs.

The pattern is straightforward: a data provider writes to R2 once, and multiple consumers in different clouds read without egress penalties. What would cost thousands in cross-cloud transfer fees becomes a storage cost measured in dollars.

For data lake architectures, this enables genuine multi-cloud analytics. Store Delta or Iceberg tables in R2, query from Spark clusters in any cloud, from Snowflake, from DuckDB on a developer's laptop. The same data serves multiple compute environments without copying and without transfer costs.

R2's Iceberg-compatible Data Catalog extends this pattern. Query engines that understand Iceberg can query R2 data directly, treating it as a data warehouse without the traditional warehouse costs.

R2 SQL: native query without external compute

R2 SQL provides a serverless, distributed query engine native to Cloudflare. Rather than spinning up Spark clusters or provisioning data warehouse infrastructure, you query Iceberg tables directly through Wrangler or the HTTP API. No compute infrastructure to manage, no connection pools to configure, no idle capacity to pay for.

The trade-off is SQL capability. R2 SQL supports basic analytics: SELECT, WHERE, GROUP BY, HAVING, ORDER BY, and aggregations (COUNT, SUM, AVG, MIN, MAX). It does not support JOINs, subqueries, window functions, or JSON field filtering. Results are limited to 10,000 rows. For complex analytical workloads requiring multi-table joins or sophisticated transformations, external engines like Spark, Snowflake, or DuckDB remain necessary.

The decision framework is straightforward. Use R2 SQL when your queries involve single-table aggregations, filtering, and grouping against Iceberg data, and when you want to avoid provisioning external compute. Use external engines when you need JOINs, complex transformations, or result sets exceeding 10,000 rows. The same underlying Iceberg tables work with both approaches, so you can start with R2 SQL for simple queries and add external engines as analytical requirements grow.

R2 SQL is currently in open beta with no additional cost beyond standard R2 operations. The pricing model will eventually bill based on data scanned, similar to other serverless query engines.

The architectural implication: for data that multiple systems need, R2 often belongs in the middle even if you're not using other Cloudflare services. The hub-and-spoke model with R2 at the centre can reduce total cloud costs substantially.

Migration strategies

Migrating from S3 to R2 is mechanically straightforward: change endpoints, update credentials, copy data. But the approach matters for cost and disruption.

Sippy: incremental migration

Sippy implements lazy migration, copying objects on first access rather than bulk-copying upfront. When a request arrives for an object not yet in R2, Sippy fetches it from S3, serves it to the client, and copies it to R2 simultaneously. Future requests serve from R2 directly.

The economic insight is egress arbitrage. You're already paying S3 egress to serve the object to users. Sippy piggybacks on that request to populate R2, avoiding the separate egress cost of a bulk migration. Actively-accessed objects migrate at zero incremental egress cost.

Combine Sippy with Super Slurper for optimal efficiency: start with Sippy to migrate actively-accessed objects through normal traffic, then use Super Slurper at migration end to bulk-move remaining cold data. Legacy data that's never accessed simply stays at source until you decide whether to migrate or delete it.

One critical consideration: ETags may not match between S3 and R2 when using Sippy. The system makes autonomous decisions about multipart operations for performance optimisation, which affects ETag calculation. If your systems rely on ETag matching for cache validation or change detection, plan accordingly.

Super slurper: bulk migration

Super Slurper handles bulk migrations from S3 and Google Cloud Storage. For detailed guidance, see Chapter 25. The decision of whether and when to migrate requires more thought.

Migrate existing data when:

  • Egress costs exceed 30% of your total storage spend
  • You don't need features R2 lacks (Object Lock, deep archival tiers)
  • The workload isn't deeply integrated with S3-specific AWS services
  • You have a natural migration window (major version release, architecture revision)

Start new projects on R2 when:

  • Your architecture routes traffic through Cloudflare
  • You're building content delivery, file sharing, or data interchange systems
  • You want to avoid accumulating AWS dependencies
  • Egress-heavy patterns are likely based on the application's nature

Stay on S3 when:

  • Your S3 integration is deep (many AWS services depending on S3 events, policies, integrations)
  • Compliance requires S3-specific features like Object Lock
  • You need true archival storage for cold data at scale
  • Egress costs are minimal relative to total storage costs
  • The operational overhead of changing isn't justified by the savings

Use both when:

  • Different workloads have different characteristics
  • You want to migrate incrementally and prove the model
  • Some content genuinely needs S3 features while other content is purely egress-heavy
  • Active data belongs in R2 while archival data belongs in Glacier

For existing systems, migration is rarely urgent. R2's value comes from avoiding future egress costs, not reclaiming past spending. New projects and new data are natural starting points.

R2's zero-egress model has made it popular for analytical data storage using open table formats like Apache Iceberg. Companies store data lakes in R2 and query from Spark, DuckDB, Snowflake, or Databricks without transfer costs. R2 Data Catalog provides a managed Iceberg catalog, and additional ingestion and query capabilities are in beta: store once in R2, query from anywhere without egress penalties.

What comes next

R2 handles object storage: files, images, documents, and large blobs. Chapter 14 covers the remaining storage primitives: KV for caching and configuration, and Hyperdrive for accelerating connections to external PostgreSQL and MySQL databases.

Together with D1 from Chapter 12, these options complete the data layer. The pattern is consistent: Cloudflare provides primitives with specific strengths, and architectural judgment lies in matching workload characteristics to primitive capabilities.