Skip to content
All posts

S3 vs Spaces vs R2: What Small Teams Actually Need

July 19, 2026·Read on Medium·

Laravel makes the switch look trivial. Your network path, egress bill and edge plan decide whether it really is.

The first time a small team adds object storage, the decision looks almost boring. You need direct uploads, backups, or private file delivery, Laravel already speaks the S3 API, DigitalOcean Spaces says it is S3-compatible, Cloudflare R2 says the same thing, Amazon S3 is the default everybody recognizes, and before long the conversation collapses into a lazy shortcut: pick the cheaper bucket, wire up the credentials and move on.

That logic holds for about a week.

Then the bucket stops being a bucket. It becomes the place your queue workers write exports to, the thing your frontend hits through a CDN, the store your private files depend on, and eventually the part of your architecture that tells you whether you optimized for simplicity or for ecosystem depth. The non-obvious part is this: small teams are usually not choosing storage first. They are choosing how much surrounding platform they want to inherit.

That is why “S3-compatible” is useful, but incomplete. It tells you the request shape may transfer. It does not tell you the operational shape will.

Laravel hides the first half of the decision

Laravel is one reason teams underestimate this choice. The framework makes object storage look pleasantly interchangeable.

The official filesystem docs say the s3 driver can work with S3-compatible services by changing credentials and the endpoint value. That is real. If your app only needs to write files, read files and hand out temporary upload URLs, the code path barely changes.

<?php

// config/filesystems.php
return [
'disks' => [
'uploads' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'endpoint' => env('AWS_ENDPOINT'),
],
],
];

That is the seductive part. It is clean. It looks like a provider choice you can defer.

For direct uploads, the abstraction still looks friendly:

<?php

use Illuminate\Support\Facades\Storage;
['url' => $url, 'headers' => $headers] = Storage::temporaryUploadUrl(
'uploads/invoices/' . $invoiceId . '.pdf',
now()->addMinutes(10)
);

If your app is built around that shape, both providers feel close enough. The SDK call works. The upload succeeds. Nobody on the team feels like they made a foundational infrastructure decision.

That feeling is exactly what gets teams later.

Laravel hides the API layer, which is helpful. It does not hide the network path, the billing model, the CDN story, the egress story, the storage-tier story, or the set of surrounding features your system will start leaning on once file storage stops being a side detail.

S3 compatibility is an API story. Provider choice becomes an operations story the minute your bucket matters to the rest of the system.

Spaces is better when you want a bucket and not a platform

DigitalOcean Spaces is attractive for a reason. The product page is simple, the pricing is simple and the mental model is simple. At the time of writing, the base plan is $5 per month and includes 250 GiB of storage plus 1 TiB of outbound transfer. It also includes a built-in CDN toggle.

That combination matters more than people admit.

A small team does not just buy storage. It buys fewer moving parts to explain on a Tuesday night when a file URL looks wrong and nobody wants to reverse-engineer three AWS service boundaries to understand why. Spaces is good at that. The product says, more or less, here is your object storage, here is the endpoint, here is the CDN option, go ship.

That is a strong default when your use case looks like one of these:

  • Serve public assets from a Laravel app where the main goal is predictable hosting and direct file serving
  • Direct browser uploads matter more than exotic storage features
  • Report exports, backups, or static file distribution where lifecycle nuance is not yet the hard part

The real appeal is not only price. It is the narrowness of the decision surface.

With Spaces, you can stay focused on a smaller set of questions:

  • Can the app read and write objects reliably?
  • Do we need direct uploads?
  • Do we want the built-in CDN?
  • Is the included transfer enough for this stage of the product?

That is a healthy list for a small team. It keeps the storage choice proportional to the problem.

There is another important detail in the DigitalOcean docs that people skim past: Spaces supports a subset of the S3 API, not the full S3 feature universe. That is not a flaw. It is the product definition. You are buying a simpler object storage surface, not a promise that every AWS-adjacent pattern will port cleanly.

If that sounds limiting, good. Limits are information.

They tell you Spaces is strongest when your object storage needs are clear and bounded. Public media, direct uploads, private files with ordinary application mediation, static content behind a CDN. That is a lot of real-world work. For many small teams, it is enough for a long time.

R2 is strongest when egress and edge delivery are the real problem

Cloudflare R2 belongs in this conversation because it changes the cost shape more aggressively than it changes the application code.

Cloudflare’s R2 docs say three things that matter immediately for a small team:

  • R2 supports an S3-compatible API, so existing SDK patterns can migrate by changing the endpoint
  • Standard pricing includes 10 GB-month free, then charges $0.015 per GB-month, 1 million Class A operations free, and 10 million Class B operations free each month
  • Egress to the Internet is free

That last line is the one that makes teams look up from the spreadsheet.

R2 is not trying to beat S3 by becoming a better version of the AWS platform. It is trying to change the economics of serving objects out to users, especially when those objects sit behind Cloudflare’s edge anyway. If your product serves downloads, public media, generated reports, or user-uploaded assets where outbound traffic grows faster than stored bytes, R2 stops being a niche option very quickly.

The useful nuance is that R2 also brings Cloudflare-specific public delivery habits with it.

Cloudflare’s public bucket docs are clear on this point:

  • r2.dev is for non-production access and is rate-limited
  • If you want cache, WAF rules, or access controls, you should put the bucket behind a custom domain

That is a very different operational story from “just use the public development URL and call it done.” R2 is strongest when you already think in terms of Cloudflare at the edge, custom domains, and delivery policy living close to the object store.

So the R2 question is not only “is it cheaper?” The better question is this: are we paying mostly for storage, or are we paying to keep sending bytes back out to the Internet?

If the second half of that sentence is what hurts, R2 becomes serious.

S3 starts making sense when the bucket stops living alone

Amazon S3 earns its extra complexity when file storage becomes entangled with the rest of your AWS topology.

This is where teams often misread the decision. They compare “store file in bucket A” versus “store file in bucket B” and conclude the providers are mostly interchangeable. In practice, the more important comparison is often “store file in an isolated object service” versus “store file inside a much larger AWS operating model.”

That distinction shows up in three places very quickly.

1. Network path

AWS documents that a gateway VPC endpoint for S3 lets resources in a VPC access S3 without an internet gateway or NAT device, and that the gateway endpoint itself has no additional charge.

That is not trivia. If your app servers, workers, or internal jobs already live inside AWS, S3 can become part of a cleaner private network path. The bucket is no longer just where files land. It becomes one more service your AWS estate can reach without stepping out through the public internet.

If your workload lives outside AWS, that advantage shrinks fast. But when your stack is already in AWS, it is a real architectural benefit. Not theoretical. Not marketing language. A real path decision that changes how your system moves bytes.

2. Storage shape over time

S3 is not one storage class. AWS offers multiple storage classes for different access patterns and cost profiles. That matters the minute your file estate splits into hot assets, cold archives and objects that should move tiers automatically instead of being paid for at the same rate forever.

Spaces is easier to reason about because it exposes less. S3 is more powerful because it exposes more.

That is the trade-off in one sentence.

If your team expects the storage layer to stay simple, Spaces benefits from refusing to become a giant menu. If your storage plan is already drifting toward lifecycle rules, tiering and long-tail retention, S3 starts paying you back for its larger surface area.

3. Feature gravity

S3 comes with management, access and data-handling features that encourage the rest of your system to grow around it. Sometimes that is exactly what you want. Sometimes it is how a simple upload decision quietly turns into a platform commitment.

This is the part small teams should say out loud before they choose.

If you are already AWS-heavy, S3 usually fits the direction of travel. If you are not, S3 can still be right, but now you should admit what you are buying: more options, more integration points and more future ways for the storage layer to become part of several other decisions.

That is not free, even when the first upload code lands in one afternoon.

The compatibility line is narrower than teams think

DigitalOcean says Spaces is S3-compatible and supports the AWS SDK. Cloudflare says R2 supports an S3-compatible API. Laravel says its s3 driver can work with S3-compatible services. All three statements are true.

The mistake is extending those truths too far.

Teams hear “compatible” and mentally upgrade it to “interchangeable.” Those are not the same word, and architecture gets expensive when you pretend they are.

Here is the more accurate version:

Compatible means your application can often keep the same object-storage request pattern.

Interchangeable would mean the surrounding operational assumptions also move cleanly.

They do not. Not automatically.

The gap usually appears in boring places first:

  • The CDN story differs
  • The bill shape differs
  • The network path differs
  • The egress behavior differs
  • The storage-lifecycle plan differs
  • The amount of platform surface your team now has to understand differs

None of that shows up in your first happy-path upload test.

This is why small teams often evaluate object storage too low in the stack diagram. They treat it like a driver detail. But the storage provider leaks upward into product cost, ops habits and security boundaries faster than most abstractions do.

Small teams rarely outgrow object storage first. They outgrow their assumptions about network path, access control and cost shape.

A decision framework that is actually useful

You do not need a giant rubric here. You need a short one that reflects how small teams really work.

Pick DigitalOcean Spaces when:

  1. Your main requirement is object storage with a clean S3-compatible API and a predictable monthly floor.
  2. The built-in CDN is a practical advantage, not a side note.
  3. Your application does not need deep AWS-native networking or a broader AWS storage strategy.
  4. You want the storage decision to stay small so the team can spend attention elsewhere.

Pick Cloudflare R2 when:

  1. Your object storage is public-serving or download-heavy enough that egress dominates the long-term bill.
  2. You already use Cloudflare for DNS, cache, WAF, or access control and want the object story to live close to that edge layer.
  3. S3-compatible application code is enough, but you do not need AWS-native storage integration.
  4. You are willing to treat custom domains as the production path instead of leaning on r2.dev.

Pick Amazon S3 when:

  1. Your app and workers already run inside AWS and private network access to storage actually matters.
  2. You expect storage classes, lifecycle behavior, or other AWS-native storage patterns to become important.
  3. Your storage layer is part of a wider AWS design, not an isolated file bucket.
  4. You are willing to pay the complexity tax now because the ecosystem advantage is real for your workload.

That last point matters. Complexity tax is still a tax, even when the thing you bought is objectively more capable.

A lot of teams choose S3 because it feels like the “serious” option. That is weak reasoning. Choose S3 when the surrounding AWS benefits are materially useful. Choose Spaces when the simpler product shape is the benefit.

Not every system needs the heavyweight answer just because the heavyweight answer exists.

What this looks like in a Laravel shop

If I were running a Laravel app for a small team, I would decide in this order.

First question: where do the bytes travel from and to?

If uploads come from browsers, files get served back publicly and the rest of the app is not deeply embedded in AWS networking, Spaces is attractive because Laravel does not care much, your team gets a narrower product, and the storage story stays explainable.

If those same uploads become public downloads at real volume, I would ask a new question before defaulting to Spaces: are we about to spend more on getting bytes out than on storing them? That is the moment R2 deserves a serious look, especially if the public delivery path already runs through Cloudflare.

Second question: will storage stay boring?

If the honest answer is yes, that pushes me further toward Spaces. Boring is a feature. A file platform that stays boring lets the team spend time on auth flows, queue reliability, bad migrations, slow queries and the fifty other things that actually compete for attention every week.

If the answer is “yes, but delivery economics are about to get weird,” that is where R2 slips in between Spaces and S3. It is still S3-compatible enough to keep the application path familiar, but it is really an edge-distribution decision wearing an object-storage label.

Third question: are we already in AWS deep enough that S3 stops being just a bucket?

That is the point where S3 can become the better choice even for a small team. If the app lives in AWS, the workers live in AWS and the file layer is starting to interact with private networking or storage-tier policy, then S3 is not overkill. It is alignment.

The wrong move is the halfway move:

  • Choosing S3 because everybody else does
  • Using almost none of the AWS-native advantages
  • Still paying the mental overhead of the larger platform

That is how teams end up with infrastructure that feels enterprise-shaped but solves a startup-sized problem.

There is a mirror-image mistake too. Some teams pick Spaces because the first month is cheaper, then slowly reintroduce complexity on top with custom workarounds, extra routing logic and ad hoc conventions because the real need was deeper platform integration all along. Cheap storage does not stay cheap if it forces awkward architecture around it.

So no, this is not a simple price comparison.

It is a shape comparison, and the phrase only sounds abstract until you are the person explaining why a “compatible” bucket quietly changed the rest of the system anyway.

Do you want your object store to remain a compact service the app talks to, or do you want it to become a native part of a broader AWS system? Small teams should answer that directly instead of hiding behind “S3-compatible” and hoping the abstraction will carry more meaning than it actually does.

My default

For a small Laravel team serving uploads, exports and ordinary media, I would start with Spaces if the system is not already AWS-centric and outbound traffic is still modest.

For a small Laravel team serving a lot of public assets, downloads, or user-generated content through Cloudflare, I would look hard at R2 before I looked at S3.

For a small Laravel team already deep in AWS, especially one that benefits from private network access and a richer long-term storage strategy, I would choose S3 early and accept the larger surface area on purpose.

That is the whole decision: not which bucket can accept a PUT, but which storage product matches the architecture you are actually building before the rest of your system, and your bandwidth bill, start building around it for you.

Found this helpful?

If this article saved you time or solved a problem, consider supporting — it helps keep the writing going.

Originally published on Medium.

View on Medium
S3 vs Spaces vs R2: What Small Teams Actually Need — Hafiq Iqmal — Hafiq Iqmal