昨日まで動いていたデータ同期が413エラーで止まるとき|毎回全件を送る設計は件数が増えると必ず壊れる理由と、未送信分だけを小分けに送る直し方
昨日まで動いていたデータ同期が413エラーで止まるとき|毎回全件を送る設計は件数が増えると必ず壊れる理由と、未送信分だけを小分けに送る直し方
手元のパソコンやスプレッドシートに溜めたデータを、サーバー上の管理画面へ自動で送っている。何か月も問題なく動いていた。コードも設定も触っていない。それなのに、ある日からログに HTTP Error 413 が出て、データが届かなくなった。
この症状で最初に疑うべき原因は1つです。
同期のたびに「全件」を1回のリクエストで送っていて、データが増えた結果、受け側サーバーが決めている「1回に受け取れる大きさの上限」を越えた、です。
413は「Request Entity Too Large」(新しい表記では Content Too Large)、つまり「送ってきた中身が大きすぎるので受け取らない」という意味の応答です。プログラムが壊れたのでも、サーバーが落ちたのでもありません。送る量が、受け口の大きさを越えただけです。
この記事では、先に正しい直し方を示し、そのあとで弊社の社内ツールで実測した数字、そして「上限を上げる」で済ませてはいけない理由をまとめます。
結論:上限を上げるのではなく、送り方を変える
直し方は次の2つをセットで入れます。
- 未送信分だけを送る:送る前に、受け側に「すでに持っているデータのID一覧」を問い合わせ、手元にあって受け側に無いものだけを抜き出す
- 小分けにして送る:抜き出した分を、50件ずつのように件数を決めて区切り、複数回に分けて送る
この2つを入れると、1回のリクエストの大きさは「データの総件数」と無関係になります。総件数が1,000件でも10万件でも、1回に運ぶのは最大50件分です。データがどれだけ育っても、上限に触れなくなります。
逆に、サーバー側の上限値を引き上げるだけの対処は、壊れる日を先に延ばしているだけです。この点は後半で詳しく書きます。
なぜ「何も変えていないのに」止まるのか
全件を毎回送る方式は、作った直後はまったく問題なく動きます。データが少ないからです。
| 時期 | 件数 | 1回の送信サイズ | 結果 |
|---|---|---|---|
| 作った直後 | 数十件 | 数十KB | 成功 |
| 数週間後 | 数百件 | 1MB弱 | 成功 |
| ある日 | 上限を越える件数 | 1MB超 | 413で拒否 |
| それ以降 | 増え続ける | さらに大きくなる | 毎回413 |
大事なのは最後の行です。一度413が出始めたら、待っていても自然に直ることはありません。データは増える一方なので、次の実行ではもっと大きくなり、また弾かれます。一時的な通信エラーなら再実行で通りますが、413は再実行しても結果が変わらない種類のエラーです。
つまりこの方式は、「件数が増えると、いつか必ず壊れる」ことが最初から決まっている設計です。テスト時のデータ量ではまず再現しないので、作った本人も気づけません。
実例:3.1MBの全件送信が、1MBの上限で弾かれていた
弊社の社内ツールで実際に確認した数字です。手元のMacで1日2回データを集め、サーバー上の確認画面へ送る構成でした。
- 手元のデータ:1,469件
- サーバー側の確認画面にあったデータ:490件
- 差の979件が、確認画面に届いていなかった
- 全件を1回で送るときの本文の大きさ:約3.1MB
- 受け側のWebサーバー(nginx)の上限:1MB(
client_max_body_sizeを指定していないときの既定値)
ログを遡ると、最後に同期が成功したのは朝の実行で、同じ日の夕方の実行から413が出始めていました。1件あたり約2KBなので、490件前後でちょうど1MBに届く計算になり、件数と上限の関係とも合います。そこから気づくまでの約6週間、1日2回の実行はすべて同じエラーで終わっていました。
ここで押さえておきたいのは、集める処理そのものは止まっていなかった点です。収集も加工も毎日正常に終わり、手元のファイルは増え続けていました。止まっていたのは最後の「送る」部分だけです。処理全体が落ちていれば誰かがすぐ気づきますが、最後の一段だけが失敗する形は、画面を見に行かない限り表に出てきません。
この「動いているように見えて届いていない」状態の見つけ方は、自動処理の監視は「最後に成功した時刻」を見るで詳しく書いています。今回も決め手は、受け側にある最新データの日付が何週間も前で止まっていたことでした。
手順1:原因が本当に「大きさ」かを確かめる
直す前に、3つの数字を確認します。推測ではなく数字で切り分けるためです。
① 送信している本文の大きさ
送る直前のデータをファイルに書き出して、サイズを見ます。JSONで送っているなら、そのJSONファイルの大きさがほぼそのまま本文の大きさです。
② 受け側の上限
受け側が何で動いているかによって、上限を決めている場所が違います。
| 受け側 | 上限を決めている設定 | 指定しないときの値 |
|---|---|---|
| nginx | client_max_body_size |
1MB |
| Apache | LimitRequestBody |
バージョンやレンタルサーバーの設定で異なる(サーバー会社の仕様を確認) |
| CDN・WAFを前段に置いている場合 | 各サービスのリクエストサイズ上限 | 契約プランごとに異なる |
| 外部サービスのAPI | サービスごとの仕様 | 各サービスの公式ドキュメントで確認 |
nginxの既定値が1MBという点は見落としやすいところです。設定ファイルに client_max_body_size の行が無い場合、「上限なし」ではなく「1MB」で動いています。
③ 手元と受け側の件数の差
手元の件数と受け側の件数を並べます。差があり、受け側の最新データの日付が古いところで止まっていれば、その日が壊れた日です。
①が②を越えていて、③の「止まった日」とログの413の初出日が一致すれば、原因は確定です。
手順2:未送信分だけを抜き出す
受け側に「いま持っているデータのID一覧」を返す口を用意し(すでに書き出し用のAPIがあればそれを使えます)、送る前に問い合わせます。
def remote_ids():
"""受け側に既にあるID。取得できなければ None"""
try:
remote = get("/api/export")
return {e["id"] for e in remote["entries"]}
except Exception as e:
print(f"既存IDの取得に失敗(全件を分割送信に切替): {e}")
return None
have = remote_ids()
if have is not None:
entries = [e for e in entries if e["id"] not in have]
ポイントは2つあります。
データ1件ごとに、変わらないIDを持たせておくこと。 行番号や配列の順番をIDの代わりにしていると、途中の1件を消しただけで以降が全部ずれ、差分が取れません。データを作った時点で固有のIDを振り、それを最後まで使い回します。
ID一覧が取れなかったときの逃げ道を決めておくこと。 上の例では、取得に失敗したら「全件を対象にして、小分け送信に進む」ようにしています。受け側で「同じIDは二重登録しない」処理を入れてあれば、全件を送り直しても重複は増えません。差分の取得と、受け側の重複排除は、どちらか片方ではなく両方入れておくと、片方が崩れても壊れません。
手順3:50件ずつに区切って送る
CHUNK = 50
added = 0
for i in range(0, len(entries), CHUNK):
batch = entries[i:i + CHUNK]
try:
res = post("/api/enqueue", {"entries": batch})
except Exception as e:
print(f"{i}件目以降の送信に失敗: {e}")
break
added += res.get("added", 0)
1回の件数は、1件あたりの大きさ × 件数が、上限の10分の1程度に収まるように決めます。弊社の例では1件あたり約2KBだったので、50件で約100KB。上限1MBに対して十分な余裕があり、1件のデータが多少長くなっても触れません。
途中で失敗したら、その場で止めて「何件目から失敗したか」をログに残します。すでに送れた分は受け側に入っているので、次回の実行では手順2の差分抽出によって、残りだけが自動的に対象になります。やり直しのための特別な仕組みを作らなくても、続きから再開できるのが、差分と小分けを組み合わせる利点です。
この修正を入れて実行した結果は「追加979件・重複スキップ0件」。受け側の件数が手元と同じ1,469件になり、最新データの日付が当日になったことを、画面ではなく受け側のデータを直接数えて確認しました。修正前のファイルは日付つきの別名で残し、問題があればすぐ戻せる状態にしてあります。
「上限を上げる」だけで済ませない理由
nginxなら client_max_body_size 10m; と1行書けば、その日のうちに同期は復旧します。それでも、これを本筋の対処にしないほうがよい理由が3つあります。
1. 壊れる日が先に延びるだけ。 1MBを10MBにしても、件数が10倍になれば同じことが起きます。しかもそのときには、今回の経緯を覚えている人がいないかもしれません。
2. 送信が毎回どんどん重くなる。 全件送信は、すでに届いているデータを毎回送り直しています。今回の例でも、止まる直前は、すでに届いている数百件を1日2回、毎回送り直していました。件数が増えるほど通信時間も受け側の処理時間も延び、今度はタイムアウトという別の形で止まります。
3. 受け口を広げることは、外からの大きな送信も受け入れること。 上限は、巨大なデータを送りつけられてサーバーが詰まるのを防ぐ安全装置でもあります。必要もなく広げるものではありません。
もちろん、画像や動画のアップロードのように、1件そのものが大きい用途では上限の引き上げが正解です。判断の軸は「大きいのは1件なのか、件数なのか」です。件数が原因なら送り方を直す、1件が大きいなら上限を上げる、と分けて考えます。
なお、急ぎで復旧が必要なときに、応急処置として上限を上げ、並行して送り方を直すのは妥当な順番です。上げたまま終わりにしないことだけ決めておきます。
同じ作りの処理が他にないかを見直す
413に限らず、「毎回全部を処理する」作りは、データが増えるといつか別の上限にぶつかります。
- スプレッドシートの全行を毎回読み直して集計する → 実行時間の上限
- 全顧客に対して毎回APIを呼ぶ → 呼び出し回数の上限
- 全記事を1回のリクエストで取得する → 1ページあたりの件数上限
実行時間や呼び出し回数の上限については、スプレッドシートの自動集計が途中で止まるときで切り分け方をまとめています。
自動化の仕組みを作るとき、あるいは制作会社から納品を受けるときは、次の3点を確認しておくと、数か月後の「突然止まった」を防げます。
- この処理は、データが10倍になっても同じ時間・同じ大きさで動くか
- 送る(処理する)のは「全部」か「前回からの差分」か
- 最後の一段だけが失敗したとき、誰がどこで気づけるか
3つ目は特に重要です。この種の失敗では、エラーはログに毎回きちんと出ます。それでも、ログは見に行かなければ気づけません。ログに書くだけでなく、「受け側の最新データが○日以上古かったら通知する」という形で、結果の側から監視しておくと、何週間も後ではなく翌日に気づけます。終了コードや実行ログではなくデータ側で健全性を見る考え方は、自動処理が「成功」と出ているのに反映されないときも参考にしてください。
まとめ
- コードを変えていないのに同期が413で止まったら、原因は「全件を1回で送る設計のまま、データが受け側の上限を越えた」こと
- 413は再実行しても直らない。データは増える一方なので、放置すれば毎回失敗し続ける
- 確認するのは3つの数字:送信本文の大きさ、受け側の上限(nginxは指定なしで1MB)、手元と受け側の件数差
- 直し方は「受け側の既存IDを取得して未送信分だけ抜き出す」+「50件ずつのように小分けで送る」のセット。1回の送信サイズが総件数と無関係になる
- 受け側の重複排除も併せて入れておくと、差分取得に失敗しても全件の小分け送信で安全に回復できる
- 上限の引き上げは、1件そのものが大きい用途の対処。件数が原因のときは応急処置にとどめ、送り方を直す
- 最後の一段だけが失敗する形は表に出にくい。受け側の「最新データの日付」を監視対象にする
店舗の売上データ、予約データ、問い合わせの記録など、日々増えていくデータを扱う仕組みは、作った日のデータ量ではなく、1年後のデータ量で動くかどうかで設計の良し悪しが決まります。「いまは動いている」仕組みこそ、送り方が全件か差分かを一度確認しておくことをおすすめします。