R2 Sippy (Incremental Migration)
Sippy is Cloudflare's incremental migration service that helps you migrate data from other cloud storage providers to R2 without upfront egress fees. When enabled, objects not found in R2 are fetched from the source bucket, returned to the client, and copied to R2 for future requests.
Overview
public class MigrationService(ICloudflareApiClient cf)
{
public async Task EnableMigrationAsync(string bucket, string sourceBucket, string region)
{
var request = new EnableSippyFromAwsRequest(
SippyAwsSource.Create(
sourceBucket,
region,
Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID")!,
Environment.GetEnvironmentVariable("AWS_SECRET_ACCESS_KEY")!
)
);
await cf.Accounts.Buckets.EnableSippyAsync(bucket, request);
}
}
Note
Jurisdictional Buckets: If your bucket was created with a jurisdiction (e.g., R2Jurisdiction.EuropeanUnion), you must pass the jurisdiction parameter to all Sippy operations. See Working with Jurisdictional Buckets.
await cf.Accounts.Buckets.EnableSippyAsync("my-eu-bucket", request, R2Jurisdiction.EuropeanUnion);
Supported Sources
Sippy supports migration from:
| Provider | Source Class |
|---|---|
| Amazon S3 | SippyAwsSource |
| Google Cloud Storage | SippyGcsSource |
Getting Sippy Status
Check if Sippy is enabled and view the configuration:
var config = await cf.Accounts.Buckets.GetSippyAsync("my-bucket");
Console.WriteLine($"Sippy Enabled: {config.Enabled}");
if (config.Source is not null)
{
Console.WriteLine($"Source Provider: {config.Source.Provider}");
Console.WriteLine($"Source Bucket: {config.Source.Bucket}");
Console.WriteLine($"Source URL: {config.Source.BucketUrl}");
}
if (config.Destination is not null)
{
Console.WriteLine($"Destination: {config.Destination.Bucket}");
}
Enabling Migration from AWS S3
Configure Sippy to migrate data from an Amazon S3 bucket:
var awsSource = SippyAwsSource.Create(
bucket: "my-source-bucket",
region: "us-east-1",
accessKeyId: "AKIAIOSFODNN7EXAMPLE",
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
);
var request = new EnableSippyFromAwsRequest(awsSource);
var result = await cf.Accounts.Buckets.EnableSippyAsync("my-r2-bucket", request);
Console.WriteLine($"Migration enabled: {result.Enabled}");
AWS IAM Policy
The source credentials need at minimum these permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-source-bucket",
"arn:aws:s3:::my-source-bucket/*"
]
}
]
}
Enabling Migration from Google Cloud Storage
Configure Sippy to migrate data from a GCS bucket:
var gcsSource = SippyGcsSource.Create(
bucket: "my-gcs-bucket",
clientEmail: "[email protected]",
privateKey: "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
);
var request = new EnableSippyFromGcsRequest(gcsSource);
var result = await cf.Accounts.Buckets.EnableSippyAsync("my-r2-bucket", request);
GCS Service Account Permissions
The service account needs:
storage.objects.get- Read objectsstorage.objects.list- List bucket contents
Disabling Sippy
Disable incremental migration when migration is complete:
await cf.Accounts.Buckets.DisableSippyAsync("my-bucket");
Note
Disabling Sippy does not delete any objects already copied to R2. Objects already in R2 remain accessible.
Models Reference
SippyConfig
| Property | Type | Description |
|---|---|---|
Enabled |
bool |
Whether Sippy is enabled |
Source |
SippySourceInfo? |
Source bucket information |
Destination |
SippyDestination? |
Destination R2 bucket information |
SippySourceInfo
| Property | Type | Description |
|---|---|---|
Provider |
SippyProvider? |
Source provider (aws, gcs) |
Bucket |
string? |
Source bucket name |
BucketUrl |
string? |
Full URL of source bucket |
Region |
string? |
Source region (for AWS) |
SippyAwsSource
| Property | Type | Description |
|---|---|---|
Provider |
SippyProvider |
Always Aws |
Bucket |
string |
AWS S3 bucket name |
Region |
string? |
AWS region (e.g., "us-east-1") |
AccessKeyId |
string? |
AWS access key ID |
SecretAccessKey |
string? |
AWS secret access key |
SippyGcsSource
| Property | Type | Description |
|---|---|---|
Provider |
SippyProvider |
Always Gcs |
Bucket |
string |
GCS bucket name |
ClientEmail |
string? |
Service account email |
PrivateKey |
string? |
Service account private key |
SippyProvider
| Value | Description |
|---|---|
Aws |
Amazon Web Services S3 |
Gcs |
Google Cloud Storage |
R2 |
Cloudflare R2 (destination only) |
Common Patterns
Migration Workflow
public async Task MigrateFromAwsAsync(
string r2Bucket,
string awsBucket,
string awsRegion,
string awsAccessKeyId,
string awsSecretKey)
{
// 1. Enable Sippy
var source = SippyAwsSource.Create(awsBucket, awsRegion, awsAccessKeyId, awsSecretKey);
await cf.Accounts.Buckets.EnableSippyAsync(r2Bucket, new EnableSippyFromAwsRequest(source));
Console.WriteLine($"Sippy enabled. Objects will be migrated on-demand.");
Console.WriteLine("Update your application to point to R2.");
Console.WriteLine("Once migration is complete, disable Sippy.");
}
Check Migration Status
public async Task<bool> IsMigrationActiveAsync(string bucket)
{
var config = await cf.Accounts.Buckets.GetSippyAsync(bucket);
return config.Enabled;
}
Environment-Based Configuration
public async Task ConfigureMigrationAsync(string r2Bucket)
{
var provider = Environment.GetEnvironmentVariable("MIGRATION_SOURCE_PROVIDER");
EnableSippyRequest request = provider switch
{
"aws" => new EnableSippyFromAwsRequest(SippyAwsSource.Create(
Environment.GetEnvironmentVariable("AWS_SOURCE_BUCKET")!,
Environment.GetEnvironmentVariable("AWS_REGION")!,
Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID")!,
Environment.GetEnvironmentVariable("AWS_SECRET_ACCESS_KEY")!
)),
"gcs" => new EnableSippyFromGcsRequest(SippyGcsSource.Create(
Environment.GetEnvironmentVariable("GCS_SOURCE_BUCKET")!,
Environment.GetEnvironmentVariable("GCS_CLIENT_EMAIL")!,
Environment.GetEnvironmentVariable("GCS_PRIVATE_KEY")!
)),
_ => throw new InvalidOperationException($"Unknown provider: {provider}")
};
await cf.Accounts.Buckets.EnableSippyAsync(r2Bucket, request);
}
Required Permissions
| Permission | Scope | Level |
|---|---|---|
| Workers R2 Storage | Account | Write |
How Sippy Works
- Request arrives: Client requests an object from R2
- Check R2: If object exists in R2, return it immediately
- Fetch from source: If not in R2, fetch from the source bucket
- Return and copy: Return object to client while copying to R2
- Future requests: Object now served directly from R2
Client Request
│
▼
┌─────────────┐
│ R2 │
│ Bucket │
└─────────────┘
│
Object exists?
/ \
Yes No
│ │
│ ▼
│ ┌─────────────┐
│ │ Source │──── Fetch object
│ │ Bucket │
│ └─────────────┘
│ │
│ ▼
│ Copy to R2
│ │
└────┬────┘
│
▼
Return to Client
Important Notes
- Egress costs: You pay source provider egress fees only for objects not yet in R2
- Latency: First request for uncached objects has higher latency (fetching from source)
- Credentials: Source credentials are stored securely but never returned in API responses
- Consistency: Objects are copied as-is; metadata is preserved
- Overwrite: Objects already in R2 are not overwritten by source objects
Migration Best Practices
- Pre-warm frequently accessed objects by requesting them before cutover
- Monitor R2 usage metrics to track migration progress
- Validate critical objects after migration
- Disable Sippy only after confirming all necessary data is in R2
- Update application endpoints gradually during migration
Related
- Bucket Management - Create and manage buckets
- R2 Object Storage - Upload and download objects