
こんにちは。Osukeラーニング、運営者の「osuke」です。
Cloudflare WorkersでAPIを作ったあと、「画像や動画はどこへ保存するのか」「R2へ置いたファイルをどう公開するのか」「egress無料なら料金は0円なのか」と迷っていませんか。R2は大きなファイルを扱いやすい一方、バケットを作っただけでは公開されず、保存容量と操作回数は別々に課金されます。
結論からいうと、最初はStandardのバケットを作り、Workers BindingでPUT・GET・DELETEを確認し、公開ファイルはカスタムドメイン、利用者ごとのアップロードは短時間の署名付きURLを選ぶと整理できます。料金は「GB-month+Class A+Class B+必要なら取得料」の4項目に分ければ、egress無料という一言に振り回されません。
この記事はosuke本人のR2利用体験としては書かず、2026年8月27日時点のCloudflare公式資料と、Xで公開された8件の保存・配信・CMS・アップロード・バックアップ・費用事例を照合しました。投稿者の結果はそれぞれの環境に限られ、R2全体の保証ではありません。Workersが初めてならCloudflare Workersの始め方、ファイルと表データの違いから整理するならCloudflare D1の使い方も先に確認してください。
記事のポイント
- Standardバケットを作り、Workers Bindingで最小の読み書きを通す
- 公開ファイル・非公開ファイル・直接uploadで入口を分ける
- 容量だけでなくClass A・Class B・取得料まで見積もる
- 本番前にCORS・Cache・予算通知・誤削除対策を確認する
Cloudflare R2の使い方を準備から進める
R2の接続は「保存先を作る」「Workerへ結び付ける」「1ファイルを往復させる」「公開方法を選ぶ」の4段階です。ここでは画像を1枚保存する小さなAPIを例にし、各段階で何ができれば成功かまで示します。
R2は画像やファイルを保存するオブジェクトストレージ
Cloudflare公式のR2概要によると、R2は画像、動画、PDF、ログ、配布物などの非構造化データを、オブジェクトという単位で保存するS3互換ストレージです。フォルダのように見える文字列も実体はkeyのprefixであり、データは「bucket名+object key」で識別します。
例えばユーザー画像なら、R2にはusers/123/avatar.webpというkeyで画像本体を置き、D1にはuser_id、object_key、mime_type、公開状態を行として保存します。画像そのものをSQLの列へ詰めるより、検索する情報と大きなバイナリを分ける構成です。Cloudflare製品の役割を広く比べたい場合は初心者向けCloudflare全体像も役立ちます。
Xでも、Aniket Patidar氏はPayload CMSの保存先へR2、データベースへD1を設定し、Trorram氏はバックアップツールの保存先候補としてR2を挙げています。前者はファイルと構造化データを分ける例、後者はS3互換の保存先として扱う例です。
反対に、商品価格、投稿日時、会員状態のように条件検索や並び替えを繰り返す値は、R2だけで管理すると一覧を作りにくくなります。R2にはfile本体、D1には検索用metadataという境界を先に決め、object keyを両者の共通IDにすると、後から保存先や表示方法を変えるときも追跡できます。
- Webサイトの画像・動画・音声・PDF
- アプリから配布するインストーラーやZIP
- ユーザーがアップロードするファイル
- ログ、バックアップ、AI学習用の大きなデータ
R2バケットを作成してWorkersへBindingする
公式Workers APIの手順では、Wranglerへログインし、バケットを作成してからWorkerの設定へR2 Bindingを追加します。初回は無料枠が適用されるStandardを選び、バケット名はmy-app-assetsのように用途が分かるASCII名にします。
npx wrangler login
npx wrangler r2 bucket create my-app-assets
npx wrangler r2 bucket list
次にwrangler.jsoncへBindingを追加します。bucket_nameはCloudflare上の実体名、bindingはWorkerコードから呼ぶ変数名です。本記事ではASSETSに統一するため、コード側はenv.ASSETSとなります。
{
"r2_buckets": [
{
"binding": "ASSETS",
"bucket_name": "my-app-assets"
}
]
}
管理画面からWorkerへBindingを追加する方法もありますが、次回のdeployで再現できるよう設定ファイルにも残します。TypeScriptならnpx wrangler typesを実行し、生成された型にASSETS: R2Bucketが含まれることを確認します。Binding名の大文字小文字が違うと、バケットが存在してもコードからは未定義です。
- bucket listに
my-app-assetsが出る - 設定に
binding: ASSETSがある - コードからは
env.ASSETSと呼ぶ - 開発用と本番用は別bucket名にする
Workerからput・get・deleteを確認する
Bindingができたら、最初から認証や画像変換を足さず、1つのkeyへ保存・取得・削除するAPIを作ります。PUT /files/hello.txtはrequest bodyを保存し、GETは本文とHTTP metadataを返し、DELETEは同じkeyを削除します。
interface Env { ASSETS: R2Bucket }
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const key = url.pathname.replace(/^\/files\//, "");
if (!key) return new Response("key required", { status: 400 });
if (request.method === "PUT") {
await env.ASSETS.put(key, request.body, {
httpMetadata: { contentType: request.headers.get("content-type") || "application/octet-stream" }
});
return new Response(null, { status: 201 });
}
if (request.method === "GET") {
const object = await env.ASSETS.get(key);
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 });
}
if (request.method === "DELETE") {
await env.ASSETS.delete(key);
return new Response(null, { status: 204 });
}
return new Response("method not allowed", { status: 405 });
}
};
npx wrangler devで起動し、テキストをPUTして同じ内容をGETし、DELETE後のGETが404になるところまで確認します。R2への直接操作は公式の整合性資料で強整合とされ、書き込みや削除は完了後すぐ反映されます。ただし後述するCache経由の表示は別です。
この例は接続確認用で、誰でも書き込み・削除できるまま公開してはいけません。本番ではログイン済み利用者を確認し、keyの先頭へuser IDを付け、拡張子だけでなくMIME type、容量、ファイル内容を検査します。同じkeyへ上書きさせる場合は競合条件も決めてください。
- PUT・DELETEへ認証と権限確認を入れる
- 1ファイルの最大容量をアプリ側で制限する
- 利用者が指定したkeyをそのまま信用しない
- 許可するMIME typeと拡張子を両方確認する
署名付きURLとCORSで直接アップロードする
大きな画像や動画をブラウザ→Worker→R2と中継すると、Workerが全byteを受け取ります。利用者を認証したWorkerが短時間の署名付きURLを発行し、ブラウザからR2へ直接PUTすれば、中継処理を減らせます。公式の署名付きURL資料では、有効期限を1秒から7日まで指定でき、GET・HEAD・PUT・DELETEに対応します。
URLは権限を含むBearer tokenです。API tokenやSecret Access Keyをブラウザへ渡すのではなく、信頼できるサーバー側で「この利用者が、このkeyへ、このMIME typeで、10分以内にPUTできる」というURLだけを発行します。漏れたURLは期限まで利用できるため、有効期限を必要以上に長くしないでください。
さらに、公式CORS資料によると、ブラウザから署名付きURLを使う場合はbucketのCORS設定が必要です。URLが正しくても、AllowedOriginsに本番originがなく、methodやheaderが許可されていなければブラウザが失敗します。localhostだけで成功し、本番ドメインで失敗する典型原因です。
Xでも、Abdulbasit氏はコースコンテンツのアップロードへR2の署名付きURLを使い、endpointとerrorのテスト、型付け、storage層の分離まで実装しています。署名URLを作ったことではなく、正常系と失敗系を分けて確認している点が参考になります。
- 未ログイン利用者は署名URLを取得できない
- 許可originからPUTすると成功する
- 期限切れURLと許可外originは失敗する
- upload後にサイズ・MIME・所有者を記録できる
公開はr2.devとカスタムドメインを使い分ける
R2 bucketは初期状態で非公開です。公開方法は、開発確認用のr2.dev、本番配信用のカスタムドメイン、非公開オブジェクトを一時的に渡す署名付きURLの3つに分けます。誰にいつまで見せるかを決めてから入口を選びます。
公式のPublic buckets資料は、r2.devを非本番向けとしています。公式上限資料では可変レート制限があり、数百requests/秒を超えると429や帯域制限が起こり得ると説明しています。動作確認後の画像配信はassets.example.comのようなカスタムドメインへ移します。
カスタムドメインならCloudflare Cache、WAF、Access、Bot Managementを組み合わせられます。ただしAccessで保護したつもりでもr2.devを有効なまま残すと、開発URLから読める可能性があります。本番へ切り替えたらr2.devを無効化し、公開不要なbucketは最初からPublic accessを有効にしません。
Xでは、shin氏が自宅サーバーとR2を組み合わせた動画中継、Rishabh氏が108MBのインストーラー配信を投稿しています。いずれもR2単体の設定値は公開されていませんが、R2がWeb画像だけでなく動画や配布物の経路にも使われている例です。
- 動作確認だけ: r2.dev
- 一般公開の本番ファイル: カスタムドメイン
- 会員限定・期限付き: 署名付きURLまたはWorker経由
- 社内限定: カスタムドメイン+Cloudflare Access
Cloudflare R2の料金と運用上の注意点
接続後は、保存容量だけでなく操作回数と公開経路を運用へ含めます。R2はInternetへのegressが無料でも、すべての処理が無料になるわけではありません。アクセス数、ファイル数、保存期間から先に式を作ります。
Standard料金と無料枠を4項目で計算する
Cloudflare公式R2料金によると、Standardは保存0.015ドル/GB-month、Class Aは4.50ドル/100万requests、Class Bは0.36ドル/100万requests、Internetへのegressは無料です。毎月の無料枠は保存10GB-month、Class A 100万回、Class B 1,000万回で、Standardだけに適用されます。
| 料金項目 | Standard | 代表的な処理 |
|---|---|---|
| 保存 | 0.015ドル/GB-month | 画像・動画・PDFを保持 |
| Class A | 4.50ドル/100万回 | PUT・LIST・multipart upload |
| Class B | 0.36ドル/100万回 | GET・HEAD |
| egress | 無料 | Internetへデータ送信 |
例えば平均2MBの画像を1,000枚なら約2GBです。月内に1,000回PUTし、10万回GETする程度なら、他のR2利用がなければ各無料枠内です。一方、平均100KBのassetを10万個保存し、毎日1,000万回読まれる公式例では、保存は無料枠内でもClass Bが月104.40ドルになります。容量が小さいから0円とは限りません。
Xでは、Boring Stackが月次R2費用0.12ドル、Venelin K.氏が400超のサイト・月間約4,500万requestsの構成でR2約1ドルと投稿しています。どちらも容量やClass内訳がないため同額になる保証はありませんが、R2以外のVPS、DNS、Cloudflare for SaaSなどの費用が別に残る点は実運用の参考になります。
- R2の保存容量とClass A・B操作
- WorkersのrequestsとCPU時間
- 外部originからR2へ移す際の元サービス側egress
- 独自ドメイン、変換サービス、監視など周辺費用
Infrequent Accessは30日と取得料を先に見る
Infrequent Accessは保存が0.01ドル/GB-monthとStandardより低い一方、Class Aは9ドル/100万回、Class Bは0.90ドル/100万回、取得は0.01ドル/GBです。最低保存期間は30日で、5日で削除しても30日分の保存料金がかかり、無料枠は適用されません。
そのため、頻繁に読むWeb画像、配布中のinstaller、毎日再生される動画を最初からIAへ置くと、保存差額より取得料と操作料が増えることがあります。初めてのbucketはStandardにし、30日以上保持し、ほとんど読まれないbackupや過去ログだけをIA候補にします。
公式Storage classes資料は、StandardをWeb・アプリ・mediaなど頻繁に読む用途、IAをarchive、backup、long-tail user content向けとしています。SoloTech氏は生成動画を長期間保存しない用途でR2の費用が小さいと投稿していますが、短期保存でIAを選ぶと最低30日の条件があるため、保存期間が短いこととIAが安いことは同義ではありません。
比較するときは、100GBを1か月置いた場合の保存差額だけならStandard 1.50ドル、IA 1.00ドルです。しかしIAから月50GBを読み出せば取得料だけで0.50ドルが加わり、操作単価も高くなります。保存差額が小さい規模では、読み出し量が少し増えるだけで逆転するため、実測前にIAへ固定しない方が安全です。
- 月に何度も読む: Standard
- 30日以上保持してほぼ読まない: IAを試算
- 短期生成物: Standard+Lifecycle削除
- 迷う段階: 無料枠があるStandardから開始
Class A・Class Bをアクセス数から見積もる
Class Aは状態を変える操作が中心で、PutObject、ListObjects、multipartの各part uploadなどが含まれます。Class BはGetObjectやHeadObjectなどの読み取りです。DeleteObject、DeleteBucket、AbortMultipartUploadは無料操作ですが、削除前に全objectを何度もLISTすれば、そのLISTはClass Aになります。
料金ページには端数切り上げも明記されています。100万1回の操作は200万回分として計算され、保存1.1GB-monthは2GB-monthとして請求される例です。上限の直前だけを見るのではなく、月次見積もりに1課金単位の余裕を持たせます。
見積もりは「新規upload数」「一覧取得数」「originまで届くGET数」「HEAD数」をログから数えます。カスタムドメインのCacheがhitすればR2のGETを減らせますが、cache missはR2へ到達します。画像1枚をHTML上で10回参照したことではなく、R2まで届いたoperationを料金画面とanalyticsで確認します。
Cloudflare公式Budget alertsでは、Pay-as-you-go accountに金額閾値のemail通知を設定できます。ただし通知は情報提供だけで、利用を停止せず、請求上限にもなりません。2026年7月から対象accountへ10ドルのdefault alertを順次追加する更新もありますが、日次処理のためリアルタイム停止装置としては使えません。
- R2 analyticsで保存量と操作数を見る
- cache hitとorigin GETを分ける
- 5ドル・10ドルなど低いbudget alertを置く
- アプリ側にもupload容量と回数制限を置く
キャッシュ・ライフサイクル・大容量uploadを整える
カスタムドメインへCloudflare Cacheを設定すると、edgeから配信でき、R2への読み取り回数を減らせます。ただし公式Consistency資料は、Cache経由では削除したobject、上書き前のobject、過去の404がTTLまで残ることがあると説明しています。同じURLで差し替えるならpurgeまでを更新処理へ含めるか、file名へhashを付けます。
一時fileにはObject Lifecycleを設定し、例としてtmp/配下を7日後、処理済みlogを90日後に削除します。長期保管はStandardからIAへ30日後に移すruleも作れます。手作業の定期削除より、prefixと保存期間を設定として残す方が再現できます。
大容量fileはupload方式も変わります。公式Limitsでは1object最大5TiB、single-part upload最大5GiB、multipart upload最大約4.995TiB、partsは最大10,000です。動画やbackupを1回のPUTへ詰め込まず、retryできるmultipartを使い、失敗したuploadをAbortして不要partsを残さないようにします。
また、同じobject keyへの同時書き込みは1秒1回が上限です。多数の利用者がlatest.jsonを更新する設計より、UUIDや時刻を含む別keyへ保存し、参照先のmetadataだけを更新する方が競合を減らせます。Cacheを使う公開assetならapp.20260827.cssのようなversion付きkeyが安全です。
- R2直接操作の強整合とCache表示を同じだと思う
- Lifecycle削除とIAの30日最低期間を混同する
- 5GiB超をsingle-partでuploadする
- 複数処理が同じkeyへ同時上書きする
R2で失敗したときの確認順と次の一歩
PUTできない場合は、bucket名、Binding名、tokenの対象bucket、Object Read & Write権限の順に確認します。S3 SDKを使うならendpointはhttps://<ACCOUNT_ID>.r2.cloudflarestorage.com、regionはautoです。Secret Access Keyは作成時しか再表示できず、ブラウザやGitへ入れてはいけません。
ブラウザだけ失敗するならCORS、公開URLだけ404ならPublic access・custom domain・object key・Cache済み404を調べます。uploadは成功するのに古い画像が見えるならR2ではなくCacheをpurgeします。料金が増えたら、容量より先にClass A・Bのoperationと、cache miss、LIST、HEAD、multipart partsを分解してください。
公式Durability資料ではR2は年11 ninesの耐久性を目標としますが、意図的・誤操作による削除は防げないと明記されています。重要fileはbucket-scoped tokenで権限を絞り、bucket lock、別accountや別serviceへのbackup、復元テストを組み合わせます。「壊れにくい」と「消せない」は別です。
最初の公開は、1つのStandard bucket、1つのBinding、1MB未満のtest imageから始めてください。PUT→GET→DELETE、custom domain、CORS、budget alertまで通ったら、利用者uploadやmultipartへ広げます。この順番なら、コード、権限、公開、料金のどこで止まったかを一段ずつ判定できます。
- StandardでPUT・GET・DELETEを確認した
- 本番はcustom domain、r2.devは無効化した
- 署名URLは短時間、CORSはorigin限定にした
- 容量・Class A・Class B・IA取得料を試算した
- budget alertと誤削除対策を用意した
R2は、D1へ入れにくい画像やファイルをWorkersと同じCloudflare account内で扱うための有力な選択肢です。egress無料だけで決めず、公開経路とoperationを先に設計すれば、小さなWebアプリから配布・動画・backupまで段階的に広げられます。


