この記事が答えること: 「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)。
-
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)。 - 全面read-onlyにはならないが、小書き込みも"exempt"ではない。壁の後、単行INSERTは24回通って25回目で落ちた(メーター差分 ~110KB の余白を消費)。=読み取りは通る(容量を増やさないので当然)/書き込みは余白がある間だけ。「小書き込みは壁を越える」ではなく「隙間を埋めていただけ」。
-
DELETEで即戻る、ただしwriteを食う。
DELETEでsize_afterが ~500MB→244MB に即降下して書き込み復活(VACUUM不要・REST経由では実行不可でもあった)。だがDELETEも rows_written に計上(docs明記)=19,998行で日次10万枠の約20%。"アップグレード不要"であって"クォータ無料"ではない。 -
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近傍は未測定)。 -
失敗の識別はメッセージ。公式エラー一覧は壁を文字列で区別し(
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 から取っています。
- 🔍 コード/挙動の監査("緑でも正しいとは限らない"を実測で示す)— ココナラで見る
- 💻 手元で完結するアプリ開発(外部送信しない設計)— ココナラで見る
- プロフィール:https://coconala.com/users/6153961 / 公式:https://getaxiom.dev