実装と本番実測 · 2026年9月13日 · 濱田優貴 · English summary

APIを2台にした。
その次に直したのは
「受けすぎない仕組み」

追記:最大8台の自動起動・停止まで実装しました。
負荷試験で稼働数の増減を確認。通常API枠の32→64→64→32並列は計192件すべて成功しました。64並列の完了時間p95は6.594秒/5.940秒。以下には2台時点の検証と、その後の追加試験を分けて掲載します。

「これでどのくらい捌ける?」。サーバーを増やした後、この質問に設定値だけでは答えられませんでした。生成中の本数を数えるところから直し、専用アカウントで実際に測りました。

24 / 248並列ステージで生成成功
3.05秒同ステージの完了時間p95
19 / 19測定中のhealthが正常

gpt-4o-mini・短文・出力上限64トークン・専用の社内アカウント。OpenAI/Anthropic互換、通常応答/ストリーミングを混在させた小規模試験です。持続処理能力や全モデル共通の性能保証ではありません。

まず、更新するたびに受付が止まる構造を変えた

調査の出発点は、ときどき起きるAPIの接続切断でした。本番ログでは、1台構成のマシンをデプロイで置き換える間に接続拒否が発生していました。

そこで、終了時には受け付け済みのリクエストを最大110秒待ち、SSEには接続維持用の通知を送るようにしました。さらに、推論専用の2台目を常時起動し、1台ずつ更新する構成へ変更しました。

両台が担当するのは推論APIです。メール・定期精算などは既存の主担当機だけで実行します。管理画面なども主担当機へ転送します。全機能の二重化や、リージョン全体の障害対策を済ませたわけではありません。

「毎分120件」と「120件を同時に生成」は違う

毎分の利用上限だけでは、同時に進行中の生成数は抑えられません。返答に長くかかるほど、リクエストが積み重なります。上流が遅いときに代替経路を増やす処理も、接続数を押し上げます。

コードを確認すると、既存のベンチ枠はストリーミングの受付を返した時点で解放されていました。その後も生成は別タスクで続いています。また、上流接続を制限する関数は、実際の生成経路から呼ばれていませんでした。

受付が終わったら席を空ける、では早すぎる。
生成の仕事が終わるまで、その席を使用中として数える必要がありました。

両互換APIで、認証済みのアカウントIDごとに枠を共有するよう修正しました。キーを変えても、OpenAI形式とAnthropic形式を切り替えても、同じアカウントの生成として数えます。ストリーミングでは生成タスク自身が枠を保持します。

上限は、性能の宣伝値ではなく保護設定

対象1台あたり意味
推論API全体32生成両互換APIで共有
同じアカウント16生成APIキーを跨いで共有
ベンチ用8生成全体32枠の内数
実際の上流接続全体64接続複数候補への生成も数える
同じ上流種別32接続特定の上流への集中を抑える

この表は各台のアプリ内部の制限です。台数を増やしても、設定値を掛けた数字が実測の処理能力になるわけではありません。各台で独立して管理するため、片方へ偏れば合計の空きがあっても拒否されます。プラン別の毎分上限は、この同時実行制御とは別に適用されます。

共有残高のままでは、容量の測定にならなかった

最初は普段使っているアカウントで測りました。しかし、他の作業と共有している残高が途中で0になり、4並列の試験で402が返りました。これは容量不足を示す結果ではありません。

そこで、社内用として区別される専用アカウントを作り、残高を分離しました。短い固定プロンプトに識別番号を加え、出力を64トークンに制限。毎分上限にぶつからないよう、各ステージの間を65秒空けています。

本番で1 → 4 → 8 → 16並列を試した結果

クライアント並列数送信数生成成功成功分p50成功分p95
1222.358秒2.722秒
4882.189秒2.547秒
824242.345秒3.051秒
1624202.642秒2.956秒

時間はクライアント送信開始から本文受信完了まで。p50/p95はnearest-rank。8並列のストリーミング初回テキスト到着時間(TTFT)のp95は2.203秒。並列数はクライアントの最大同時送信数で、全リクエストの厳密な同時開始ではありません。

16並列の残り4件は、すべてベンチ枠の429でした。全体の推論上限ではなく、1台8件のベンチ枠に先に達した結果です。OpenAI/Anthropic両形式で、次の再試行目安が返りました。

HTTP 429 Too Many Requests
Retry-After: 2

error.code: bench_lane_full

測定中に接続エラーや5xxは観測せず、公開healthの19回の確認はすべて正常でした。成功54件は、2台からそれぞれ24件・30件返りました。負荷を下げた後の追加2件も、通常応答とストリーミングの両方で成功しました。

429は生成成功には数えていません。受けられない仕事を明示して、既に受けた仕事と通常の受付を守る。その動作を今回確認しました。

失敗を「成功したように」返さないことも確認した

ローカルでは実際の認証付きHTTPハンドラーを通し、SSE開始後も枠が埋まっていること、別キー・別形式から上限を迂回できないことをテストしています。

さらに上流を失敗させるテストで、エラーが返り、成功の終了イベントを送らず、その失敗リクエストの残高や利用記録を更新しないこと、使っていた枠が解放されることを確認しました。本番で意図的に上流障害を起こした検証ではありません。

追加改善:満杯になる前に、次の台を起こす

その後、Fly標準の自動起動・停止を有効にしました。主担当1台は常時稼働。推論専用機は最大7台を用意し、空いているときは停止させ、需要に応じて起動します。常時稼働の最小構成は主担当+推論機の2台、用意するプールの上限は合計8台です。

HTTPリクエストの同時数が各台4件を超えると、別の台への振り分けや予備機の起動を促します。Fly側のhard limitは24件/台です。これも性能保証ではなく、アプリの32生成枠より手前で働く受付設定です。初回配備では新規機のcreated状態を検証が早すぎて異常と判定したため、初回起動を待って確認するよう修正しました。

また、認証のたびに待っていたAPIキーの最終利用時刻のDB更新を、直近60秒に更新済みなら省略するようにしました。認証結果をキャッシュしているわけではなく、失効確認は毎回行います。書き込みを省いてもキーの失効が即座に反映されることをテストしています。

同じ専用アカウントで、認証付きプロフィール取得を前後それぞれ10回測りました。中央値は623ms→279msでした。ただし別時刻で、通信やDBの変動も含む小標本です。生成全体が同じ割合で速くなる、という意味ではありません。

4アカウントで64並列まで追加測定

追加予算5,000円の範囲で、残高を分離した社内アカウント4つを使用。モデル・64トークン上限は前と同じで、通常応答とSSE、両互換形式を混在させました。まずベンチ枠を使い、各ステージを65秒空けました。

最大並列送信生成成功ベンチ枠429成功分p95
8161602.706秒
16322753.383秒
326453113.822秒
646438265.823秒

この試験中、稼働マシン数は3→4→5台へ増え、追加機からも生成応答が返りました。4ステージのhealth確認11回はすべて正常。接続エラー・5xx・402は観測しませんでした。64並列を全件処理したわけではありません。短いバーストでは、新しい台が立ち上がるまでに1台8件のベンチ枠が先に埋まることも確認できました。

負荷低下後には、マシン状態を60秒間隔で確認しました。観測開始時の5台から、122秒後に4台、244秒後に3台、306秒後に2台へ戻りました。主担当1台と推論機1台は稼働を続けています。手動停止ではなく、自動停止による減少です。

最後に、ベンチ用ヘッダーを付けない通常API枠で16並列を1回試しました。一般のアカウント制限と毎分上限は有効なままです。結果は16件すべて成功、p95 2.837秒、health正常でした。ベンチ枠の拒否数を、そのまま通常APIの限界と解釈できないことが分かります。

追加試験は176件+通常枠16件。クレジット付与200円相当+送信192件×保守的予約25円=5,000円の予算枠で終了。これは実請求額ではありません。東京の4 shared CPU/2GBは公式料金で約$16.71/30日・台。8台が常時動けば約$133.68/30日、停止中のrootfs・通信・DB・LLMは別料金です。

ここから先は、まだ測っていない

さらに通常枠を32・64並列で測定しました。追加承認の予算5,000円以内で、同じ4アカウントを使い、各ステージの送信開始をバリアで揃えました。短い固定プロンプト・出力上限64トークン、ステージ間65秒、再送なしです。

通常枠の並列数成功/送信完了p50完了p95SSE初回テキストp95
32(1回目)32 / 323.460秒4.001秒2.606秒
64(1回目)64 / 644.356秒6.594秒3.208秒
64(2回目)64 / 643.510秒5.940秒2.616秒
32(2回目)32 / 322.477秒3.134秒1.898秒

計192件すべて生成成功。測定中のhealth確認11回も正常でした。送信192件×25円=4,800円を保守的に予約し、追加枠の残り200円は使わず終了。並列数はクライアント側で同時送信する数であり、同じ瞬間に上流が生成している本数を直接測ったものではありません。

持続負荷と長文は、次の検証課題

「通常枠64並列で2回とも全件成功」を、毎分・毎時の保証値へ外挿はしません。今回言えるのは、短文生成の実測、自動的な稼働機の増減、ベンチ枠での明示的な拒否と受付の回復を確認したことです。継続的な高負荷でもこの成功率を保てるかは、コールドスタートを含む持続負荷を測り、常時稼働台数と起動閾値をさらに評価する必要があります。

測定条件と8並列ステージの生値

2026年9月13日17:47 JST開始。手元のMacから公開HTTPSへ送信。Python標準ライブラリ、1つの社内Freeプランアカウント、ベンチ用ヘッダー付き。モデルはgpt-4o-mini、要求・応答モデル名の一致と非空本文、SSE終了を成功条件として検査。失敗時の自動再送なし。HTTP200だけでは成功に数えていません。

プロンプト:Test {番号}. Explain why the sky is blue in three short sentences.

完了時間(秒、送信順)
3.051, 2.752, 3.051, 2.920, 2.919, 2.365,
2.820, 2.918, 2.345, 1.921, 1.885, 1.870,
2.288, 2.288, 1.862, 2.380, 2.734, 3.035,
2.304, 2.617, 2.162, 1.942, 1.889, 2.211

制御本体:8d45b8d。GitHub Actions経由で配備し、両台の環境変数とhealthを確認。測定スクリプト:scripts/measure_inference_capacity.py、scripts/measure_dedicated_capacity.py。予算上限2,000円に対し、過去試験・クレジット付与・全送信の保守的予約額を合わせて2,000円以内で停止。これは実請求額を意味しません。

速く返すことと、受けすぎないことはセット。
台数や上限の数字を増やすだけでなく、生成中の仕事を正しく数え、満杯を伝え、再び使えることまで確かめる。今回の改善は、その土台です。

利用方法はAPIドキュメントへ。429を受けた場合は、Retry-Afterを尊重し、少しランダムな待ち時間を加えてから再試行してください。接続が切れたPOSTを結果不明のまま繰り返すと、二重生成につながる可能性があります。

Measured capacity, not a capacity promise

We added a second always-on inference machine and fixed admission control so a slot remains occupied until generation finishes, including streaming. Both compatible APIs share per-account admission; actual upstream calls are bounded too.

With a dedicated internal account, gpt-4o-mini and a 64-token output cap, the 8-concurrency stage completed all 24 requests. Completion p95 was 3.051 seconds. At concurrency 16, 20 of 24 requests succeeded; four were explicitly rejected by the benchmark lane with HTTP 429 and Retry-After: 2. All 19 health probes passed. Two follow-up requests succeeded after load dropped.

We then enabled a bounded pool of eight machines. In a four-account test, running machines increased from three to five automatically; after load dropped, they returned from five to two within 306 seconds of observation. A separate normal-lane test completed all 16 requests at concurrency 16, with p95 of 2.837 seconds. Benchmark-lane tests reached concurrency 64 but rejected 26 of 64 requests with explicit 429 responses.

A further normal-lane test synchronized request starts at concurrency 32, 64, 64 and 32: all 192 requests completed successfully. The two 64-concurrency stages had completion p95 of 6.594 and 5.940 seconds respectively. All 11 health probes passed. These results include client, proxy, database and upstream time.

These are short burst measurements, not sustained throughput or a 64-concurrent-generation guarantee. Long contexts, reasoning models, full-fleet saturation and failure during deployment remain unmeasured. The configured limits are protective ceilings, applied independently on each machine.