ロックを取得して、ステータスを更新して、投稿した。
完璧な制御だと思っていたのに、二重投稿された。
物理的に不可能なはずの現象が目の前で起きた。
※この記事は、Claude Codeで1人開発しているSNS運用SaaS「ThreadPost」の開発日記です。
SNS運用を自動化しませんか?
ThreadPostなら、投稿作成・画像生成・スケジュール管理まで全てAIにお任せ。
矛盾だらけの1週間
今週は投稿実行システムの安定化に全振りした。
総コミットは29件。
新機能が1件、バグ修正が1件だった。
事実上の全書き換えだ。

二重投稿を防ぐための多層防御を構築した。
結果的に、投稿はほぼ確実に成功するようになった。
代償として、コードの複雑さは跳ね上がった。
SNS運用ツールにおいて、二重投稿は致命的なバグだ。
同じ内容が連続でタイムラインに並ぶと、スパムアカウントとして認識される。
ユーザーの信頼を一瞬で失う。
絶対に防がなければならない。
そのために、何重ものロック機構と状態管理を実装した。
しかし、防御を固めれば固めるほど、システムは身動きが取れなくなった。
鉄壁の防御が、自らの首を絞める結果になった。
Claude Codeに「二重投稿を防いで」と指示を出した。
AIは素直に、DBのトランザクションとロック機構を提案してきた。
教科書通りの完璧なアプローチだ。
しかし、現実のシステムは教科書通りには動かない。
AIは理想的な環境を前提にコードを書く。
ネットワークは常に安定していて、APIは仕様通りにレスポンスを返す。
そんなユートピアは現実のインターネットには存在しない。
泥臭いエラーハンドリングを人間が指示する必要がある。
終わらないデッドロック地獄
「pending」のレコードを「processing」に変えて、他のプロセスが触れないようにした。
単純なロック機構だ。
これで二重投稿は物理的に不可能になるはずだった。
ロック競合が起きたとき、プロセスが落ちた。
ステータスが「processing」のまま凍結された。
次のバッチが来ても「処理中だ」と判断してスキップする。
二重投稿どころか、投稿自体が永遠に止まるデッドロック地獄に陥った。
分散システムにおける「Exactly-once(正確に1回)」の実現は非常に難しい。
1回だけ確実に処理を実行させるのは、想像以上に骨が折れる。
AIが書いたロック機構は、正常系では完璧に動いた。
しかし、異常系に入った瞬間に牙を剥いた。
ロックを取得したままプロセスが落ちると、そのレコードは永遠にロックされたままになる。
Claudeに「ロックが解除されない」と伝えた。
すると「タイムアウト処理を追加します」と返ってきた。
タイムアウト時間を5分に設定した。
今度は、処理に5分以上かかる重い画像投稿が、途中でロック解除されて二重投稿された。
あちらを立てればこちらが立たず。
サーバーレス環境では、APIのタイムアウトとDBのトランザクション分離レベルのズレが致命傷になる。
一般的に、AWS Lambdaのような環境でも二重実行問題は頻発する。
イベントの再送やネットワークの瞬断で、同じ処理が複数回走るからだ。
金融システムなら、これが即座に二重課金というインシデントに直結する。
決済ゲートウェイのStripeなどは、冪等性キーをAPIリクエストに含める設計を採用している。
同じキーでのリクエストなら、何度送っても1回しか処理されない仕組みだ。
エンタープライズならこれが普通だ。
しかし、SNSのAPIにそんな親切な機能はない。
自分で制御するしかない。
「fix: ロック競合時にprocessingのままになるバグを修正」
ステータス遷移を厳格化し、リトライ制御を実装した。
「feat: allow lock acquisition from both pending and processing states」
「pending」と「processing」の両方からロックを取得できるようにした。
これで無限ループを回避した。
ただし、コードはスパゲッティになった。
さらに、ステータスに「publishing」を追加した。
「fix: 投稿の重複実行を防止する多層防御を追加」
処理の各段階で状態を細かく管理する。
「fix: Threads二重投稿防止の強化」
各プラットフォームの投稿成功直後に「posted_at」を即座に設定する。
カルーセル投稿、単一投稿、X投稿それぞれで即時設定する。
Inngestのステップ再実行時に、確実にスキップさせるためだ。
Fly.ioのマシンも1台に絞った。
「fix: Fly.ioマシン数を1台に制限」
分散環境では、複数のワーカーが同じキューを拾うと競合が起きる。
ワーカーを1台にするのが一番安上がりで確実だ。
これで競合は物理的に起きなくなった。
システムのスケーラビリティは完全に消滅した。
しんたろー:
まじかよ。単純なロックでいけると思ったのに。
12時間かけて書いたコードの半分がエラー処理。
競合を防ぐために、コードの複雑さが10倍になった。
シュレーディンガーの投稿
Threads APIのレスポンスを信じることにした。
正常終了した投稿だけをDBに記録すればいい。
単純明快なロジックだ。
APIがエラーを返ってきた。
当然、システムは失敗と判断してリトライをかけた。
しかし、実際にはサーバー側で投稿が完了していた。
APIはエラーと言っているのに、投稿は成功している。
シュレーディンガーの投稿が発生した。
リトライのたびに投稿が増殖し、タイムラインが同じ投稿で埋め尽くされた。
SNS系APIは、HTTP 500を返しても裏で処理が進むことがよくある。
ネットワークの切断や、ロードバランサーのタイムアウトが原因だ。
クライアントにはエラーが返るが、バックエンドのワーカーは処理を継続している。
この非同期な挙動をクライアント側で完全に把握することは不可能だ。
普通はサーバー側で重複排除させる。
しかし、Threads APIにはその仕組みがない。
Claudeに「APIが嘘をつく」と相談した。
AIは「APIのドキュメントにはそんな仕様は書かれていません」と困惑していた。
ドキュメントにない挙動に対応するのがエンジニアの仕事だ。
クライアント側で防衛策を講じるしかない。
「fix: Threads APIエラー時の二重投稿防止を強化」
APIの戻り値に依存しない防御へシフトした。
投稿前に、直近5分以内の同一内容を検索するチェックロジックを追加した。
事実確認型の防御だ。
APIがエラーを返しても、まずはタイムラインを見に行く。
本当に投稿されていないか、自分の目で確かめる。
泥臭いが、これが一番確実だった。
多層防御という名の要塞を築き上げた。
どんなエラーが起きても、二重投稿だけは絶対に防ぐ。
完璧なシステムが完成した。
APIの呼び出し回数が2倍に跳ね上がった。
しんたろー:
APIを信じた僕がバカだった。
エラーを返したくせに、しれっと投稿してるんじゃないよ。
結局、APIより自分の検索結果を信じるハメになった。
落とし穴:完璧な設計図の末路
投稿ステータスの遷移を完璧にしようとした。
「publishing」というステータスを追加し、状態管理を精緻化した。
完璧な設計図を描いた。
DBの制約チェックで「publishing」を許可し忘れた。
新しいステータスへの更新が、すべて制約違反で弾かれた。
全投稿がDB書き込みエラーになった。
「fix: add publishing status to scheduled_posts check constraint」
投稿システムを直すという目的だった。
一番最初に、投稿を全滅させるという大惨事を引き起こした。
アプリケーション側のコードは完璧に動いていた。
Claude Codeも自信満々に「実装完了しました」と報告してきた。
しかし、PostgreSQLのCHECK制約がすべてを拒絶した。
マイグレーションファイルとアプリケーションコードの同期漏れだ。
ローカル環境のテストデータでは、たまたま制約が緩かった。
本番環境にデプロイした瞬間、すべての投稿がエラーを吐き出した。
Claudeに「DBエラーが出た」と伝えると、即座にマイグレーションファイルを生成した。
原因は一瞬で特定された。
しかし、ダウンタイムは確実に発生した。
しんたろー:
Claude、お前さぁ。
アプリ側のコードだけ直して、DBの制約を無視するなよ。
僕の確認漏れだけど。全投稿が止まって血の気が引いた。
ここまで読んだあなたに
今なら無料で全機能をお試しいただけます。設定後は完全放置でプロ品質の投稿を毎日生成。
今日の数字
| 項目 | 数字 | 比較対象 |
|---|---|---|
| コミット数 | 29件 | 普段の週末の3倍 |
| 新機能 | 1件 | バグ修正に追われた結果 |
| バグ修正 | 1件 | 事実上の全書き換え |
| API費用 | $0 | 外注なら数十万円 |
| 開発時間 | 14時間 | 企業なら2-3週間。僕は14時間 |

個人開発の投稿システムで、エンタープライズ級の冪等性制御を実装した。
Fly.ioのインスタンス代をケチるために、ワーカーを1台に絞った。
インフラの札束で殴るのではなく、コードで解決した。
エンジニアの時給換算なら大赤字だ。
タイムゾーンとバランスの崩壊
「fix: 自動スケジュールのタイムゾーン処理をIntl.DateTimeFormatで明確化」
不明瞭なJSTからUTCへの変換を廃止した。
手動のオフセット計算は、サーバーのTZ設定変更で必ず破綻する。
Claudeは最初、単純な足し算引き算でタイムゾーンを計算するコードを出してきた。
「+9時間すればJSTになります」という安易な提案だ。
サマータイムやサーバーのローカル設定を完全に無視している。
Intl APIを使うことで、実行環境のOS設定に依存せず、常に期待したロケールで日時を扱える。
これもシステム安定化の一環だ。
「fix: プラットフォームバランスロジックを削除して春菊が表示されるように」
バランスを取ろうとして、逆に投稿がスキップされていた。
Threadsが4件続くと5件目がスキップされていた。
シンプルに時系列順で選択するように変更した。
複雑なロジックを捨てて、単純なソートに戻した。
まだ終わらない
画像付き投稿の優先順位付けロジックがまだ怪しい。
古い画像付き投稿が無視される現象は直したはずだった。
しかし、特定の条件下でまだ順番が狂う気がしている。
次からは画像処理の仕様を先に見る。たぶん。
よくある質問
Q: なぜ分散ロック(Redis等)を使わなかったの?
Redisを立てると月額コストが跳ね上がる。
Fly.ioのマシンを1台に制限すれば、追加コストゼロで物理的に競合を防げる。
インフラ代をケチる代わりに、スケーラビリティを捨てるトレードオフを選択した。
Q: 開発の再現手順はどのように管理しているの?
ステータス遷移を図に書いてからClaudeに渡す。
口頭指示だと、認識のズレがそのままバグになる。
視覚的な仕様書を挟んで、AIとの意思疎通の精度を物理的に上げた。
Q: DBの制約とアプリケーションの整合性はどう担保するの?
アプリ側のステータス追加時は、必ずDBのチェック制約も同時に更新する。
これを忘れると、アプリは正常に動いているのにDB書き込みで全滅する。
マイグレーションファイルとアプリのコードは常にセットで変更する。

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