Skip to content
Files SDK
Esc
navigateopen⌘Jpreview
On this page

Cloudflare R2

Cloudflare R2 over the S3-compatible HTTP API. Auto-loads R2_* env vars or accepts an R2Bucket binding inside Workers.

Installation

The @aws-sdk/* packages are only needed for the default "aws-sdk" HTTP client. The Workers binding path, hybrid signing, and the lightweight fetch client need none of them - files-sdk alone is enough:

npm install files-sdk
pnpm add files-sdk
yarn add files-sdk
bun add files-sdk

For the default "aws-sdk" HTTP client, @aws-sdk/client-s3, @aws-sdk/s3-presigned-post, and @aws-sdk/s3-request-presigner are optional peer dependencies - install alongside the SDK so the adapter’s imports resolve at runtime.

npm install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner
pnpm add files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner
yarn add files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner
bun add files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner

Over the HTTP API with the "aws-sdk" client, upload reports true byte-level progress via onProgress when the optional @aws-sdk/lib-storage package is installed (it’s loaded only when onProgress is used). Under the Workers R2Bucket binding and the fetch client the SDK reports progress generically instead — byte-level for stream bodies, start and finish for buffered ones.

Usage

Cloudflare R2 over the S3-compatible HTTP API. Auto-loads from R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY. Inside Cloudflare Workers you can pass an R2Bucket binding directly instead.

import { Files } from "files-sdk";
import { r2 } from "files-sdk/r2";

const files = new Files({
  adapter: r2({
    bucket: "uploads",
    accountId: process.env.R2_ACCOUNT_ID!,
    // accessKeyId / secretAccessKey auto-loaded
    // from R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY
  }),
});

publicBaseUrl - optional, an r2.dev subdomain or custom domain bound to the bucket. When set, url() returns `${publicBaseUrl}/${key}` and skips signing.

Lightweight fetch client

Pass client: "fetch" to swap the @aws-sdk/* stack for a SigV4-signed fetch engine built on aws4fetch (~2.5 KB gzipped, Web Crypto only). No @aws-sdk/* packages are installed or bundled - ideal for Cloudflare Workers and other edge runtimes where the AWS SDK’s ~500 KB defeats the point of web-standard tooling.

const files = new Files({
  adapter: r2({
    bucket: "uploads",
    accountId: process.env.R2_ACCOUNT_ID!,
    client: "fetch",
  }),
});

The fetch client covers upload, download (including ranges), head, exists, delete, list (including delimiter folding), server-side copy, presigned url(), and signedUploadUrl(). Trade-offs against the default "aws-sdk" client:

  • ReadableStream bodies are buffered in memory before a single PUT (a lone PUT needs a Content-Length, and single-request uploads cap at 5 GB on R2).
  • multipart and resumable (control) uploads throw instead of engaging the S3 multipart API.
  • Bulk deletes fan out as per-key delete() calls instead of batched DeleteObjects requests.
  • Byte-level onProgress reporting falls back to the SDK’s generic reporting.

Options

R2AdapterOptions is a union of two shapes depending on whether you have a Workers R2Bucket binding available.

HTTP mode

Could not generate a type table for R2HttpOptions: Cannot read properties of undefined (reading 'ESNext')

Binding mode (inside a Worker)

Could not generate a type table for R2BindingOptions: Cannot read properties of undefined (reading 'ESNext')

Hybrid: binding + HTTP credentials

Inside a Worker, you can pass both a binding and HTTP credentials. Reads and writes go through the binding (no egress, no extra round trip); url() and signedUploadUrl() route through an S3-compatible SigV4 signer because a Worker binding has no signing primitive. Hybrid signing runs on aws4fetch (Web Crypto only), so neither the binding path nor hybrid mode ever pulls @aws-sdk/* packages into the Worker bundle.

// Inside a Cloudflare Worker. The binding handles uploads/downloads
// (intra-Worker, no egress fees). The HTTP credentials let url() and
// signedUploadUrl() sign presigned URLs the binding alone can't produce.
const files = new Files({
  adapter: r2({
    binding: env.UPLOADS,
    bucket: "uploads",
    accountId: env.R2_ACCOUNT_ID,
    accessKeyId: env.R2_ACCESS_KEY_ID,
    secretAccessKey: env.R2_SECRET_ACCESS_KEY,
  }),
});

Signed uploads and maxSize

signedUploadUrl() returns a presigned PUT URL. Unlike S3, R2 does not implement the S3 POST Object API, so it has no content-length-range policy to enforce an upload size cap at the bucket. Passing maxSize throws a Provider error rather than handing back a POST form that R2 would reject with 501 Not Implemented at upload time.

// ✅ presigned PUT — the browser uploads with fetch(url, { method: "PUT", body: file })
const upload = await files.signedUploadUrl("avatars/abc.png", {
  expiresIn: 60,
  contentType: "image/png",
});

// ❌ throws: R2 has no server-enforced size limit
await files.signedUploadUrl("avatars/abc.png", {
  expiresIn: 60,
  maxSize: 5_000_000,
});

To cap upload sizes on R2, enforce the limit at your application gateway before issuing the URL.

Compatibility

HTTP mode

Method Status Notes
upload
download
delete
list
search
head
exists
copy
url
signedUploadUrl ⚠️ PUT URL only - Cloudflare R2 doesn’t implement the S3 POST Object API, so maxSize throws (no content-length-range policy; a presigned POST would 501 at upload time). Enforce upload caps at your application gateway instead.

HTTP mode (client: "fetch")

Method Status Notes
upload ⚠️ Single PUT - ReadableStream bodies are buffered in memory first, and multipart / resumable control uploads throw. Use the "aws-sdk" client for multipart.
download
delete Bulk deletes fan out per key (no batched DeleteObjects).
list
search
head
exists
copy
url
signedUploadUrl ⚠️ PUT URL only - same maxSize limitation as above.

Binding mode

Method Status Notes
upload
download
delete
list
head
exists
copy ⚠️ Read-then-write - Workers bindings have no native copy command, so the source is fetched and re-uploaded. Not server-side atomic; concurrent writes to the source between the get and put are not detected.
url Throws unless publicBaseUrl is set on the adapter (an r2.dev subdomain or a custom domain). For a presigned URL from a Worker, switch to hybrid mode by also passing accountId + accessKeyId + secretAccessKey.
signedUploadUrl Workers bindings can’t sign uploads - the secret access key is not available to the runtime. Use hybrid mode (binding + HTTP credentials) to issue presigned upload URLs.

Hybrid mode

Method Status Notes
upload
download
delete
list
head
exists
copy ⚠️ Read-then-write - copy goes through the binding (no native copy command on Workers).
url
signedUploadUrl ⚠️ PUT URL only - signing routes through the HTTP signer. R2 doesn’t implement the S3 POST Object API, so maxSize throws (no content-length-range policy; a presigned POST would 501 at upload time). Enforce upload caps at your application gateway instead.

Was this page helpful?