ADR-001: Storage split — Cloudflare R2 (CDN media) vs Wasabi S3 (backups & archives)
Status: Accepted
Date: 2026-06-27
Deciders: Radek Konarski
Context: BrandPilot CDN architecture (issues #56–#59 in p24-infra/brandpilot)
Decision
Use two separate storage tiers based on access pattern:
| Tier | Storage | Use case |
|---|---|---|
| CDN / hot media | Cloudflare R2 | Files that are opened/served to end users |
| Backups / cold archives | Wasabi S3 | Files stored long-term, rarely or never accessed |
Context
BrandPilot generates and serves media assets (hero photos, avatars, icons, template previews) to potentially thousands of client landing pages worldwide. We also maintain backups for Supabase and MongoDB replica sets. These two workloads have opposite access patterns and different cost drivers.
Option comparison
Cloudflare R2
What it is: S3-compatible object storage by Cloudflare. Objects in a public R2 bucket are automatically served from Cloudflare’s global CDN — 330+ Points of Presence (PoPs) worldwide.
What is a PoP? A PoP is a physical Cloudflare data center in a specific city (Warsaw, Frankfurt, London, New York, Singapore, São Paulo, etc.). When a user anywhere in the world requests a file, Cloudflare routes them to the nearest PoP. The file is fetched from origin once, then cached at that city’s PoP — every subsequent user in that region gets it locally (5–15ms) instead of from a European server (150–250ms). A single file effectively “lives” in 330 cities simultaneously.
| Metric | Value |
|---|---|
| Storage | $0.015/GB/month |
| Egress to internet | $0 always |
| Read operations | $0.36/million |
| Write operations | $4.50/million |
| Free tier | 10 GB storage + 1M writes + 10M reads/month |
| CDN layer | Built-in — no separate setup |
Wasabi S3
What it is: S3-compatible object storage optimised for cold/warm data. No per-operation charges. Egress is free as long as monthly egress ≤ monthly stored volume (the 1:1 ratio policy). No built-in CDN.
| Metric | Value |
|---|---|
| Storage | **0.007/GB/month** (6.99/TB) |
| Egress to internet | 0 if egress ≤ stored volume; 0.01/GB above ratio |
| Operations | $0 — no per-request charges |
| Minimum object duration | 90 days (charged even if deleted earlier) |
| CDN layer | None |
Cost comparison by workload
Workload A — CDN media (BrandPilot: photos, avatars, previews)
Assumption: 1,000 brands × 5 assets × 1.5 MB = 7.5 GB stored. Monthly traffic: 50,000 page views × 800 KB media/page = 40 GB egress.
| Wasabi | Cloudflare R2 | |
|---|---|---|
| Storage | $0.05 | $0.11 |
| Egress 40 GB | $0.40 (exceeds 7.5 GB stored ratio) | $0 |
| CDN setup | Extra work (Caddy proxy or separate CDN) | Included |
| Total | ~$0.45 + CDN overhead | ~$0.11 |
At 10,000 brands / 1M page views / month: Wasabi ~8/month vs R2 ~1–6/month. Gap grows with traffic. R2 wins for CDN media.
Workload B — Backups & archives (Supabase, MongoDB rs0, long-term)
Assumption: 2 TB stored. Monthly restore/access: ~10 GB (rare — only on incident).
| Wasabi | Cloudflare R2 | |
|---|---|---|
| Storage 2 TB | $14/month | $30/month |
| Egress 10 GB | $0 (10 GB << 2 TB stored) | $0 |
| Operations | $0 | ~$0 (few ops on cold data) |
| Total | ~$14/month | ~$30/month |
Wasabi is ~2× cheaper for cold storage. Egress is irrelevant at low access ratios. No CDN needed for backups. Wasabi wins for archives.
Decision rationale
- R2’s zero-egress advantage only matters when files are served frequently to end users. For backups that are accessed <1× per incident, egress cost is negligible on Wasabi.
- Wasabi’s storage rate (
0.007/GB) is 2× cheaper than R2 (0.015/GB) — this compounds at multi-TB backup scales. - R2’s built-in CDN (330+ PoPs) eliminates additional infrastructure for media delivery. Adding a CDN layer to Wasabi would cost extra and add operational complexity.
- Both are S3-compatible — the same SDK (
@aws-sdk/client-s3) works with both, just different endpoint URLs and credentials.
Scope of this decision
Cloudflare R2 — use for:
- BrandPilot media: hero backgrounds, section photos, HeyGen-generated avatars, template icons, preview screenshots
- Any file that is served to end users via a public URL (landing pages, brand kits, portfolio assets)
- Files requiring global low-latency delivery
Wasabi S3 — use for:
- Supabase database backups — daily encrypted dumps, retained 30 days
- MongoDB rs0 backups — replica set dumps from bms-2/bms-3, retained 30 days
- Thanos long-term metrics — Prometheus block uploads from vps-i1, retained 1 year
- Application log archives — compressed log exports, retained 90 days
- PDF report archives — generated inspection reports from
report-scheduler, retained indefinitely - Any file stored for compliance/recovery purposes with rare or no routine access
Existing Wasabi bucket — no migration
p24-infra bucket on Wasabi (eu-central-2) remains as-is for all existing monitoring, backup, and Thanos workloads. No migration needed.
Implementation
R2 (new — BrandPilot)
- Bucket:
p24-brandpilot-media - Public URL:
media.brandpilot.io(CNAME to R2 public endpoint) - Credentials:
CLOUDFLARE_R2_ACCOUNT_ID,CLOUDFLARE_R2_ACCESS_KEY_ID,CLOUDFLARE_R2_SECRET_ACCESS_KEYinsecrets/brandpilot.env.sops - SDK:
@aws-sdk/client-s3withendpoint: https://{account_id}.r2.cloudflarestorage.com - See:
p24-infra/brandpilotissue #59
Wasabi (existing — unchanged)
- Bucket:
p24-infra(eu-central-2) - Credentials:
P24_INFRA_WASABI_ACCESS_KEY/P24_INFRA_WASABI_SECRET_ACCESS_KEYinsecrets/monitoring.env.sops - Covers: Thanos, backups, PDF archives, log exports
Consequences
- Positive: Lower cost for both workloads; media delivery is globally fast without extra infrastructure; backups stay cheap at multi-TB scale.
- Negative: Two storage providers to manage, two credential sets to rotate.
- Neutral: Both are S3-compatible — code reuse is high; only endpoint URL and credentials differ.
- Future trigger to revisit: If Cloudflare R2 drops storage pricing below Wasabi, consolidate everything to R2. Currently R2 storage is 2× more expensive.