リトライボタンを押した。SNSのタイムラインに、まったく同じ投稿が2つ並んだ。
血の気が引いた。ユーザーの信頼を一瞬で吹き飛ばす、SaaS開発者にとって最悪のバグだ。
※この記事は、Claude Codeで1人開発しているSNS運用SaaS「ThreadPost」の開発日記です。
SNS運用を自動化しませんか?
ThreadPostなら、投稿作成・画像生成・スケジュール管理まで全てAIにお任せ。
核心回答:サーバーレスのタイムアウトが二重投稿を生む
投稿システムの安定化をやっていた。結果として、システムを根底から作り直す羽目になった。

今週の総コミットは29件。新機能1件に対して、バグ修正が3件。
InngestとVercelの組み合わせは強力だ。でも、ネットワークの瞬断やタイムアウトが起きた瞬間に牙を剥く。
「完了したのに完了通知が届かない」というサーバーレスの呪い。これと3日間戦い続けた。
サーバーレスアーキテクチャは、スケーラビリティに優れている。アクセスが急増しても自動でリソースが割り当てられる。
しかし、その代償として「状態の管理」が極めて難しくなる。プロセスはいつ強制終了されるかわからない。
APIへの送信は完了したのに、DBへの「完了済み」フラグの書き込みがタイムアウトする。
次にリトライが走った時、システムは「まだ送信されていない」と勘違いする。
結果、同じ内容がもう一度SNSに放たれる。スパムボットの完成だ。
これを防ぐため、DB再チェック、24時間検出窓、投稿時間ガードの3層構造を実装した。
一般的なWebアプリケーションなら、DBのユニーク制約だけで十分なことが多い。
しかし、外部APIと連携する非同期処理では、それだけでは防ぎきれない。
ネットワークの向こう側で何が起きているか、完全には把握できないからだ。
だからこそ、複数のガードを重ねて、異常な状態を検知して止める仕組みが必要になる。
分散システムにおける状態管理の難しさは、大手テック企業でも頻繁に障害を引き起こす。
それを1人開発のSaaSで、しかもAIの力を借りて解決しなければならない。
AIにリトライを頼んだらスパムボットが生まれた

Claude Codeに「失敗したら再実行して」と指示した。
テスト環境でリトライボタンを押した。SNSに同じ投稿が2回流れた。
「再実行」のコードだけが追加され、重複チェックのロジックは一切なかった。
分散システムにおける「Exactly-once(正確に1回)」の保証。業界では聖杯と呼ばれる難題だ。
AWS SQSやGoogle Cloud Pub/Subを使っても、デフォルトは「At-least-once(少なくとも1回)」。
つまり、何もしなければ重複は必ず起きる。だからシステム側で冪等性(べきとうせい)の担保が必要になる。
僕はAIがよしなに冪等性を担保してくれるはずだと高を括っていた。
結果として、エンジニアにとっての悪夢が現実になった。
サーバーレス環境のInngestやVercelでは、この問題がさらに複雑になる。
Vercelの実行時間制限でプロセスが強制終了されると、タスクは中途半端な状態になる。
APIへの送信は完了したのに、DBへの「完了済み」フラグの書き込みがタイムアウトする。
次にリトライが走った時、システムは「まだ送信されていない」と勘違いする。
結果、同じ内容がもう一度SNSに放たれる。
しんたろー:
タイムラインに並んだ2つの同じ投稿を見た瞬間、変な汗が出た。
企業アカウントでこれが起きたら、一発で契約解除だ。炎上の火種にしかならない。
ここから泥臭い戦いが始まった。一般的なAPI開発なら、DBのユニーク制約という1層のガードで済ませることが多い。
でも、それじゃ防げなかった。「fix: リトライ時の重複投稿防止 — DB再チェック+24h検出窓+posted_atガード」を実装した。
まず、処理の冒頭でDBから最新のステータスを再チェックする。キャッシュをバイパスして直接見に行く。
次に、24時間以内の投稿を検出する窓を作った。Inngestの指数バックオフ対応のため、5分から24時間に拡大した。
最後に、「posted_at」でガードをかけた。X投稿は「posted_at」を先行設定する仕様のため、これでスキップ判定を行う。
これで3層。1個のガードが抜けたとき、次のガードが止める。
「fix: next_execution_atの分・秒を切り捨てて正時にする」という修正も入れた。
cronは毎時0分に実行されるが、秒単位の端数が残っていると数秒の差で取りこぼす。
1時間遅れで送信されるという地味なバグ。これも重複や送信漏れの温床だった。
時刻の扱いは、プログラミングにおいて常に鬼門だ。
特に分散システムでは、サーバー間のわずかな時刻のズレが致命的なバグを引き起こす。
「fix: 日常投稿の画像自動生成をawaitに変更(Vercelで打ち切られる問題)」も追加した。
「fire-and-forget」だとVercelのサーバーレス環境でレスポンス返却後に処理が打ち切られる。
Inngestイベントが送信されず、システムが宙吊りになる。これもリトライの暴走を招く。
非同期処理の完了を待たずにレスポンスを返すのは、パフォーマンス向上の常套手段だ。
しかし、サーバーレス環境ではそれが命取りになる。プロセス自体が凍結されるからだ。
さらに「fix: JSON文字列内のリテラル改行でパース失敗するバグを修正」も絡んでいた。
JSON内に生改行が入ってパースエラー。これが無駄なリトライを引き起こす原因の一つだった。
細かいエラーが積み重なって、Inngestのリトライキューにタスクが溜まっていく。
データの境界線が曖昧になると、システムは簡単に暴走する。
重複率100%から0%へ。3層のガードを作って、ようやく「同じ投稿が2回出ない」状態になった。
30分で終わった。ただしコードはガード句だらけでボロボロだった。
触るな危険のダイアログと泥臭いDOMの戦い
投稿作成ダイアログで、直感的にテキストを入力したかった。
「feat: 日常投稿機能の実装(Phase 1a/1b/2: DB・型・サービス層・API・UI)」の続きだ。
ペルソナに日常投稿の自動生成機能を追加していた。手動でサクッとテキストを修正できる、気持ちのいいUIを目指した。
textareaをクリックした。キーボードが即座に閉じた。
何度タップしても、一瞬でフォーカスが外れる。入力成功率0%。
「触るな危険」状態のダイアログが爆誕した。
ReactなどのUIフレームワークでよくある、DOMイベント伝播の罠だ。
親要素の背景クリックで「ダイアログを閉じる」処理を入れるのは、モダンUIの基本。
でも、子要素からのイベントバブリングを止め忘れると、悲惨なことになる。
入力欄を触ったつもりが、親要素に「閉じろ」という命令が伝わってしまう。
ユーザー体験を向上させるための機能が、逆にユーザーを激怒させるバグに変わる瞬間だ。
「fix: 投稿作成ダイアログでテキスト入力できない問題を修正」をコミットした。
クリック対象が「input」「textarea」「select」の場合は早期リターンする。単純なガード句を一つ入れるだけ。
たったこれだけのことで、入力成功率は100%になった。
複雑なダイアログを作っていると、こういう基礎的なDOMの挙動を見落としがちだ。
AIにUIコンポーネントの生成を任せると、見た目は完璧なものが一瞬で出来上がる。
しかし、イベントの伝播やフォーカス管理といった、目に見えない部分の考慮がすっぽり抜け落ちることがある。
しんたろー:
基礎的なイベント伝播のバグ。初心者がやるようなミスだ。
派手なAI機能の裏には、こういう地味なDOMとの格闘が100個くらいある。
「fix: 予約時間変更が全アカウント分更新されないバグを修正」も同じような泥臭い修正だ。
idで1件のみ更新していたため、複数アカウントへの予約が片方にしか反映されなかった。
配列を使って全アカウント分を一括更新するよう修正した。
状態管理の不整合は、マルチテナントSaaSにおいて最も恐ろしいバグの一つだ。
AさんのデータがBさんに見えてしまうような、致命的な情報漏洩に繋がる危険性すらある。
「fix: 投稿作成ダイアログのscheduled_postsをupsertに変更」もやった。
「insert」だと日時変更時に既存レコードが更新されず、重複レコードが作成されていた。
ここでも「重複」との戦いがあった。一つずつ潰していくしかない。
データベースの操作において、「作成」と「更新」を厳密に分けるのは意外と難しい。
「upsert」を使うことで、レコードが存在すれば更新、なければ作成という処理を安全に行える。
「fix: AI生成画像がSNSフィルタで弾かれてP1に紐づかないバグを修正」も追加した。
AI生成画像は自前で生成したものだから、SNSフィルタを通す必要がない。
1枚目を自動的に選択状態にするよう変更した。
外部APIの仕様変更や、予期せぬフィルタリングによって、システムは簡単に壊れる。
こういう泥臭い修正の積み重ねが、システムに安定をもたらす。
僕は二重投稿の恐怖から解放され、ようやく安眠を手に入れた。
ここまで読んだあなたに
今なら無料で全機能をお試しいただけます。設定後は完全放置でプロ品質の投稿を毎日生成。
落とし穴:スレッド投稿がXに流し込まれる
AIに「スレッド投稿を実装して」と頼んだ。
「feat: スレッド自動生成 + 編集UI + publish対応」というコミットだ。
Threads APIとX APIの仕様差を無視して、Threads用のコードがXに流し込まれた。
全投稿がエラーで弾かれた。
「fix: 投稿作成ダイアログのplatformがthreadsにハードコードされていたバグを修正」で直した。
Xアカウントの予約投稿もThreads APIで実行されて失敗していた。
「スレッド」という単語一つで、プラットフォームの判定ロジックがすべて「threads」に書き換わっていた。
しんたろー:
まじかよ…全部Threads行きになってる。
XのAPIキーでThreadsのエンドポイント叩いてるんだから、そりゃ動くわけない。
プラットフォームごとにAPIの仕様は全く異なる。
Xは文字数制限が厳しく、メディアの添付方法も独特だ。
一方、Threadsは比較的緩いが、スレッドの構築方法が違う。
AIは「スレッド」という言葉の響きだけで、すべての処理をThreads向けに最適化してしまった。
文脈の境界線を勝手に越えてしまう。これがAI開発の恐ろしいところだ。
人間なら「Xのスレッド機能」と「ThreadsというSNS」を文脈で区別できる。
しかし、AIにとっては単なるトークンの連続に過ぎない。
だからこそ、プロンプトで指示を出す際には、人間相手以上に厳密な言葉の定義が必要になる。
今日の数字
| 項目 | 数字 | 比較・備考 |
|---|---|---|
| 総コミット | 29件 | 先週は15件。倍近い修正量。 |
| 新機能 | 1件 | 企業開発なら1週間。僕は1日。 |
| バグ修正 | 3件 | そのうち1件が致命傷になりかけた。 |
| 重複防止層 | 3層 | 一般的なAPI開発では1層。 |
1回の投稿失敗が招くSNS上の信頼損失は、開発工数100時間分に匹敵する。
ユーザーからの「また重複してるよ」という問い合わせ対応コストは、プライスレスだ。
僕は数時間の格闘で、未来の炎上を未然に防いだ。
よくある質問
Q: サーバーレス環境でのタイムアウトによるコスト増加はどう防ぐ?
実行時間の制限を厳密に設定し、再試行回数に上限を設ける。今回は24時間の検出窓を設けることで、無駄なリトライループを物理的に遮断した。無限リトライはAPI課金を爆発させる原因になる。
Q: 複数プラットフォームのAPIを扱う際、AIはどうコードを生成した?
APIのエンドポイントやペイロードの構造が違うのに、AIは共通の型定義でまとめようとした。結果としてXの投稿がThreadsの仕様で送信されて全滅した。型定義の段階で厳密に分離しないと、プラットフォーム間の境界線が簡単に崩れる。
Q: 冪等性を担保するためのDB設計はどう実装した?
単一のユニークキーに頼らず、複合的な条件で状態を管理した。今回はステータス確認とタイムスタンプを組み合わせた。一つの制約がすり抜けられても、別のカラムで異常を検知できる構造にした。
泥臭いガードの先に安眠がある
二重投稿の悪夢から解放され、ようやく安心してシステムを動かせるようになった。

この記事が参考になったら、ThreadPostを試してみませんか?
投稿作成・画像生成・スケジュール管理まで、全てAIにお任せできます。
ThreadPostをもっと知る