0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cloudflare D1の無料枠を500MBの壁まで埋めて実測:全面read-onlyにはならず、DELETEで容量メーターが即戻った

0
Posted at

この記事が答えること: 「Cloudflare D1(無料枠)のデータベースを1個の上限500MBまで埋めると、実際にどう失敗して・どう戻せるのか」 を、公式の説明でなく実測(REST API)で確かめる。
対象: Cloudflare D1(Workers Free)/捨てDB/REST API /query 経由/2026-07。
誠実さの宣言: この記事は初稿から二度、framing を作り直した。初稿の「操作サイズで生死が決まる」は、自分の測定の余白(headroom)に交絡していた。さらに追試で、その訂正(「CREATE INDEX はメモリ問題」)すら外れていた。机上の推論が二度外れて、測って初めて正解に着いた——その過程ごと出す(§6〜§8)。

30秒でわかる結論

無料のデータベースを上限まで埋めると何が起きるか、実際に1個あたり上限(約500MB)まで埋めて触って確かめた。

  • 上限に当たっても、全面的に読めなくなる(read-only化する)わけではない。読み取りも、行を消す操作も通る。
  • 行を消すと、D1が返す容量の数字がその場で下がって、書き込みが復活する。ただし「消して戻す」はタダではない——D1では削除も「書き込み」に数えられ、1日の書き込み枠を食う。
  • そして、大きな列にインデックスを1本張るだけで容量が約2倍になり、300MBのデータなら500MB壁を突き破って失敗した(実測)。

この下は、それを実測ログで確認していく。技術的興味がなければ、ここまでで結論はほぼ伝わる。

⚠ 射程: 数値・エラー文は 2026-07 の Workers Free・REST API の実測。粗い壁は2つの捨てDBで再現、それ以外は各n=1(本文で明示)。仕様は改定され得る。有料(Workers Paid)だと per-DB 上限が10GBになる。Worker Binding API では挙動が違う可能性がある(特にVACUUM)。

TL;DR(結論だけ)

観察対象=Cloudflare D1 無料枠(Workers Free)・捨てDB・REST /query・2026-07。測ったのは per-DBの500MB壁(アカウント合計5GB壁・日次read/write壁とは別・§3)。

  1. per-DB壁は約500MB(10進)。最後に成功した書き込みで size_after=499,994,624(≈ 500,000,000 − 5,376)、次が HTTP 400 / 7500 / "Exceeded maximum DB size"この粗い壁(~499.9MBでbulk拒否)は2つ目の捨てDBでも再現した。ただし公開ログはaccount IDをredactしているので、ログから辿れるのは「2つの充填が同地点で停止」まで(colo は Anycast で決まり、アカウントの証拠にはならない)。天井を1行単位でpinしたのは1DBのみ(n=1)
  2. 全面read-onlyにはならないが、小書き込みも"exempt"ではない。壁の後、単行INSERTは24回通って25回目で落ちた(メーター差分 ~110KB の余白を消費)。=読み取りは通る(容量を増やさないので当然)/書き込みは余白がある間だけ。「小書き込みは壁を越える」ではなく「隙間を埋めていただけ」。
  3. DELETEで即戻る、ただしwriteを食うDELETEsize_after が ~500MB→244MB に即降下して書き込み復活(VACUUM不要・REST経由では実行不可でもあった)。だがDELETEも rows_written に計上(docs明記)=19,998行で日次10万枠の約20%。"アップグレード不要"であって"クォータ無料"ではない。
  4. CREATE INDEXは容量を約2倍にして壁に当たる(=二度読み違えた本命)。BLOB列にindexを張ると size_after×1.998(84,029,440→167,923,712・80MiBで1回実測)。この倍化が他サイズでも成り立つなら、80MBは成功(→160MB)・300MBは失敗(→約600MBで壁超え Exceeded maximum DB size)・壁ちょうどではOOM(SQLITE_NOMEM)。indexの失敗は「メモリ問題で壁と無関係」ではなく、index自身の容量で壁に当たっていた。実用の目安は「無料枠では1列あたり約250MBまで」だが、これは倍化からの外挿(250MB近傍は未測定)。
  5. 失敗の識別はメッセージ。公式エラー一覧は壁を文字列で区別し(Exceeded maximum DB size/account storage/memory)、数値コード7500はdocsに一切出てこない(7500はRESTラッパの汎用番号)。

サマリ表(Workers Free・捨てDB・REST・2026-07・生ログ results/*.jsonl

操作(per-DB 500MB壁の付近/到達後) 実測 失敗時のcode/メッセージ
SELECT(読み取り・新規リクエスト含む) ✅ 通る
単行INSERT・UPDATE・DELETE ✅ 余白がある間は通る 余白ゼロで 7500 Exceeded maximum DB size
CREATE TABLE(空) ✅ 通る(+1ページ)
bulk INSERT(50行/約600KB) ⛔ 壁 7500 Exceeded maximum DB size
CREATE INDEX(BLOB列) ⛔ 容量約2倍で壁 300MB→7500 Exceeded maximum DB size/壁上→7500 out of memory: SQLITE_NOMEM
DELETE(回復用・大量) ✅ 通る+容量即降下 (rows_writtenを消費)

1. なぜこの検証をやるのか(動機と白地)

要点: 無料枠DBの"上限に当たると何が起きるか"は、確認した範囲では全部docsの言い換えで、実際に壁へ当てて失敗形と復旧を測った例が見当たらない。

「無料DB比較」記事は多い(容量・料金・cold start)。だが上限"到達時の挙動"を公式ドキュメントから引き写しているだけのものが大半で、筆者が確認した範囲では、実際に埋めて「どう失敗し、どう無料枠内で復旧するか」をログ付きで記録したものは見つけられなかった。本記事はその一点——壁の向こう側——をD1で測る。(筆者の系列: 無料枠ベクトルDB4種の律速比較、サーバレスDB3種の距離分解 の続き。D1単体の先行実測で、他プロバイダは続編。)

2. 実験設定(統制条件)

要点: 捨てDBを高エントロピー行で per-DB 上限まで埋め、失敗形を request ID ごと捕獲する。

  • 対象 = Cloudflare D1、Workers Free。使い捨ての新規DB(本番リソースに触れない)。粗い壁の再現性のため、別々に作った2つの捨てDBで実施した。
  • アクセス経路 = D1 REST API /query(外部ホストから)。Worker Binding API ではない(限界§9で開示)。
  • 投入データ = 決定的に生成した高エントロピーのバイト列を、REST本文に載せる都合で base64 文字列にしたもの(base64自体は約25%圧縮可能なので「圧縮不能」ではない。ただしサイズはD1の報告メーター基準で見るので、payloadの圧縮性は本質ではない)。
  • サイズ計測 = D1は各クエリ応答の meta.size_after(docs:「クエリ適用後のDBサイズ」)を返す。これを壁検出・容量計測の一次信号にした。
  • 安全性 = 充填は無料枠の日次書込枠(10万行/日)とアカウントstorage(5GB)の内側=金銭課金は起きない(Freeはブロック型)。

3. 事実テーブル:D1 の"別々の壁"(公式・確認日 2026-07-09・一次照合済

要点: 「無料枠の壁」は1つでない。per-DB storage(500MB)/アカウント storage(5GB)/日次 read・write の系統で、スコープも失敗形も違う。本記事が測ったのは1つ目。

スコープ 値(Free) 失敗形(公式エラー一覧の文言) 本記事で測定
per-DB storage DB単位 500 MB(Paid 10GB) "Exceeded maximum DB size"(action: delete rows / shard) ✅ 測定
account storage アカウント 5 GB "Your account has exceeded D1's maximum account storage limit…" 未測定
daily rows read アカウント 5,000,000 / 日 日次上限超過で「クエリ実行不可」 未測定(続編)
daily rows written アカウント 100,000 / 日 同上(INSERT/UPDATE/DELETE すべてが計上) 部分(充填は10万未満で完了)

補助的な数値(All plans): 最大行/BLOB 2,000,000 bytes (2MB) / 1テーブル最大 100列 / bound params 100 / DB数 Free 10個 / storage上限は日次リセットされない("Free limits reset daily at 00:00 UTC" は read/write の日次上限のみ)。

出典: developers.cloudflare.com/d1/platform/limits ・ /platform/pricing ・ /observability/debug-d1 ・ /worker-api/return-object(確認日 2026-07-09・全数値が現行docsと一致)。

4. ハーネス設計の要点

要点: coarse→fine で埋め、実 enforcement エラーで停止し、request ID まで残す。壁の値は単行INSERTでrow単位までpinした。

  • 充填は INSERT ... VALUES (?,?),... を50行バッチ(bound params上限100=2列×50)。meta.size_after を毎バッチ記録。
  • 停止は推定サイズでなく、実際の enforcement エラー(ok:false)で。壁到達後は単行INSERTを反復して天井をrow単位まで詰めた。
  • 記録は allowlist 方式(status・code・error body・CF-Ray/rayId・timestamp・meter・served_by_coloのみ/生レスポンスは非公開。account ID は記録しない)。
  • REST fetchは60秒タイムアウト+1回リトライ(作りたてDBの初回クエリが詰まる事象があったため)。コードはリポジトリ参照。

5. 結果①:per-DB壁は約500MB(10進)/粗い壁は2つ目のDBでも再現

要点: 最後に成功した size_after=499,994,624、次が 7500 "Exceeded maximum DB size"。粗い壁は別の捨てDBでも同地点で停止した。天井の1行pinは1DBのみ。

高エントロピー行をbulkで積むと約499.9MBで弾かれる。そこから単行INSERT(1回4KB)を反復して天井を詰めた:

… 単行INSERT × 24回 成功(size_after が段階的に上昇)
25回目: HTTP 400  [{"code":7500,"message":"Exceeded maximum DB size"}]
最後に成功した size_after = 499,994,624 bytes(≈476.83 MiB ≈ 500,000,000 の 5,376バイト下)

壁は約500MB(10進)。SQLite既定page_size=4096で size_after は4KB単位で動く(499,994,624 = 122,069 × 4096)。この粗い壁は別の捨てDBでも再現した(そのDBはbulkが同じ 499,884,032 で停止して同じ 7500)。ただし公開ログはaccount IDをredactしているので、ログから確認できるのは「2つのDB充填が同地点で停止」まで。天井を1行単位でpin(499,994,624)したのは1DBだけ。壁が発火したので、このDBはWorkers Free相当の500MB上限だと分かる(Paidなら10GBで当たらない)。

6. 結果②:主峰——全面read-onlyではない/小書き込みは"余白"/DELETEで即戻る(がwriteを食う)

要点: 壁の後、読み取りは常に通り(容量を増やさないので当然)、書き込みは"余白がある間だけ"通る(exemptではない)。復旧はDELETEで即・無料枠内だがwrite消費。

6-1. 「小書き込みは壁を越える」のか?——測ったら"SLACK"だった

初稿は「小書き込みは壁を越えて通る(大きい確保だけ止まる)」と書いた。だが壁の直後に単行INSERTを反復したら、24回通って25回目で 7500。消費した容量は 499,884,032 → 499,994,624 = 約110KB(メーターの差分)。つまり小書き込みは壁を越えていない——壁の直前に残った ~110KB の余白を段階的に埋めていただけで、余白を食い切った瞬間に単行でも落ちた。

→ 正しい結論は「読み取りは通る/書き込みは"余白がある間だけ"通る」。「操作サイズで生死が決まる(小はexempt)」という初稿の読みは、probeが壁の手前で走っていた交絡を見落としたもので、測って否定した。

6-2. 復旧=DELETE(無料枠内・ただしwrite消費)

DELETE WHERE k < 20000  → ✅ 19,998行削除・size_after 499.9MB→243,884,032(~244MB) に即降下・rows_written=19,998
bulk INSERT(削除後)    → ✅ 復活
VACUUM                  → ⛔ 7500 "cannot VACUUM from within a transaction: SQLITE_ERROR"(REST /query固有の可能性)
  • 回復クラス = R1(SQLのみ・アップグレード不要)。DELETE で報告サイズが即座に下がり書き込みが戻る(VACUUM不要・REST経由では不可でもあった)。
  • ただしタダではない: rows_written=19,998 を消費(D1は削除もwrite計上・docs)。日次10万writeの約20%。コストは削除した"行数"に比例する(この1回で約256MB解放=約12.8KB/行。刻んで消したときの線形性は別途測っていない)。小さい行を大量に積んだDBは、storage壁(#1)を逃げる途中で日次write壁(#4)に激突しうる。
  • size_after即降下 ≠ 物理ファイル収縮: これは報告メーターが下がった話(Cloudflareのローカル実装ではDELETEでSQLiteファイルが縮まない報告 workerd#1618 もある)。言えるのは「メーター上のサイズが下がり書込が復活」まで。

7. 結果③:CREATE INDEXは容量を約2倍にして壁に当たる(机上で二度外し、測って確定)

要点: BLOB列のindexは容量を約2倍にする(×1.998・1点で実測)。だからindexの失敗は「壁と無関係のメモリ」ではなく、index自身の容量で500MB壁に当たっていた。

壁ちょうどのDBで CREATE INDEX すると out of memory: SQLITE_NOMEM。ここから初稿は「大きい確保が止まった」、レビュー後の訂正は「これはメモリ問題で壁と無関係」と書いた。どちらも外していた。

壁より下(300MB)の控えDBで CREATE INDEX したら、返ってきたのは SQLITE_NOMEM ではなく 7500 "Exceeded maximum DB size"(ストレージ)。壁の遥か下なのにstorageエラー? 機序を直接測った——控えを80MBまで削ってindexを張ると:

CREATE INDEX ON bench(v) at 84,029,440 (80.1MiB) → ✅ OK
  size_after 84,029,440 → 167,923,712  = ×1.998(ほぼ2倍)

BLOB列のindexは、その列の値をキーとして持つため容量を約2倍にする——と考えられる(実測はこの1点・1サイズ)。この倍化が他サイズでも成り立つなら:

index前の容量 index後(約2倍) 結果
80 MB 160 MB ✅ 成功(壁内)
300 MB 約600 MB Exceeded maximum DB size(500MB壁超え)
500 MB(壁ちょうど) SQLITE_NOMEM(ビルド中にOOM)

→ CREATE INDEXの失敗は index自身の容量で壁に当たっていた(壁ちょうどでは先にOOMするのが別の顔)。実用の目安は「D1無料枠では1列あたり約250MBまで」だが、これは倍化からの外挿で、250MB近傍は測っていない。

8. 落とし穴ログ(測ってから語る・自分の訂正ごと出す)

要点: 通説・帰属・そして"自分の初稿と、その訂正"を、一次(docs+生ログ)で分けて直した。CREATE INDEXは誤→正が三段。

  • 公式docの"storage満杯=insert/DDL全面禁止"という粗いmental model(pricing・account 5GB の記述)。私が測ったのは per-DB 500MB壁で、そこでは小INSERT・CREATE TABLEは通った。公式エラー一覧が2つの壁を別メッセージで定義していること自体が、この区別の裏づけになる。
  • エラーコード7500の帰属。誤=「overload専用・非クォータ」→ 正=「per-DB壁の失敗形も7500/ただし7500は汎用で、docsは壁をメッセージで区別し番号を書かない」。
  • DELETE回復の"新規性"の範囲。公式は per-DB壁の推奨対応に「行削除 or shard」を挙げる=DELETE回復はdocsの処方箋そのもの。本記事が足したのは「同一クエリで即降下」と「write枠を消費」の二点だけ。
  • ★CREATE INDEX=誤→正が三段(この記事のハイライト)。①初稿「大きい確保だけ止まる」=過大主張 → ②レビュー後「indexはメモリ問題で壁と無関係」=今度は過剰訂正 → ③実測「indexは容量を約2倍にして500MB壁に当たる(300MBでもstorageエラー・80MBなら成功・壁上でのみOOM)」=真相。机上で二度外し、測って着地した。
  • 「小書き込みはexempt」も自分の初稿の誤り。probeが壁の手前で走っていた交絡で、単行反復で「余白消費」と確定した。

締め: 「一次で測る」を、docsだけでなくフォーラム・自分の初稿・その"訂正"・一般常識にも当てて、それぞれ別々に直した。自分が二度間違えた過程を消さずに出すのが、この検証記事の芯だと思う。

9. 限界・未測定の正直な開示

  • 測って確定した: per-DB壁の粗い値(~499.9MBで停止・2DBで再現)/1DBで天井を1行pin(499,994,624)/小書き込みは余白消費(exemptでない)/DELETE即降下+write消費/300MBでCREATE INDEXがstorageエラーになること。
  • n=1 / 外挿 / 未測定: **index倍化 ×1.998 は1点(80MiB・1DB)**=他サイズ・他row形状では未確認/「1列~250MB」は倍化からの外挿(250MB近傍は未測定)/DELETEの線形性(刻んだとき)は未測定(1回の19,998行DELETEのみ)/account 5GB壁・日次read壁(5M/日)・日次write壁は未測定。
  • size_after は挙動が一貫した論理メーター的(1回のDELETEで減り、1回のindexで約2倍)だが、docsは物理/論理を明言しない=物理ファイル収縮の証明ではない。
  • REST /query 限定(Worker Binding では挙動が違い得る・特にVACUUM)・Free限定・2026-07スナップショット。この実験自体が日次write壁に律速される(1周≈ 充填39,050 + 回復19,998 + 控え24,550 ≈ 約84,000 rows_written = 1 UTC日に1周強)。

10. 再現手順

要点: RESTトークンとDB IDを .env に置いて node を回すだけ。ダッシュボードが空でもDBはAPIで作れる(bin/d1-admin.mjs)。

git clone https://github.com/axiom-pro/free-tier-quota-probe
cd free-tier-quota-probe
npm install
npm run selftest                     # 認証情報なしのオフライン検査
# .env に CF_ACCOUNT_ID / CF_API_TOKEN(D1:Edit) / D1_DATABASE_ID / D1_CONTROL_DATABASE_ID / BILLING_SAFE=confirmed
node --env-file=.env bin/d1-admin.mjs create wallprobe   # ダッシュボードが空でもAPIで作れる
npm run ping    -- --target d1
npm run fill    -- --target d1 --confirm-destructive       # per-DB ~500MB壁まで
npm run probe-slack -- --target d1                         # 単行INSERT反復で壁をpin+余白測定
npm run fill    -- --target d1 --control --cap-mb 300 --confirm-destructive
npm run probe-index -- --target d1                         # 控えでCREATE INDEX(容量倍化)

検証環境: Node ≥20(ESM)・D1はnative fetch(追加パッケージ不要・60秒timeout+retry)。生ログは results/*.jsonl に凍結(redacted)。凍結した予測と追試のamendmentは PREREGISTRATION.md

まとめ

per-DB壁は約500MB(10進)で、全面read-onlyにはならない——読み取りも小書き込みも余白がある間は通る。DELETEで報告サイズが即降下して書き込みが戻るが、DELETEもwriteに計上されるので"クォータ無料"ではない。そしてCREATE INDEXは容量を約2倍にするため、300MBの列でも壁を突き破って失敗する。失敗の識別はメッセージで(7500はdocsに無い汎用ラッパ)。

「無料枠の壁」は1つでない(per-DB / アカウント / 日次read / 日次write)。測ったのはper-DB 1つで、残りと、上の未確定点(倍化の閾値・線形性など)は正直に「測っていない」と書いた。数字の横並びでは見えない——測る(そして自分の測り方と、その訂正すら疑う)しかない。


全数値は実測(results/*.jsonl)。粗い壁は2つの捨てDBで再現。n=1・外挿・未測定は本文に明記した。本稿は外部レビューと追試で初稿から二度framingを直している(§6〜§8)。

リポジトリ:https://github.com/axiom-pro/free-tier-quota-probe


筆者について

「ドキュメントを鵜呑みにせず一次で測って、自分の思い込み(初稿の framing も、その訂正すら)直す」 検証規律を専門にしています(本記事の §6〜§8 がその実演——机上で二度外し、測って着地した過程を消さずに出しました)。本記事はAI支援で執筆し、数値はすべて公開ログ results/*.jsonl から取っています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?