---
title: R2-compatible object storage
description: "Object storage bound to env.NAME, with the R2 binding API, multipart uploads and direct transfer URLs."
---

Object storage with the R2 binding API. Each object's bytes live in their own file on disk.

## Configuration

```jsonc
"r2_buckets": ["MEDIA"]
```

Bare names are auto-provisioned on deploy; see [Storage resources](/docs/storage).

## Usage

```js
env.MEDIA.put("path/file.txt", body, { httpMetadata: { contentType: "text/plain" } });
const obj = env.MEDIA.get("path/file.txt");   // null when missing
return obj.toResponse();                      // serves the bytes unchanged
env.MEDIA.head("path/file.txt");              // metadata only
env.MEDIA.delete("path/file.txt");
const { objects, truncated, cursor } = env.MEDIA.list({ prefix: "path/", limit: 100 });
```

From compatibility date `2026-10-26` R2 calls return Promises, as on Workers; before it they are synchronous. `await` works at both.

:::warning
Serve objects with `obj.toResponse()`. `new Response(obj.body)` encodes the body as text a second time and corrupts anything that is not ASCII.
:::

## Methods

| Method | Signature | Returns |
| --- | --- | --- |
| `put` | `(key, body, { httpMetadata?, customMetadata? })` | `void` |
| `get` | `(key)` | an object, or `null` |
| `head` | `(key)` | the object's metadata without the body, or `null` |
| `delete` | `(key)` | `void` |
| `list` | `({ prefix?, limit?, cursor? })` | `{ objects, truncated, cursor }`. `limit` defaults to and is capped at 1000. |
| `createMultipartUpload` | `(key, { httpMetadata?, customMetadata? })` | a multipart upload |
| `resumeMultipartUpload` | `(key, uploadId)` | a multipart upload |
| `createUploadUrl` | `(key, { maxBytes?, expiresIn?, sha256?, httpMetadata?, customMetadata? })` | `{ url, expiresAt }` |
| `createDownloadUrl` | `(key, { expiresIn? })` | `{ url, expiresAt }` |

An object has `key`, `size`, `etag`, `httpEtag` (quoted, for an `ETag` header), `uploaded`, `httpMetadata` and `customMetadata`. From `get`, it also has `body`, `text()`, `json()` and `toResponse(init?)`. `toResponse` sets `content-type` from `httpMetadata` and `etag` from `httpEtag` unless `init.headers` sets them.

## Large files

`put` and `get` hold the whole object in memory, and a request body is capped at 1 MiB by default. For anything larger, use one of these:

**Direct transfer URLs.** `createUploadUrl` and `createDownloadUrl` return a short-lived URL on your own hostname. The client sends the file straight to it with `PUT`, or fetches it with `GET` or `HEAD` (byte ranges supported), and the bytes never pass through your handler.

```js
fetch(request) {
  const { url } = env.MEDIA.createUploadUrl("uploads/video.mp4", { maxBytes: 500 * 1024 * 1024 });
  return Response.json({ uploadTo: url });
}
```

- Each URL works once and expires after `expiresIn` seconds: 900 by default, between 30 and 3600.
- An upload is capped at `maxBytes`: 100 MiB by default, 5 GiB at most.
- Pass `sha256` (lowercase hex) to have the upload rejected when its contents do not match.

**Multipart uploads.** Send a large object in parts, each within the request body limit:

```js
const upload = env.MEDIA.createMultipartUpload("big.bin");
const part = upload.uploadPart(1, chunk);           // part numbers 1 to 10000
upload.complete([part]);                            // or upload.abort()
```

Store `upload.key` and `upload.uploadId` to continue later with `resumeMultipartUpload`. Unfinished uploads are deleted after seven days.

## Related

- [Platform limits](/docs/platform/limits)
