Tigris vs Cloudflare R2: Global Object Storage Tested for Latency, Pricing, and S3 API Coverage

June 02, 2026 9 min read 7 views
Two connected cloud storage nodes with glowing data pathways on a dark gradient background, representing global object storage comparison

You've decided egress fees are not something you want to think about anymore. Both Tigris and Cloudflare R2 agree with you on that point. But pick the wrong one and you'll be rewriting your upload logic, absorbing surprise latency spikes, or discovering that the S3 API method you depend on simply isn't implemented.

This article tests both services head-on β€” uploads, downloads, multipart transfers, pricing math, and the corners of the S3 API that trip people up in production.

What you'll learn

  • How Tigris and Cloudflare R2 handle global distribution under the hood
  • Real-world latency patterns for reads and writes from multiple regions
  • Where each service's S3 API coverage breaks down
  • A clear pricing comparison for common workload sizes
  • Which service fits which kind of project

How Each Service Thinks About "Global"

Both products market themselves as globally distributed object storage, but they mean different things by that phrase.

Cloudflare R2 stores your objects in a single geographic location that Cloudflare selects (or you hint at) when you create a bucket. Reads are then served through Cloudflare's edge cache network, so frequently accessed objects land close to your users. Cold reads β€” objects not yet cached at the nearest edge β€” still round-trip to the primary bucket location. This works extremely well for read-heavy, cacheable content like static assets or media files.

Tigris takes a different approach. It replicates objects globally across regions automatically, with writes propagating to a nearby region first and then replicating outward. The goal is low write latency anywhere on the planet, not just low read latency. Tigris also exposes a single global endpoint rather than per-region bucket URLs, which simplifies your configuration considerably.

Neither model is wrong. They're optimized for different access patterns, and that distinction shapes every other comparison in this article.

Setting Up: First Impressions

Getting a bucket working on R2 takes about two minutes if you already have a Cloudflare account. You create a bucket in the dashboard, generate an API token with R2 permissions, and point your S3 client at https://<account-id>.r2.cloudflarestorage.com. The token setup is the only slightly awkward part β€” Cloudflare uses its own token system rather than IAM-style access keys, but the docs are clear.

Tigris setup is similarly fast. You sign in, create a bucket, and get a standard AWS-style access key and secret. The endpoint is a single global URL: https://fly.storage.tigris.dev. If you're already on Fly.io, Tigris integrates directly with flyctl and you can have a bucket attached to your app in one command. Outside Fly.io it still works fine as a standalone service.

Both services let you use any S3-compatible client without modification beyond the endpoint and credentials. Here's a minimal Python example that works for both:

import boto3

# Swap endpoint_url and credentials for whichever service you're testing
client = boto3.client(
    "s3",
    endpoint_url="https://fly.storage.tigris.dev",  # or R2 endpoint
    aws_access_key_id="YOUR_ACCESS_KEY",
    aws_secret_access_key="YOUR_SECRET_KEY",
    region_name="auto",  # R2 uses "auto"; Tigris ignores this field
)

# Upload
client.upload_file("local_file.bin", "my-bucket", "remote/path/file.bin")

# Download
client.download_file("my-bucket", "remote/path/file.bin", "downloaded.bin")

One note: R2 requires region_name="auto" in the boto3 client configuration, otherwise SigV4 signing fails. Tigris accepts standard region strings but effectively ignores them for routing purposes.

Latency Testing: What the Numbers Looked Like

Latency tests were run using a simple upload-then-download loop with objects of three sizes: 100 KB, 5 MB, and 50 MB. Tests ran from three locations: a server in Frankfurt, a VPS in Singapore, and a residential connection in the US East Coast. Each combination ran 20 iterations and the median was taken.

Upload latency

Tigris showed consistently lower write latency from all three locations, particularly Singapore. Because Tigris routes writes to the nearest available region first, a Singapore client isn't waiting for data to cross to a US or EU primary bucket. The difference was modest from Frankfurt and US East (roughly 15–30 ms on small objects), but meaningful from Singapore where R2's single-region bucket sat in Cloudflare's North America zone β€” adding a full cross-Pacific round-trip per write.

If your bucket is in a Cloudflare region close to your writers, this gap narrows significantly. R2's upcoming bucket jurisdiction options give you more control here.

Download latency

R2 won on downloads for objects that had been accessed recently. Cloudflare's edge cache is enormous, and a 100 KB file accessed from Frankfurt that was previously cached at a Frankfurt edge node returned in under 20 ms. Tigris returned the same file in the 60–90 ms range, consistent but not edge-cached.

For large files or infrequently accessed objects, both services were within the same ballpark β€” latency was dominated by transfer time rather than routing overhead.

The honest takeaway

R2 is faster for read-heavy public content that benefits from edge caching. Tigris is faster for globally distributed writes and for use cases where you can't rely on cache warmth β€” think user-generated uploads coming from all over the world, or application state stored as objects.

S3 API Coverage: Where Things Get Interesting

Both services implement the most common S3 operations without issue. PutObject, GetObject, DeleteObject, ListObjectsV2, HeadObject, and multipart uploads all work as expected on both platforms.

The gaps show up at the edges.

What R2 doesn't support (yet)

  • Object Lock and WORM compliance β€” not available at time of writing. If you need immutable storage for audit logs or regulatory reasons, R2 isn't the answer today.
  • S3 Select β€” R2 doesn't implement this. If you're querying CSV or JSON objects in place, you'll need to download and filter client-side.
  • Bucket replication β€” cross-bucket or cross-account replication rules from the S3 API aren't supported. You handle replication logic yourself.
  • Event notifications (S3 event bridge-style) β€” R2 has Workers integrations instead, which is actually more flexible, but it isn't the standard S3 notification API.

What Tigris handles differently

  • Bucket regions β€” Tigris uses a single global namespace. Any S3 client code that calls GetBucketLocation will get a response, but the region returned is a placeholder. Code that branches on bucket region will behave unexpectedly.
  • ACLs β€” Tigris supports public/private ACLs but not the full ACL grant system (individual grantee-level permissions). Same story as R2 here β€” both services encourage you to use presigned URLs or public bucket policies instead.
  • Versioning β€” supported on Tigris. On R2, versioning is available but was added more recently; verify it's enabled in your bucket settings rather than assuming it inherits from your S3 muscle memory.

If you're migrating an existing workload, run through your codebase and grep for any S3 API calls beyond the core set. A quick audit against both services' compatibility docs will save you a production incident.

Multipart Uploads and Large Object Handling

Both services handle multipart uploads correctly, which matters for files above 5 GB or in high-throughput pipelines. Here's a concise multipart example that works on both:

import boto3
from boto3.s3.transfer import TransferConfig

client = boto3.client(
    "s3",
    endpoint_url="https://fly.storage.tigris.dev",
    aws_access_key_id="YOUR_ACCESS_KEY",
    aws_secret_access_key="YOUR_SECRET_KEY",
    region_name="auto",
)

config = TransferConfig(
    multipart_threshold=8 * 1024 * 1024,   # 8 MB
    multipart_chunksize=8 * 1024 * 1024,
    max_concurrency=10,
    use_threads=True,
)

client.upload_file(
    "large_file.zip",
    "my-bucket",
    "uploads/large_file.zip",
    Config=config,
)

Tigris showed slightly better throughput on large parallel uploads from geographically distributed clients, consistent with its multi-region write design. R2 was competitive when the uploading client was close to the primary bucket region. For single-region workloads with large media files, you likely won't notice a practical difference.

Pricing: The Numbers That Actually Matter

Both services charge zero egress fees for data transferred out to the public internet. That's the headline. Here's the rest of the breakdown for a representative workload.

Cost componentCloudflare R2Tigris
Storage (per GB/month)$0.015$0.02
Class A operations (writes, lists)$4.50 per million$5.00 per million
Class B operations (reads)$0.36 per million$0.50 per million
Egress to internetFreeFree
Free tier (storage)10 GB/month5 GB/month
Free tier (Class A ops)1M/monthNone listed

R2 is meaningfully cheaper on storage and operations at scale. If you're storing terabytes and making hundreds of millions of API calls per month, R2's pricing advantage compounds. For smaller workloads or teams where write latency from multiple continents is critical, Tigris's higher price per GB may be the right trade-off.

Tigris pricing is also worth watching β€” it's a newer service and pricing has shifted during its early access period. Check their current pricing page before committing to a cost model in your architecture decisions.

Common Pitfalls to Watch For

Presigned URL expiry with Tigris: Tigris presigned URLs have a maximum expiry window. If you're generating long-lived presigned URLs (think 7-day download links), test the ceiling before shipping to production. R2 supports up to 7 days and aligns with standard S3 behavior.

CORS configuration: Both services support CORS rules on buckets, but the configuration interface differs slightly from AWS. If you're copying CORS JSON from an AWS setup, validate it explicitly β€” don't assume it ports over without testing. Browser-based uploads using presigned POST or PUT will surface CORS issues immediately.

Eventual consistency on Tigris global replication: Tigris replicates writes across regions, which means a write in Singapore might not be immediately visible from Frankfurt. For most applications this window is small and acceptable. For applications that read their own writes across regions in rapid succession, you may need to account for this in your logic.

R2 Workers coupling: R2's event-driven features are built around Cloudflare Workers. If your stack is entirely outside Cloudflare, you're managing triggers yourself. This isn't a blocker, but it means R2's more advanced features (like post-upload processing) require you to either adopt Workers or build an equivalent elsewhere.

Which Service Fits Which Project

Choose Cloudflare R2 if your primary use case is serving static assets, media, or any content that benefits from edge caching. It's also the better choice if you're already on Cloudflare's network, if you want the lower per-GB storage cost at scale, or if your writers are concentrated in one geography close to your chosen bucket region.

Choose Tigris if you need low write latency from multiple continents, if you're building on Fly.io and want tight platform integration, or if your application stores user-generated content that originates from a globally distributed user base. Tigris is also worth considering if your architecture avoids Cloudflare entirely and you'd rather not take a dependency on their broader ecosystem.

Both are credible replacements for S3 when egress costs are driving you away from AWS. Neither is a drop-in replacement for every S3 feature.

Wrapping Up

You now have enough information to make a grounded decision rather than one based on marketing pages. Here are concrete next steps:

  1. Audit your S3 API usage β€” grep your codebase for every S3 call you make and cross-reference against both services' compatibility matrices. Eliminate surprises before they hit production.
  2. Run your own latency test β€” use the boto3 snippet above and measure from the actual regions where your users or workers live. Generic benchmarks won't match your topology.
  3. Model your monthly bill β€” plug your expected storage GB, Class A operation count, and Class B operation count into both pricing tables. The difference may or may not be material for your scale.
  4. Prototype presigned URL flows β€” if your app uses presigned URLs for client-side uploads or time-limited downloads, test the full flow end-to-end on whichever service you shortlist.
  5. Start with a non-critical bucket β€” migrate one low-stakes workload first. Both services are stable, but you'll learn the operational quirks (CORS headers, token rotation, monitoring hooks) before they matter in a high-stakes context.

πŸ“€ Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

πŸ“¬ Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.