Overview

R2 is Cloudflare’s S3-compatible object store with zero egress fees. The API mirrors S3 so existing SDKs work after swapping the endpoint, and the pricing replaces bandwidth charges with per-operation costs. Pick R2 over S3 when egress is the dominant cost; stay on S3 when the object store has to live inside the AWS data plane next to other services.

Pay for storage and operations, not for egress

S3 charges roughly 0.00. The trade-off is a higher per-operation cost.

  • Storage: 0.023 per GB-month (S3 Standard).
  • Class A operations (writes, lists): $4.50 per million on R2.
  • Class B operations (reads): $0.36 per million on R2.
  • Egress: free on R2; 0.09 per GB on S3 depending on tier.

The break-even is roughly 1 GB of egress per object per month. Asset-heavy sites, video, and backups served back to users save the most; write-once, read-rarely archives can be cheaper on S3 Glacier.

Use the S3 API with the AWS SDK

R2 speaks S3 over a region-less endpoint. Swap the endpoint and use the same SDK code.

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"
 
const r2 = new S3Client({
  region: "auto",
  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
  credentials: { accessKeyId, secretAccessKey },
})
 
await r2.send(
  new PutObjectCommand({
    Bucket: "site-assets",
    Key: "hero.webp",
    Body: file,
    ContentType: "image/webp",
  })
)

Inside a Worker, the R2 binding skips the HTTP roundtrip and exposes a typed API. Use the binding inside Workers and the S3 SDK from outside (Node scripts, GitHub Actions, mobile apps).

Multipart upload anything over 100 MB

The single PutObject request caps at 5 GB and times out around 5 minutes. Use multipart for anything larger or unreliable.

  • The SDK’s Upload helper (from @aws-sdk/lib-storage) handles parts, concurrency, and resume.
  • Pick a part size of 8 MB to 64 MB. Smaller parts means more requests; larger parts means more memory.
  • Abort incomplete uploads with a lifecycle rule (next section). Stranded parts cost real money.
import { Upload } from "@aws-sdk/lib-storage"
 
const upload = new Upload({
  client: r2,
  params: { Bucket: "backups", Key: "db-2026-05-14.sql.zst", Body: stream },
  partSize: 16 * 1024 * 1024,
  queueSize: 4,
})
await upload.done()

Set lifecycle rules to expire old objects

Lifecycle rules run server-side and delete objects on a schedule. Configure them per-bucket through the dashboard or API.

  • Expire incomplete multipart uploads after 1 day. The only reason to keep them longer is debugging a stuck client.
  • Delete temporary files (image previews, signed-URL targets) after 7 to 30 days.
  • For backups, set a tiered policy: keep daily for 14 days, weekly for 90 days, monthly for one year. Pair with an external retention script for anything more nuanced.

R2 does not yet have all of S3’s storage classes; the trade is simpler pricing without Glacier-style cold tiers.

Issue presigned URLs for direct browser uploads

Presigned URLs let a client upload directly to R2 without proxying through the origin. Generate the URL server-side, hand it to the browser, and the browser PUTs the file.

import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import { PutObjectCommand } from "@aws-sdk/client-s3"
 
const url = await getSignedUrl(
  r2,
  new PutObjectCommand({ Bucket: "user-uploads", Key: `${userId}/${filename}` }),
  { expiresIn: 300 } // seconds
)

Constrain the URL with ContentType and a short TTL. Set bucket-level CORS to allow only the origin that needs uploads. For downloads, presigned GETs with a 5-minute TTL replace your need to proxy bytes through a Worker.

Pick R2 for these workloads

R2 fits a clear set of use cases.

  • Static asset hosting fronted by cloudflare: images, video, fonts, downloads. Zero egress matters here.
  • Database backups uploaded from cron, retained per lifecycle policy.
  • User-generated uploads (avatars, attachments) collected via presigned URLs.
  • Build artifacts and CI outputs that survive between jobs.
  • Public datasets shipped at internet scale.

Pair R2 with cloudflare-workers for image transformations and access control. Skip R2 for workloads that need filesystem semantics (rename a file in place), strong consistency on overwrite, or AWS-specific features like Object Lambda.