---
title: R2 (object storage)
description: "Object storage bound to env.NAME. Bytes live in their own file, never inline in a database row."
---

## Configuration

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

## Usage

```js
env.MEDIA.put("path/file.txt", body, { httpMetadata: { contentType: "text/plain" } });
const obj = env.MEDIA.get("path/file.txt");   // obj.body, obj.size, obj.etag, obj.uploaded  (or null)
obj.toResponse();                             // serves obj.body byte for byte -- use this for
                                               // binary content, not new Response(obj.body), which
                                               // re-encodes it as text and corrupts anything non-ASCII
env.MEDIA.head("path/file.txt");
env.MEDIA.delete("path/file.txt");
const { objects, truncated, cursor } = env.MEDIA.list({ prefix: "path/", limit: 100 });
```

## Methods

| Method | Signature | Returns |
| --- | --- | --- |
| `put` | `(key, body, { httpMetadata? })` | — |
| `get` | `(key)` | an object with `.body`, `.size`, `.etag`, `.uploaded`, and `.toResponse()` — or `null` |
| `head` | `(key)` | metadata without the body |
| `delete` | `(key)` | — |
| `list` | `({ prefix?, limit?, cursor? })` | `{ objects, truncated, cursor }` |

:::warning
Use `obj.toResponse()` for binary content, not `new Response(obj.body)` — the latter re-encodes the body as text and corrupts anything non-ASCII.
:::

An object's bytes live in their own file (`r2-blobs/<hash-of-bucket-and-key>.blob` next to the bucket's metadata), never inline in a database row or the request frame — one write, one read, no extra copy on top. Binary objects round-trip byte for byte on both paths.

## Memory and size limits

:::danger
**Keep objects small.** An R2 object is held whole in memory at every step (put, get, and everything in between), so resident memory settles at a multiple of the largest object a handler touches — and stays there for the life of the process, idle or not.
:::

Two limits keep this bounded by default:

- Inbound request bodies over 1 MiB (`SB_REQUEST_BODY_MAX`) are refused, so an upload cannot exceed that unless an operator raises it.
- Outbound `fetch()` refuses a response body over 32 MiB (`SB_FETCH_MAX_BYTES`).

True streaming put/get needs Porffor's own `ReadableStream` support, which it does not have yet ([porffor#349](https://github.com/CanadaHonk/porffor/issues/349)).

## Related

- [Platform limits](/platform/limits)
