Post

The S3 Compatibility Trap: A Cloudflare R2 + Ruby SDK Gotcha

The S3 Compatibility Trap: A Cloudflare R2 + Ruby SDK Gotcha

Why even consider R2?

There are plenty of good reasons to use AWS S3 in a Rails application, and for most teams it works perfectly well.

However, one concern that comes up frequently is cost over time, especially when it comes to egress (data transferred out of the service). If your application serves many files—images, PDFs, backups, etc.—those costs can grow quickly.

Because of that, I recently tried Cloudflare R2, an object storage service that aims to be compatible with the S3 API but does not charge egress fees.

That makes it especially attractive for:

  • personal projects
  • side projects
  • apps serving a lot of downloads
  • workloads already behind Cloudflare

Another nice part: since R2 is S3-compatible, you can use the same Ruby gem you would normally use for AWS.

1
gem "aws-sdk-s3"

In theory, switching providers should be straightforward.

But I ran into a problem.


Using Cloudflare R2 with Rails Active Storage

Since R2 exposes an S3-compatible API, you can configure it in Rails using the normal S3 service in storage.yml.

Example configuration:

1
2
3
4
5
6
7
cloudflare:
  service: S3
  access_key_id: <%= ENV["R2_ACCESS_KEY_ID"] %>
  secret_access_key: <%= ENV["R2_SECRET_ACCESS_KEY"] %>
  region: auto
  bucket: <%= ENV["R2_BUCKET"] %>
  endpoint: https://<ACCOUNT_ID>.r2.cloudflarestorage.com

Then set it in your environment configuration:

1
config.active_storage.service = :cloudflare

From Rails’ perspective, this behaves like any other S3-compatible storage provider.

However, this is where the checksum issue appears.


The Checksum Issue

Recent versions of the AWS S3 SDK introduced additional checksum validation behavior.

Instead of sending a single checksum, the SDK may include multiple checksum headers when uploading files.

For example:

  • MD5
  • CRC32

This works perfectly with AWS S3, which supports multiple checksum types.

However, Cloudflare R2 currently rejects requests containing multiple checksums, which leads to this error:

1
2
Aws::S3::Errors::InvalidRequest:
You can only specify one checksum at a time.

What’s happening under the hood

Here’s a simplified version of what the request flow looks like.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Rails App
   │
   │ Upload file
   ▼
aws-sdk-s3 (newer versions)
   │
   │ Adds multiple checksums
   │
   │  Content-MD5
   │  x-amz-checksum-crc32
   ▼
Cloudflare R2
   │
   │ ❌ Rejects request
   │
   ▼
InvalidRequest:
"You can only specify one checksum at a time"

With AWS S3, the same request would succeed because S3 supports multiple checksum algorithms.


The Simple Fix

The easiest workaround is to pin the AWS SDK to an earlier version that does not automatically add the extra checksum.

1
gem "aws-sdk-s3", "~> 1.100.0"

After doing this, uploads to R2 work normally again.

This avoids the automatic multi-checksum behavior introduced in later versions of the SDK.


Quick S3 vs R2 Comparison

FeatureAWS S3Cloudflare R2
APINativeS3-compatible
Egress feesYesNo
EcosystemVery matureGrowing
SDK compatibilityFullMostly compatible
Checksum supportMultiple algorithms supportedLimited compatibility
Typical use caseEnterprise workloadsCost-efficient storage + CDN

A Subtle Problem With “S3-Compatible”

One subtle challenge of “S3-compatible” services is that the AWS SDK continues to evolve alongside S3 itself.

When the SDK introduces new defaults—like additional checksum headers—providers that only partially implement the S3 API can suddenly become incompatible.

This doesn’t mean the provider is broken; it just means the compatibility layer hasn’t caught up with the latest behavior of the SDK.


Final Thoughts

Cloudflare R2 is a compelling alternative to S3, especially for projects that serve a lot of files and want to avoid egress costs.

Because it exposes an S3-compatible API, integrating it with Ruby applications is usually straightforward. However, since it’s not a full implementation of every S3 feature, you may run into compatibility issues like the checksum behavior described above.

The good news is that once you know the cause, the workaround is simple.

Sometimes the hardest part is just discovering why the request fails in the first place.

This post is licensed under CC BY 4.0 by the author.