- R2 の特徴 — エグレス無料・S3 互換 API・強い整合性
- バケットの作成と wrangler r2 コマンドによるオブジェクト操作
- Worker バインディング経由の put / get / delete / list の実装
- multipart アップロードとプリサインド URL の概要と使いどころ
- パブリックバケット(r2.dev / カスタムドメイン)と料金モデル
R2 とは — エグレス無料の S3 互換ストレージ
R2 は Cloudflare のオブジェクトストレージです。画像・動画・バックアップ・機械学習のモデルファイルといった非構造化データを「オブジェクト」として「バケット」に格納します。前章の KV が小さな値の高速読み取りに特化していたのに対し、R2 は大きなバイナリの保存と配信が主戦場です。
最大の特徴はエグレス(インターネットへのデータ転送)が無料であることです。従来のクラウドストレージはデータの取り出しに従量課金があり、配信量が伸びるほど転送料が膨らみます。R2 ではこれがゼロで、課金対象はストレージ容量と操作回数が中心です(低頻度アクセス向けの Infrequent Access ストレージクラスのみ取り出し料金が加わります。詳細は後述の料金モデル)。
もう 1 つの柱が S3 互換 API です。Amazon S3 の API と互換性があるため、aws-sdk・boto3・rclone といった既存の S3 ツールが、エンドポイントと認証情報の差し替えだけでそのまま動きます。既存アプリからの移行や、S3 エコシステムの資産を活かした運用がしやすい設計です。
整合性のモデルも押さえておきましょう。R2 の書き込みと削除は強い整合性を持ちます。put() の Promise が解決した時点で、以降の読み取りは世界中どこからでも新しい値を返します。KV の結果整合性(伝播に時間がかかる)との大きな違いです。
R2 には 3 つの API サーフェスがあります。
| API | アクセス方法 | 主な用途 |
|---|---|---|
| Workers API | バケットを Worker にバインドし env 経由で操作 | Worker 内での読み書き。この章の主役 |
| S3 互換 API | https://<ACCOUNT_ID>.r2.cloudflarestorage.com | 既存の S3 SDK・ツール(aws-sdk、rclone 等)からの利用、プリサインド URL |
| Cloudflare REST API | api.cloudflare.com | ダッシュボードや wrangler が使う管理系 API。5 分あたり 1,200 リクエストのレート制限あり |
主要な制限(2026-08 時点)は次のとおりです。
| 項目 | 上限 |
|---|---|
| バケットあたりの容量・オブジェクト数 | 無制限 |
| オブジェクトサイズ | 5 TiB(正確には 4.995 TiB) |
| 1 回のアップロード(単一 PUT / multipart の 1 パート) | 5 GiB(正確には 4.995 GiB) |
| multipart のパート数 | 10,000 |
| オブジェクトキー長 | 1,024 バイト |
| オブジェクトメタデータ | 8,192 バイト |
| 同一キーへの同時書き込み | 毎秒 1 回(超過は 429 応答) |
バケット作成と wrangler r2 コマンド
バケットはダッシュボードでも wrangler でも作成できます。バケット名には次の制約があります。
- 使える文字は小文字英字(a-z)・数字(0-9)・ハイフン(-)のみ
- ハイフンで始める・終わることはできない
- 長さは 3〜63 文字
wrangler の R2 サブコマンドでバケットとオブジェクトの基本操作が一通りできます。
| コマンド | 説明 |
|---|---|
r2 bucket create [NAME] | バケット作成。--location で配置ヒント、--storage-class で既定ストレージクラスを指定可能 |
r2 bucket list | アカウント内のバケット一覧 |
r2 bucket info [BUCKET] | バケットの情報を表示 |
r2 bucket delete [BUCKET] | バケット削除(先に中身を空にする必要がある) |
r2 object put [BUCKET/KEY] | オブジェクト作成。--file でファイル指定、--content-type 等でメタデータ指定 |
r2 object get [BUCKET/KEY] | オブジェクト取得。--file で保存先指定、--pipe で標準出力へ |
r2 object delete [BUCKET/KEY] | オブジェクト削除 |
r2 bucket dev-url enable [BUCKET] | r2.dev 公開 URL の有効化 |
r2 bucket domain add [BUCKET] | カスタムドメインの接続(--domain と --zone-id が必須) |
r2 bucket cors set [BUCKET] | CORS ポリシーの設定 |
オブジェクトパスは {バケット名}/{キー} の形式です。r2 object 系コマンドには --local / --remote フラグがあり、ローカル開発用ストレージと本番バケットのどちらを操作するかを明示できます。本物のバケットを操作したいときは --remote を付けると確実です。
wrangler の R2 サポートは基本操作向けです。大量ファイルの一括アップロードや他ストレージからのミラーリングには、rclone などの S3 互換ツールが公式に推奨されています。
Worker バインディングで put / get / delete / list
バインディングの定義
Worker から R2 を使うには、KV と同じくバインディングを wrangler.jsonc に定義します。binding が Worker コード内での変数名、bucket_name が実際のバケット名です。
{
"r2_buckets": [
{
"binding": "MY_BUCKET",
"bucket_name": "<YOUR_BUCKET_NAME>"
}
]
}
<YOUR_BUCKET_NAME> は自分のバケット名(このあとのハンズオンでは textbook-uploads)に置き換えます。これで env.MY_BUCKET からバケットを操作できます。
基本メソッド
| メソッド | 動作 |
|---|---|
put(key, value, options?) | オブジェクトを保存し、メタデータの R2Object を返す。onlyIf の事前条件が満たされないと null を返し保存しない |
get(key, options?) | 本文つきの R2ObjectBody を返す。キーが存在しなければ null |
head(key) | 本文なしのメタデータのみ取得。存在しなければ null |
delete(key) | 削除。キーの配列を渡せば 1 回で最大 1,000 件まで削除できる |
list(options?) | キーの辞書順で最大 1,000 件を列挙。prefix / limit / cursor / delimiter を指定可能 |
createMultipartUpload(key) | multipart アップロードを開始し R2MultipartUpload を返す |
resumeMultipartUpload(key, uploadId) | 進行中の multipart アップロードの操作オブジェクトを得る |
実装時に効いてくる仕様をまとめます。
get()が返すR2ObjectBodyはbody(ReadableStream)のほかarrayBuffer()/text()/json()/blob()を持ちます。大きなファイルはbodyをそのまま Response に流すのがメモリ効率の面で有利ですput()のhttpMetadataオプションにリクエストのHeadersを渡すと、Content-Type などの HTTP メタデータがオブジェクトと一緒に保存されます。配信時はwriteHttpMetadata(headers)で復元でき、httpEtagを etag ヘッダーとして返すのが推奨ですlist()の続きの有無は件数ではなくtruncatedプロパティで判定し、cursorを次の呼び出しに渡してページングしますonlyIf(etag 一致・アップロード日時など)をget()/put()に渡すと条件付き操作ができます
multipart アップロード
1 回のアップロード上限(約 5 GiB)を超えるファイルや、失敗時の再開・並列転送が必要なケースでは multipart アップロードを使います。createMultipartUpload() で開始し、uploadPart(partNumber, value) でパートを送り、各パートの etag と番号を集めて complete(parts) で確定する流れです。パートは最後を除き 5 MiB 以上かつ同一サイズで、最大 10,000 パート・合計約 5 TiB まで扱えます。未完了の multipart アップロードは 7 日後に自動で中断されます。単一 PUT との使い分けの目安は「100 MB を超えるあたりから multipart」です。なお S3 互換 SDK や rclone は閾値を超えると自動的に multipart へ切り替えるため、S3 API 経由なら自前実装は通常不要です。
complete() が成功して初めて 1 つのオブジェクトになります。途中のパートは個別にやり直せる一方、完了しないまま放置されたアップロードは 7 日後に自動で中断されます。プリサインド URL
プリサインド URL は、S3 API の署名をクエリパラメータとして URL に埋め込み、API キーを渡さずに特定オブジェクトへの一時アクセスを許可する仕組みです。ブラウザから R2 へ直接アップロードさせたい(Worker のリクエストサイズ上限を回避したい)場合や、期限付きダウンロードリンクの配布に向きます。有効期限は 1 秒〜7 日(604,800 秒)で、GET / HEAD / PUT / DELETE に対応します(HTML フォームの POST は非対応)。生成には R2 API トークン(Access Key ID / Secret Access Key)が必要で、R2 との通信なしにクライアント側だけで生成できます。
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const S3 = new S3Client({
region: "auto", // SDK 上必須だが R2 では使われない
endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: "<ACCESS_KEY_ID>",
secretAccessKey: "<SECRET_ACCESS_KEY>",
},
});
// 1 時間有効なアップロード用 URL(Content-Type も署名に含めて制限)
const putUrl = await getSignedUrl(
S3,
new PutObjectCommand({
Bucket: "textbook-uploads",
Key: "image.png",
ContentType: "image/png",
}),
{ expiresIn: 3600 },
);
URL を持つ人は誰でも操作できるベアラートークン相当のものなので、必要最小限の有効期限で発行し、共有先を限定してください。
公開方法と料金モデル
パブリックバケット
バケットはデフォルトで非公開です。コンテンツを直接インターネットへ公開するには 2 つの方法があります。
- r2.dev サブドメイン — Cloudflare 管理の開発用 URL。ワンコマンドで有効化できる反面、レート制限(毎秒数百リクエスト規模で 429 応答)と帯域制限があり、本番用途は対象外です
- カスタムドメイン — 同じアカウントにゾーンとして追加済みのドメインをバケットに接続します。Cloudflare のキャッシュ・WAF・Access・Bot 対策をバケットの前段に適用でき、本番公開はこちらが前提です
# 開発用 r2.dev URL を有効化
npx wrangler r2 bucket dev-url enable textbook-uploads
# カスタムドメインを接続(ゾーンは同一アカウントにあること)
npx wrangler r2 bucket domain add textbook-uploads \
--domain files.example.com --zone-id <ZONE_ID>
<ZONE_ID> はダッシュボードの対象ゾーン概要ページで確認できるゾーン ID に置き換えます。なお、公開バケットではドメイン直下でのオブジェクト一覧表示はできません。また WAF や Access で保護する場合は、抜け道にならないよう r2.dev 側の公開を無効化しておく必要があります。
料金モデル
R2 の課金は「ストレージ容量」と「操作回数」の 2 軸です。操作は状態を変更する Class A(PutObject、ListObjects、CopyObject、multipart 系など)と、読み取り中心の Class B(GetObject、HeadObject など)に分かれ、Class A の方が高単価です。DeleteObject・DeleteBucket・AbortMultipartUpload は無料です(2026-08 時点)。
| 項目 | Standard | Infrequent Access | 無料枠(Standard のみ) |
|---|---|---|---|
| ストレージ | $0.015 / GB-月 | $0.01 / GB-月 | 10 GB-月 / 月 |
| Class A 操作 | $4.50 / 100 万件 | $9.00 / 100 万件 | 100 万件 / 月 |
| Class B 操作 | $0.36 / 100 万件 | $0.90 / 100 万件 | 1,000 万件 / 月 |
| データ取り出し | なし | $0.01 / GB | — |
| エグレス | 無料 | 無料 | 無料 |
ストレージは GB-月(1 日ごとのピーク使用量を 30 日で平均した値)で計測され、使用量は次の課金単位に切り上げられます。低頻度アクセス向けの Infrequent Access ストレージクラスは保存単価が下がる代わりに操作単価と取り出し料金が上乗せされ、30 日の最低保存期間があります。認可されなかったリクエスト(401)には操作課金が発生しません。
この章で使うバケットを作成し、wrangler だけでオブジェクトの出し入れを体験します。所要時間は 10 分程度です。
事前準備: R2 を初めて使う場合、ダッシュボードの Storage & databases > R2 > Overview でチェックアウトフローを完了し、R2 サブスクリプションを有効化する必要があります。無料枠(前節参照)の範囲内であれば料金はかかりません。
- バケットを作成し、一覧で確認する。
npx wrangler r2 bucket create textbook-uploads npx wrangler r2 bucket listlistの出力にtextbook-uploadsが含まれていれば成功です。 - テキストファイルをアップロードし、取得してみる。
--remoteで本番バケットを明示的に指定する。echo "hello r2" > hello.txt npx wrangler r2 object put textbook-uploads/hello.txt --file hello.txt --remote npx wrangler r2 object get textbook-uploads/hello.txt --pipe --remote最後のコマンドが
hello r2を出力すれば、書き込みと読み取りの両方が確認できています。 - オブジェクトを削除する。
npx wrangler r2 object delete textbook-uploads/hello.txt --remoteもう一度
getするとオブジェクトが存在しない旨のエラーになります。バケット自体は次のハンズオンで使うため残しておきます。
PUT でアップロード、GET で配信、DELETE で削除、ルートパスで一覧という 4 機能を持つファイルアップローダー Worker を作ります。所要時間は 20 分程度です。
- 新しいプロジェクトを作成する。質問には「Hello World example」「Worker only」「TypeScript」、デプロイは「No」を選ぶ。
npm create cloudflare@latest -- r2-uploader cd r2-uploader - wrangler.jsonc に R2 バインディングを追記する。
{ // ...既存の設定はそのまま残す "r2_buckets": [ { "binding": "MY_BUCKET", "bucket_name": "textbook-uploads" } ] } src/index.tsを次の内容に置き換える。interface Env { MY_BUCKET: R2Bucket; } export default { async fetch(request, env): Promise<Response> { const url = new URL(request.url); const key = url.pathname.slice(1); switch (request.method) { case "PUT": { const object = await env.MY_BUCKET.put(key, request.body, { httpMetadata: request.headers, // Content-Type などを保存 }); return Response.json({ key: object.key, size: object.size }); } case "GET": { if (key === "") { // ルートパスは一覧を返す const listed = await env.MY_BUCKET.list(); return Response.json( listed.objects.map((o) => ({ key: o.key, size: o.size })), ); } const object = await env.MY_BUCKET.get(key); if (object === null) { return new Response("Object Not Found", { status: 404 }); } const headers = new Headers(); object.writeHttpMetadata(headers); // 保存時のメタデータを復元 headers.set("etag", object.httpEtag); return new Response(object.body, { headers }); } case "DELETE": { await env.MY_BUCKET.delete(key); return new Response("Deleted!"); } default: return new Response("Method Not Allowed", { status: 405, headers: { Allow: "PUT, GET, DELETE" }, }); } }, } satisfies ExportedHandler<Env>;npx wrangler devで起動し、別のターミナルから動作確認する。アップロード時は必ず--data-binaryを使う。curl -X PUT http://localhost:8787/hello.txt --data-binary "hello r2" #=> {"key":"hello.txt","size":8} curl http://localhost:8787/hello.txt #=> hello r2 # 手元の画像を Content-Type つきでアップロード curl -X PUT http://localhost:8787/cat.png \ -H "Content-Type: image/png" --data-binary @cat.png curl http://localhost:8787/ #=> [{"key":"cat.png","size":...},{"key":"hello.txt","size":8}]
ブラウザで http://localhost:8787/cat.png を開くと、保存した Content-Type が writeHttpMetadata で復元されるため画像として表示されます。なお wrangler dev は既定でローカルストレージに読み書きするため、ここでのオブジェクトは本番バケットには入っていません(つまずきポイント参照)。
このままデプロイすると誰でもアップロード・削除ができてしまいます。書き込み系操作を事前共有キーで保護してから本番へデプロイします。所要時間は 15 分程度です。
src/index.tsの先頭に認可ロジックを追加し、Envにシークレットの型を足す。interface Env { MY_BUCKET: R2Bucket; AUTH_KEY_SECRET: string; } const hasValidHeader = (request: Request, env: Env) => request.headers.get("X-Custom-Auth-Key") === env.AUTH_KEY_SECRET; function authorizeRequest(request: Request, env: Env): boolean { switch (request.method) { case "PUT": case "DELETE": return hasValidHeader(request, env); // 書き込みは秘密ヘッダー必須 default: return true; // GET(配信・一覧)は公開のまま } } // fetch() の先頭に次のガードを追加する // if (!authorizeRequest(request, env)) { // return new Response("Forbidden", { status: 403 }); // }- シークレットを登録し、デプロイする。ローカル開発時のシークレットには第 3 章で扱った
.dev.varsを使う。npx wrangler secret put AUTH_KEY_SECRET # プロンプトに秘密の文字列を入力 #=> Success! Uploaded secret AUTH_KEY_SECRET. npx wrangler deploy - デプロイ先 URL で認可の動作を確認する。
# ヘッダーなし → 拒否される curl -X PUT https://r2-uploader.<YOUR_SUBDOMAIN>.workers.dev/cat.png \ --data-binary @cat.png #=> Forbidden # 正しいヘッダーつき → 成功 curl -X PUT https://r2-uploader.<YOUR_SUBDOMAIN>.workers.dev/cat.png \ -H "X-Custom-Auth-Key: <YOUR_SECRET>" --data-binary @cat.png #=> {"key":"cat.png","size":...} # 配信は認証なしで通る curl -o downloaded.png https://r2-uploader.<YOUR_SUBDOMAIN>.workers.dev/cat.png<YOUR_SUBDOMAIN>は自分の workers.dev サブドメイン、<YOUR_SECRET>は手順 2 で登録した文字列に置き換えます。今回はデプロイ後なので、オブジェクトは本番のtextbook-uploadsバケットに書き込まれ、ダッシュボードからも確認できます。
つまずきポイント
wrangler dev でアップロードしたファイルが本番バケットにない — wrangler dev は既定でローカル開発モードで動き、R2 操作はマシン上のローカルストレージに対して実行されます。ダッシュボードに出ないのは仕様です。開発中に本物のバケットへつなぎたい場合は、バインディング設定に "remote": true を付けます。wrangler r2 object コマンドも --remote / --local で対象を明示できます。
curl でアップロードしたファイルが壊れる — curl の -d(--data)はテキスト送信用で、バイナリが途中で切り詰められます。R2 へのファイル送信では必ず --data-binary を使ってください。
r2.dev URL で 429 Too Many Requests が返る — r2.dev サブドメインは開発用で、毎秒数百リクエスト規模のレート制限とスループット制限がかかります。本番トラフィックはカスタムドメインを接続して配信してください。カスタムドメインならキャッシュや WAF も併用できます。
プリサインド URL がカスタムドメインで使えない — プリサインド URL は S3 API のエンドポイント(<ACCOUNT_ID>.r2.cloudflarestorage.com)専用です。カスタムドメイン経由のアクセスに認証を付けたい場合は、WAF の HMAC 検証機能(Pro プラン以上)を使います。
理解度チェック
Q1. R2 のエグレス(インターネットへのデータ転送)にはいくら課金される?
無料です。どのストレージクラスでもエグレス課金はありません。課金対象はストレージ容量(GB-月)と Class A / Class B 操作の回数(Infrequent Access では加えてデータ取り出し料金)です。
Q2. Class A 操作と Class B 操作の違いは?
Class A は PutObject や ListObjects など状態を変更・列挙する操作で単価が高く、Class B は GetObject や HeadObject など読み取り中心の操作で単価が安く設定されています。なお DeleteObject などの削除系は無料です。
Q3. 1 回の PUT でアップロードできる上限と、それを超えるファイルの扱いは?
単一アップロードの上限は約 5 GiB(正確には 4.995 GiB)です。それを超えるファイルは multipart アップロードで分割して送り、最大 10,000 パート・合計約 5 TiB まで扱えます。
Q4. プリサインド URL の最長有効期限と、使えるエンドポイントは?
最長 7 日間(604,800 秒)です。S3 API エンドポイント(<ACCOUNT_ID>.r2.cloudflarestorage.com)でのみ有効で、カスタムドメインでは使えません。
Q5. list() の結果に続きがあるかどうかは何で判定する?
truncated プロパティで判定します。true なら cursor を次の list() 呼び出しに渡して続きを取得します。返却件数と limit の比較で判定すると、メタデータ量の調整で件数が減った場合に誤動作します。
まとめと次章へ
- R2 はエグレス無料・S3 互換のオブジェクトストレージで、書き込み・削除は強い整合性を持つ
- バケットとオブジェクトの基本操作は
wrangler r2 bucket/wrangler r2 objectで完結する - Worker からは
r2_bucketsバインディング経由でput/get/head/delete/listを使う。httpMetadataとwriteHttpMetadataで Content-Type を保って配信できる - 約 5 GiB を超えるファイルは multipart、クライアント直接アクセスにはプリサインド URL を使い分ける
- 公開は開発用の r2.dev と本番用のカスタムドメインの 2 択。料金はストレージ + Class A/B 操作で、無料枠は 10 GB-月・Class A 100 万件・Class B 1,000 万件(2026-08 時点)
ファイルのような非構造化データの置き場所は手に入りました。次章では構造化データの番です。SQLite ベースのサーバーレス SQL データベース D1 で、リレーショナルな API を組み立てます。