---
title: R2
description: Store and serve objects in a Cloudflare R2 bucket with Head, Get, Put, Delete, and List.
---

[Cloudflare R2](https://developers.cloudflare.com/r2/) is S3-compatible object storage. The `cloudflare/r2` package wraps a bound bucket.

## Declare the binding

```toml
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"
preview_bucket_name = "my-bucket-dev"
```

## Connect to a bucket

```go
bucket, err := r2.NewBucket("BUCKET")
if err != nil {
	// no binding named BUCKET
}
```

## Get an object

`Get` returns an `*r2.Object` whose `Body` (an `io.ReadCloser`) streams the contents. It returns `nil` (with no error) when the key does not exist. `Head` returns the metadata only — its `Body` is always `nil`.

```go
obj, err := bucket.Get("images/logo.png")
if err != nil {
	return err
}
if obj == nil {
	// not found
}
defer obj.Body.Close()
io.Copy(w, obj.Body)
```

An `*r2.Object` exposes `Key`, `Version`, `Size`, `ETag`, `HTTPETag`, `Uploaded`, `HTTPMetadata`, `CustomMetadata`, and `Body`.

## Put an object

`Put` stores an object from an `io.ReadCloser`. The body is copied into memory before upload.

```go
_, err = bucket.Put("images/logo.png", req.Body, &r2.PutOptions{
	HTTPMetadata: r2.HTTPMetadata{
		ContentType:  req.Header.Get("Content-Type"),
		CacheControl: "public, max-age=14400",
	},
	CustomMetadata: map[string]string{"custom-key": "custom-value"},
})
```

`PutOptions` fields: `HTTPMetadata` (`ContentType`, `ContentLanguage`, `ContentDisposition`, `ContentEncoding`, `CacheControl`, `CacheExpiry`), `CustomMetadata`, and `MD5`.

## Delete an object

```go
err := bucket.Delete("images/logo.png")
```

## List objects

```go
objects, err := bucket.List()
for _, obj := range objects.Objects {
	fmt.Println(obj.Key, obj.Size)
}
```

:::note
List currently takes no options — prefix, limit, and cursor are not implemented yet.
:::

## Multipart upload

For large objects, use a [multipart upload](https://developers.cloudflare.com/r2/api/workers/workers-multipart-usage/) to upload parts individually instead of buffering the whole object in memory. `CreateMultipartUpload` starts one; `UploadPart` sends each part (numbered 1 to 10,000) from an `io.Reader`; `Complete` assembles the uploaded parts, in order, into the final object.

```go
upload, err := bucket.CreateMultipartUpload("videos/large.mp4", &r2.MultipartOptions{
	HTTPMetadata: r2.HTTPMetadata{ContentType: "video/mp4"},
})
if err != nil {
	return err
}

var parts []r2.UploadedPart
for i, chunk := range chunks {
	part, err := upload.UploadPart(i+1, bytes.NewReader(chunk))
	if err != nil {
		// abort on failure to avoid leaving an incomplete upload behind
		upload.Abort()
		return err
	}
	parts = append(parts, *part)
}

obj, err := upload.Complete(parts)
```

`ResumeMultipartUpload(key, uploadID)` returns a handle to an existing multipart upload without validating that it exists on the server — a mistaken key/uploadID surfaces as an error from the first `UploadPart`, `Complete`, or `Abort` call instead. `Abort` cancels an in-progress upload. `MultipartOptions` accepts the same `HTTPMetadata`, `CustomMetadata`, and additionally `StorageClass` fields as `PutOptions`.

## Example projects

- [_examples/r2-image-server](https://github.com/syumai/workers-go/tree/main/_examples/r2-image-server) — upload and download images.
- [_examples/r2-image-viewer](https://github.com/syumai/workers-go/tree/main/_examples/r2-image-viewer) — serve stored images.
