Read, write, and stream objects

Stream large bodies and use conditions

Pass object streams without buffering and use entity tags or conditional requests to avoid accidental overwrite and wasted transfer.

8 minute lesson

~~~

Workers have bounded memory, around 128 MB per isolate. A 2 GB video cannot fit in that. The way you survive large files is to never hold them.

object.body is a ReadableStream. Hand it straight to the Response and the bytes flow from R2 to the client without accumulating in the Worker:

const object = await env.FILES.get('videos/launch-demo.mp4')
if (!object) return new Response('Not found', { status: 404 })

const headers = new Headers()
object.writeHttpMetadata(headers)
headers.set('etag', object.httpEtag)

return new Response(object.body, { headers })

The same applies on the way in: pass request.body to put instead of reading unknown files into one array buffer. The moment you write await request.arrayBuffer() for uploads of unknown size, you’ve set a memory trap that fires on the first big file.

Forwarding the entity tag matters, and here is why.

Conditional reads save transfer

When a client sends back the etag it already has, you can answer 304 and skip the body entirely:

const object = await env.FILES.get(key, {
  onlyIf: request.headers,
})

if (object && !('body' in object)) {
  return new Response(null, { status: 304 })
}

onlyIf accepts the request headers directly, so If-None-Match works with the etag you sent earlier. When the precondition fails, R2 returns the object metadata without a body, and the 'body' in object check detects that case.

Conditional writes prevent lost updates

Two processes read an object, both modify it, both write it back. The second write silently destroys the first. Conditional writes close that gap:

const updated = await env.FILES.put(key, newBody, {
  onlyIf: { etagMatches: previousEtag },
})

if (!updated) {
  return new Response('Object changed, reload and retry', { status: 412 })
}

The put only succeeds if the object still has the version you read. A null return means someone wrote in between, and your workflow decides what to do about it.

To verify the streaming claim, upload a file larger than your comfortable in-memory test size and watch the Worker serve it. If memory usage stays flat, you’re streaming. If the request dies with an out-of-memory error, something in your code buffers.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →