Cut GCS Egress Costs: Migrate to Cloudflare R2

Photo by Unsplash

Photo by Unsplash
Google Cloud Storage egress fees are a silent budget killer. Every gigabyte of data your application reads from GCS and delivers to users costs money — $0.11/GB for the first 10 TB per month. At scale, this becomes one of the largest line items in your cloud bill. Cloudflare R2 solves this problem entirely: zero egress fees, S3-compatible API, and global distribution through Cloudflare's network. This guide walks through the real cost comparison, migration tooling, and application code changes needed to make the switch.
Cloud storage pricing is deceptively simple on the surface — GCS Standard storage is $0.020/GB/month. But the egress costs hit hard: $0.11/GB for the first 10 TB exiting to the internet, $0.08/GB for the next 40 TB. A media platform storing 5 TB and serving 1 TB of video per month pays $20 for storage and $110 for egress — a 5.5x multiplier. Cloudflare R2 charges $0.015/GB for storage and literally $0 for egress, making the economics compelling for any read-heavy workload.
The numbers speak clearly when you lay them side by side. At 100 GB egress per month, GCS costs $31/month versus R2 at $15/month — a 52% saving. At 1 TB egress, GCS costs $131/month versus R2 at $15/month — an 89% saving. The higher your egress volume, the more dramatic the savings. The break-even point where R2's storage price difference ($0.005/GB more expensive) is offset by egress savings is at roughly 3 GB of egress per 1 GB stored.
# Cost comparison — 1 TB data stored, 100 GB downloaded per month
# Google Cloud Storage (Standard)
Storage: 1,000 GB × $0.020/GB = $20.00/month
Egress: 100 GB × $0.110/GB = $11.00/month ← the sting
Total: $31.00/month
# Cloudflare R2
Storage: 1,000 GB × $0.015/GB = $15.00/month
Egress: 100 GB × $0.000/GB = $0.00/month ← zero egress
Total: $15.00/month
# Annual savings: ($31 - $15) × 12 = $192/year (at 100 GB egress)
# At 1 TB egress/month: GCS costs $110/month in egress alone vs $0 on R2rclone is the industry-standard tool for cloud storage migrations. It understands dozens of storage backends including GCS and Cloudflare R2 (via the S3-compatible API), can sync in parallel with multiple transfer threads, and supports dry-run mode for safe testing. The migration is a two-step process: first sync all data from GCS to R2, then update your application configuration to point at R2, then run a final sync to catch any objects written during the cutover window.
Configure rclone with both a GCS remote (using your service account JSON) and an R2 remote (using R2 API tokens from the Cloudflare dashboard). Always run with --dry-run first to verify the object count and total size matches your expectations. The --transfers 32 flag parallelizes the upload across 32 concurrent threads — adjust based on your network bandwidth. Large migrations (>100 GB) are best run from a VM in the same region as your GCS bucket to minimize transfer costs during migration.
#!/usr/bin/env bash
# migrate-gcs-to-r2.sh — Migrate a GCS bucket to Cloudflare R2
# Prerequisites: gsutil, rclone configured with r2 remote
GCS_BUCKET="gs://my-gcs-bucket"
R2_REMOTE="r2"
R2_BUCKET="my-r2-bucket"
# Step 1: Create R2 bucket via Wrangler
wrangler r2 bucket create "$R2_BUCKET"
# Step 2: Configure rclone R2 remote (~/.config/rclone/rclone.conf)
# [r2]
# type = s3
# provider = Cloudflare
# access_key_id = <R2_ACCESS_KEY>
# secret_access_key = <R2_SECRET_KEY>
# endpoint = https://<account_id>.r2.cloudflarestorage.com
# Step 3: Sync GCS → R2 (dry run first)
rclone sync "$GCS_BUCKET" "$R2_REMOTE:$R2_BUCKET" --transfers 32 --checkers 16 --dry-run --progress --log-file migration.log
# Remove --dry-run to perform actual migration
echo "Migration complete. Verify with:"
echo "rclone ls $R2_REMOTE:$R2_BUCKET | wc -l"During migration, enable GCS Object Change Notifications or Pub/Sub to capture any new objects written during the migration window. Write them to both GCS and R2 simultaneously (dual-write pattern) to ensure zero data loss during cutover. Only remove GCS after verifying R2 has all objects via rclone check --size-only.
Because R2 is S3-compatible, migrating your application code is often just changing the endpoint URL and credentials. If you're using the AWS SDK (@aws-sdk/client-s3), you only need to update the S3Client constructor — all the GetObject, PutObject, DeleteObject, and ListObjects calls remain identical. If you're using the GCS client library directly, you'll need to swap to the AWS SDK or use an S3-compatible library like MinIO client.
The AWS SDK for JavaScript, Python, Go, and other languages all support custom endpoints — just point them at your R2 endpoint (https://{account_id}.r2.cloudflarestorage.com) with your R2 API token credentials. The region should be set to 'auto' as R2 automatically routes to the nearest Cloudflare datacenter. For public assets, enable R2's public bucket feature to serve files directly from a custom domain through Cloudflare's CDN at zero egress cost.
// app-migration.ts — Update S3-compatible SDK endpoint to R2
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
// Before (GCS with S3 interoperability)
const gcsClient = new S3Client({
region: "auto",
endpoint: "https://storage.googleapis.com",
credentials: { accessKeyId: GCS_KEY, secretAccessKey: GCS_SECRET },
})
// After (Cloudflare R2 — drop-in replacement)
const r2Client = new S3Client({
region: "auto",
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: { accessKeyId: R2_ACCESS_KEY, secretAccessKey: R2_SECRET_KEY },
})
// Usage is identical — just swap the client
const object = await r2Client.send(
new GetObjectCommand({ Bucket: "my-r2-bucket", Key: "my-file.jpg" })
)Cloudflare R2 is S3-compatible, not GCS-compatible. Features specific to GCS — like Object Lifecycle Management (equivalent to S3 Lifecycle), Uniform Bucket-Level Access, and IAM Conditions — need to be replicated using R2 equivalents or Cloudflare Workers. Audit your GCS configuration before migrating to ensure all features you rely on have R2 equivalents.
Understanding the key concepts behind object storage helps you make the most of the migration: egress fees, Cloudflare R2, rclone, S3-compatible API, and R2 Public Bucket.
After migrating to R2, explore these features to further reduce costs and improve performance: R2 Event Notifications trigger Cloudflare Workers on object changes — useful for image processing pipelines. R2 Object Lifecycles automatically expire or transition objects to reduce storage costs. Cloudflare's Cache API can cache R2 objects at edge for the hottest content, reducing even the R2 operation count. Combined with Workers for on-the-fly image resizing, R2 can replace a CDN + object storage + image processing stack at a fraction of the cost.
Sources & Further Reading