launchd

launchd × BlueSky自動エンゲージ、429エラーと日次上限に詰まった記録

Ichinose Taito — MBTI × 心理学 × AI

心理学で、
人間関係を
ちょっとラクにする。

MBTIとアドラー心理学を軸に、
自分と他者を理解するヒントを発信しています。

記事を読む ›
ちのくん
ちのくん
— こんな活動をしています —
launchd × BlueSky自動エンゲージ、429エラーと日次上限に詰まった記録

十分な実データがそろった。記事を書く。


何が起きたか

launchd で bluesky_engagement.py を1日3回自動実行するようにしてから2週間が経ったころ、フォロー数の伸びが急に止まった。ログを見ると 📵 1日上限(100件)到達 が朝イチのセッションで出ていて、残り2セッションは完全にフォロー処理がスキップされていた。「これは設定ミスだろう」と思って数字を緩めようとしたら、今度はBlueSkyのAPIから 429 が返ってくるようになった。制限に当たった、というよりは自分で制限を無視して突き進んでいた。

環境

  • macOS Sequoia / MacBook Pro M5 32GB
  • Python 3.12(Homebrew)
  • スクリプト: 01_Scripts/sns/bluesky_engagement.py
  • 自動実行: launchd com.ichinosei.bluesky-post.plistbluesky_launcher.sh
  • ログ: 04_Config/logs/bluesky_post_launchd.log
  • 状態管理: 04_Config/bluesky_engagement_log.json

詰まったポイント

最初に設定していた上限値はこうだった。

FOLLOW_PER_SESSION = 30   # 1セッションあたりフォロー上限
FOLLOW_PER_DAY     = 100  # 1日フォロー上限

1日3セッション × 30件 = 90件、余裕を持って100件という計算だった。問題はこの「余裕」が甘すぎたこと。BlueSkyのフォロー制限は公開されていないけど、短時間に大量フォローするアカウントはスパム扱いされやすい——これは後で気づいた。

具体的に出たエラー:

requests.exceptions.HTTPError: 429 Client Error: Too Many Requests

bluesky_launcher.sh のガードファイル(/tmp/bluesky_launcher_guard_$(date +%Y%m%d_%H).lock)が効いていてセッションの二重起動は防げていたのに、1セッション内の処理速度が速すぎた。タグ検索 → いいね → フォローを1投稿ごとに0.5秒インターバルで回していたので、短時間に集中アクセスが発生していた。

もう一つの詰まりポイントは followed リストの肥大化。フォロー済みDIDを全件ベタに配列へ積んでいて、1万件を超えたタイミングで set() に圧縮する処理は書いてあったが、それ以前の線形探索で in チェックが遅くなっていた。セッション終盤になるほど1投稿あたりの処理が重くなる、という現象が出ていた。

解決までの手順

ステップ1: ログで状況を把握する

tail -100 04_Config/logs/bluesky_post_launchd.log | grep -E "429|上限|フォロー"

これで「1日上限到達」がいつのセッションで出ているか、429 がどのタグ処理中に出ているかが一目でわかった。

ステップ2: セッション上限と日次上限を下げる

429が出た原因はスループットではなく累積量だったので、まず数字を下げた。

FOLLOW_PER_SESSION = 15   # 30 → 15 に変更
FOLLOW_PER_DAY     = 80   # 100 → 80 に変更(BAN安全圏として設定)

「80件で足りるのか」と最初は思ったが、1日80フォロー × 週5日でも毎週400件ペース。フォロワー増加の観点では十分だった。

ステップ3: アンフォローログとの同期を追加する

bluesky_unfollow_log.json に記録したアンフォロー済みDIDが、followed リストに入っていない場合は再フォローされてしまう、という欠陥に気づいた。セッション開始時にアンフォローログを読み込んで followed に突っ込む処理を入れた。

_unfollow_log_path = CFG / "bluesky_unfollow_log.json"
_unfollowed_dids: set = set()
if _unfollow_log_path.exists():
    _ul = json.loads(_unfollow_log_path.read_text(encoding="utf-8"))
    _unfollowed_dids = {e["did"] for e in _ul.get("unfollowed", []) if "did" in e}
    _new_dids = _unfollowed_dids - set(log.get("followed", []))
    if _new_dids:
        log.setdefault("followed", []).extend(list(_new_dids))

ステップ4: 日次カウントを daily_follows に分離する

これが一番効いた変更。それまで followed リストの長さで今日のフォロー数を推測していたが、daily_follows という日付をキーにした辞書に分離した。

def _today_follow_count(log: dict) -> int:
    today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    return log.get("daily_follows", {}).get(today, 0)

def _increment_follow_count(log: dict, did: str = ""):
    today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    log.setdefault("daily_follows", {})[today] = _today_follow_count(log) + 1
    if did:
        log.setdefault("follow_dates", {})[did] = datetime.now(timezone.utc).isoformat()

これで日をまたいだらカウントが自動リセットされるようになった。以前は followed の全件を舐めて今日フォローした件数を数えていたので、ここだけで体感できるレベルで速くなった。

コード/設定の抜粋

launchd の plist は時刻指定でシェルを起動するシンプルな構成。

<plist version="1.0">
<dict>
  <key>Label</key><string>com.ichinosei.bluesky-post</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Users/ichinosetaito/Documents/AI_Automation_Base/05_Launchers/_自動実行用(launchd)/bluesky_launcher.sh</string>
  </array>
  <key>StandardOutPath</key>
  <string>/Users/ichinosetaito/Documents/AI_Automation_Base/04_Config/logs/bluesky_post_launchd.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/ichinosetaito/Documents/AI_Automation_Base/04_Config/logs/bluesky_post_launchd.log</string>
</dict>
</plist>

bluesky_launcher.sh はガードファイルで二重起動を防いでいる。

GUARD_FILE="/tmp/bluesky_launcher_guard_$(date +%Y%m%d_%H).lock"
if [ -f "$GUARD_FILE" ]; then
  echo "[$(date)] already running, skip" >> "$LOG"
  exit 0
fi
touch "$GUARD_FILE"

時間単位のガードなので、同一時間帯に複数回トリガーされても1回しか実行されない。

試してわかったこと

FOLLOW_PER_DAY = 100 という数字は「BAN安全圏」として設定したつもりだったが、1日3セッションで分散しても1セッション30件は短時間に集中しすぎた。結果として「安全のため設けた上限がそのまま原因になった」というオチ。

もう一つ、アンフォローと再フォローの管理は設計当初から考慮しないと後から収拾がつかなくなる。followed リストと unfollow_log を別ファイルで管理しておきながら、起動時に同期しない作りにしていたのは明らかな設計ミスだった。ファイルが増えるほど「どこを正とするか」を最初に決めておく必要がある。

BSkyのAPI制限は公式ドキュメントに数字が出ていない(少なくとも僕が調べた範囲では)。「429が出ない最大値」を経験則で探るしかないが、1日80件 / 1セッション15件は今のところ問題なく動いている。もしまた429が出たらさらに絞る。

まとめ

  • フォロー上限は「1日何件」よりも「短時間に何件が集中するか」が本質的な問題
  • アンフォローログとfollowedリストは起動時に必ず同期する設計にしておく
  • 日次カウントは専用フィールドに分離しないと、リスト増大で処理が重くなる