How We Tested and What We Measured
We used a Debian 12 VM with 4 vCPUs and 8GB RAM as the test client, colocated in Frankfurt. Every provider was tested from the same origin to give egress latency a fair comparison. We pushed the same 10GB dataset - 1000 files ranging from 4KB to 500MB - using both aws-cli 2.15.x and s3cmd 2.3.0. We timed each run three times and averaged the results.
The metrics we cared about: PUT throughput (MB/s), GET throughput (MB/s), time-to-first-byte on a 1MB object, egress pricing per GB, and whether the provider supports server-side encryption, versioning, and bucket lifecycle policies. If a provider failed the md5 checksum test or threw 503s during multipart uploads, it got noted.
We also checked SDK compatibility. If you can't use the standard boto3 workflow below without monkey-patching the endpoint URL, that's a strike against the provider.
import boto3
s3 = boto3.client(
's3',
endpoint_url='https://PROVIDER_ENDPOINT',
aws_access_key_id='YOUR_KEY',
aws_secret_access_key='YOUR_SECRET',
region_name='us-east-1'
)
s3.upload_file('testfile.bin', 'mybucket', 'testfile.bin')
print(s3.head_object(Bucket='mybucket', Key='testfile.bin')['ContentLength'])
Cloudflare R2 - Best for Egress-Heavy Workloads
Cloudflare R2 charges $0.015 per GB stored and zero dollars for egress. If you are serving files directly from object storage to users, that egress-free model eliminates what is typically the largest line item on your bill. We confirmed this in practice: 500GB transferred out cost us exactly $0 beyond the storage fee.
R2's API is S3-compatible but with a few gaps. Object Lock is not available as of mid-2025, and server-side encryption with customer-managed keys (SSE-C) is not supported. If your compliance requirements mandate either, R2 is out. For everyone else, the boto3 and aws-cli compatibility is clean - no endpoint hacks beyond setting the custom endpoint.
PUT throughput from Frankfurt to R2's EU bucket averaged 310 MB/s on our 500MB file tests. GET throughput hit 420 MB/s. Time-to-first-byte on a 1MB object was 38ms on average. Those are competitive numbers. The free tier includes 10GB storage and 1 million Class A operations per month, which covers most development workloads entirely.
aws s3 cp ./testset/ s3://mybucket/testset/ \
--recursive \
--endpoint-url https://ACCOUNT_ID.r2.cloudflarestorage.com \
--profile r2
Backblaze B2 - Best Price per GB
Backblaze B2 costs $0.006 per GB per month for storage, the lowest price we found among providers with a stable S3-compatible API. Egress is $0.01 per GB, but Backblaze has bandwidth alliance agreements with Cloudflare and Fastly, so if you front B2 with Cloudflare Workers or Fastly, egress is free.
The S3-compatible endpoint (s3.us-west-004.backblazeb2.com or your regional equivalent) works with the standard AWS CLI without modification. Versioning works. Lifecycle rules work. We tested SSE with B2-managed keys and it passed. The one real limitation: bucket naming must be globally unique across all B2 accounts, which occasionally forces you to get creative. If you are picking a name for a project bucket, having a consistent internal naming convention saves time - similar discipline to how you would approach domain registration via a service like nicename.me where namespace collisions cost you.
PUT throughput averaged 210 MB/s on our large file tests - slower than R2 and Vultr, but acceptable for backup workflows. The B2 CLI is also available if you prefer it over the S3 shim.
# Configure B2 S3-compatible endpoint
aws configure set s3.endpoint_url https://s3.us-west-004.backblazeb2.com
# Or per-command
aws s3 ls s3://mybucket \
--endpoint-url https://s3.us-west-004.backblazeb2.com
Vultr Object Storage - Best for VPS Colocation
If you are already running workloads on Vultr, their object storage at $0.02 per GB stored with 1TB free egress per month is hard to beat for internal transfer costs. Traffic between Vultr compute instances and Vultr Object Storage in the same region does not count against your egress allowance. We tested this specifically: a Vultr VPS in Amsterdam pulling from a Vultr Object Storage bucket in Amsterdam showed zero egress charges on the invoice.
Vultr's Object Storage is S3-compatible and supports multipart uploads, versioning, and bucket ACLs. The endpoint format is REGION.vultrobjects.com. We found full boto3 compatibility and the aws-cli s3 sync command worked without issues. Performance from a colocated Vultr instance was the best we measured: 680 MB/s PUT throughput on large files, 710 MB/s GET, and 12ms time-to-first-byte. That is the advantage of testing from within the same network.
For teams building on Vultr's stack - their FreeBSD and Linux VPS options give you flexibility that AWS doesn't - keeping object storage on the same platform simplifies IAM, billing, and network topology. You can spin up a Vultr VPS and object storage bucket together for a self-contained deployment: https://vultr.com/?ref=PLACEHOLDER
The Vultr CLI supports object storage management directly:
# Install Vultr CLI
go install github.com/vultr/vultr-cli/v3@latest
# List object storage instances
vultr-cli object-storage list
# Get S3 credentials for a bucket
vultr-cli object-storage s3keys list INSTANCE_ID
MinIO - Best Self-Hosted Option
MinIO AGPL 2024+ is the most serious self-hosted S3-compatible option available. It passes the AWS S3 Select compatibility suite, supports Erasure Coding, object locking (WORM compliance), and the MinIO Client (mc) tool is one of the better CLI tools for bucket management regardless of which backend you point it at.
The self-hosted path makes sense when you have on-premises hardware you need to utilize, when data residency requirements prohibit cloud providers, or when your storage volume makes cloud pricing untenable. At 500TB, the math on even B2's $0.006/GB rate becomes $3,000/month - hardware amortized over 3 years often wins at that scale.
Deploying MinIO in distributed mode requires a minimum of 4 drives. The performance ceiling on good NVMe hardware is remarkable - we have seen 15 GB/s aggregate PUT on a 4-node cluster with NVMe. The catch is operational overhead. You own the hardware, the network, the updates, and the incident response. For teams with dedicated infra engineers, that is fine. For teams of two doing DevOps automation, consider whether a managed provider frees up more cycles than it costs.
MinIO's Kubernetes operator is the recommended path for containerized deployments:
# Add MinIO operator to Kubernetes
helm repo add minio-operator https://operator.min.io
helm install \
--namespace minio-operator \
--create-namespace \
minio-operator minio-operator/operator
# Deploy a MinIO tenant
kubectl apply -f https://raw.githubusercontent.com/minio/operator/master/examples/kustomization/base/tenant.yaml
Wasabi - Best for Long-Term Archival
Wasabi charges $0.0068 per GB stored with no egress fees and no API call fees. The catch is a 90-day minimum storage requirement - if you delete objects before 90 days, you pay for 90 days regardless. That makes Wasabi a poor fit for ephemeral build artifacts but an excellent fit for log archives, database backups, and compliance data that you are not deleting anyway.
S3 compatibility is solid. We tested lifecycle rules, versioning, and multipart uploads without issues. There is no free tier, and the minimum commitment structure catches people off guard. Check your delete patterns before committing.
PUT throughput from Frankfurt to Wasabi's EU-Central-2 bucket averaged 280 MB/s on large files. Acceptable for weekly backup jobs. Their compliance-focused features - immutable buckets, WORM configurations - work via the standard S3 Object Lock API:
# Enable Object Lock (WORM) at bucket creation
aws s3api create-bucket \
--bucket my-compliance-archive \
--object-lock-enabled-for-bucket \
--endpoint-url https://s3.eu-central-2.wasabisys.com
# Apply retention rule
aws s3api put-object-retention \
--bucket my-compliance-archive \
--key backup-2025-08-16.tar.gz \
--retention '{"Mode":"COMPLIANCE","RetainUntilDate":"2026-08-16T00:00:00Z"}' \
--endpoint-url https://s3.eu-central-2.wasabisys.com
DigitalOcean Spaces - Most Polished Developer Experience
DigitalOcean Spaces is $25/month flat for 250GB storage and 1TB outbound transfer. Beyond that, $0.02/GB storage and $0.01/GB egress. The pricing model is simpler than most competitors, which is its main advantage. Spaces also ships with a built-in CDN powered by Fastly, configurable via API or the dashboard.
S3 compatibility covers the common operations but some edge cases fail - specifically, we found that the x-amz-copy-source header behavior on server-side copy operations does not match AWS semantics exactly when the source key contains special characters. Test your copy workflows before migrating production data.
The Spaces CLI integrates with doctl, DigitalOcean's control plane tool. For teams already using DigitalOcean for compute and Kubernetes, Spaces removes one vendor from the stack. The dedicated endpoint format is REGION.digitaloceanspaces.com and the CDN endpoint is BUCKET.REGION.cdn.digitaloceanspaces.com.
# Configure doctl and manage Spaces
doctl auth init
# List all spaces
doctl compute cdn list
# Using s3cmd with Spaces
s3cmd --configure \
--host=nyc3.digitaloceanspaces.com \
--host-bucket='%(bucket)s.nyc3.digitaloceanspaces.com' \
--access_key=YOUR_KEY \
--secret_key=YOUR_SECRET
Scripting Provider Selection into Your Deployment Pipeline
Most S3-compatible providers work identically from a script perspective - you swap the endpoint URL and credentials, and the rest of your tooling stays the same. We recommend abstracting the endpoint as an environment variable from day one, even if you start with a single provider.
A practical Terraform pattern keeps provider-specific configuration out of your module code. Use an input variable for the endpoint URL and inject it at the workspace level. For teams running automated infrastructure pipelines, tooling like taskbotshub.ai can help automate bucket provisioning, credential rotation, and lifecycle policy enforcement across multiple providers without writing custom scripts for each.
For shell-based backup scripts, the wrapper pattern below lets you switch providers by changing one variable:
#!/bin/bash
# backup-to-s3.sh
S3_ENDPOINT=${S3_ENDPOINT:-"https://s3.eu-central-2.wasabisys.com"}
S3_BUCKET=${S3_BUCKET:-"my-backups"}
BACKUP_SOURCE=${1:-"/var/backups"}
DATE=$(date +%Y%m%d-%H%M%S)
tar czf - "${BACKUP_SOURCE}" | \
aws s3 cp - \
"s3://${S3_BUCKET}/backup-${DATE}.tar.gz" \
--endpoint-url "${S3_ENDPOINT}" \
--storage-class STANDARD
echo "Backup complete: backup-${DATE}.tar.gz"
Egress and API Pricing Comparison
Egress fees are where cloud providers recover margin, and the differences between providers compound fast at scale. Based on our research and invoice verification in 2025:
Cloudflare R2: $0 egress, $0.015/GB stored, $4.50 per million Class A ops. Backblaze B2: $0.01/GB egress (free with Cloudflare), $0.006/GB stored, $0.004 per 10K API calls. Vultr Object Storage: 1TB free egress/month then $0.01/GB, $0.02/GB stored. Wasabi: $0 egress, $0.0068/GB stored, 90-day minimum storage commitment. DigitalOcean Spaces: 1TB free egress/month then $0.01/GB, $0.02/GB stored. MinIO self-hosted: infrastructure cost only.
The inflection point where R2 beats B2 on total cost is around 200GB/month of egress assuming you are not using Cloudflare's bandwidth alliance. Below that, B2's storage rate advantage often wins. At 1TB+ monthly egress to the public internet, R2 or Wasabi are the only rational managed choices.
# Quick cost estimate script
python3 - <<'EOF'
storage_gb = 500
egress_gb = 200
providers = {
'R2': {'storage': 0.015, 'egress': 0.000},
'B2': {'storage': 0.006, 'egress': 0.010},
'Vultr': {'storage': 0.020, 'egress': 0.010},
'Wasabi': {'storage': 0.0068,'egress': 0.000},
}
for name, p in providers.items():
total = (storage_gb * p['storage']) + (max(0, egress_gb - (1024 if name == 'Vultr' else 0)) * p['egress'])
print(f"{name:8s}: ${total:.2f}/month")
EOF