個人開発でAI画像編集サービスを作っています。
最初は「画像をアップロード → AI APIを呼ぶ → 結果を返す」というシンプルな構成を想定していました。
ただ、実際にサービスとして提供することを考えると、画像生成そのものよりも、その周辺の処理が意外と難しいことに気づきました。
特に気になったのは次の4点です。
- AI APIの処理時間が長い場合、HTTPリクエストをどう扱うか
- Webhookが重複して届いた場合、どう処理するか
- 生成に失敗した場合、消費したクレジットをどう返すか
- ユーザーが画面を閉じた場合でも、処理を完了できるか
今回は、Next.jsを使ったAI画像編集サービスで、これらの問題にどう向き合うかを整理してみます。
※この記事は設計上の考え方をまとめたものです。コードやステータス名は説明用に簡略化しています。
1. 画像生成を同期HTTPリクエストで待たない
最も単純な実装は、API Routeで画像生成APIを呼び、結果が返るまで待つ方法です。
しかし、画像生成は数秒で終わるとは限りません。混雑状況やモデルによっては、さらに時間がかかることもあります。
この方式にはいくつか問題があります。
- サーバーレス環境のタイムアウト
- ブラウザの再読み込みや通信切断
- ユーザーが処理状況を確認しにくい
- リトライ時の重複リクエスト
そこで、生成を非同期ジョブとして扱う設計を考えました。
大まかな流れは次のとおりです。
- ユーザーが画像とプロンプトを送信する
- サーバーが生成ジョブをDBに作成する
- AIプロバイダーに生成を依頼する
- フロントエンドにはジョブIDを返す
- プロバイダーからWebhookで結果を受け取る
- DBのジョブ状態を更新する
- フロントエンドが完了状態を取得して結果を表示する
ポイントは、ブラウザとの接続が切れても、ジョブの状態がサーバー側に残ることです。
画面を再読み込みしても、ジョブIDを使って状況を取得できます。
2. ジョブの状態を明確にする
非同期処理では、ジョブの状態管理が重要になります。
例えば、次のような状態を考えられます。
-
created:ジョブを作成した -
processing:AIプロバイダーで処理中 -
completed:生成に成功した -
failed:生成に失敗した
大切なのは、状態をただ保存するだけでなく、許可する状態遷移を決めておくことです。
例えば、completedになったジョブに、後から届いた失敗通知をそのまま適用してしまうと、正常に生成されたのにクレジットまで返金される可能性があります。
そのため、完了済みのジョブへの重複通知は、原則として状態を変更しないようにします。
また、処理中のまま長時間更新されないジョブを検出する仕組みも必要になります。
3. クレジットはいつ消費するべきか
AI APIは呼び出しごとにコストが発生するため、無料トライアルがあるサービスではクレジット管理も重要です。
私が作っているサービスでは、ユーザーが無料クレジットで画像編集を試せるようにしています。
ここで悩むのが、クレジットを消費するタイミングです。
生成完了後にだけクレジットを消費すると、残高が少ないユーザーが複数の生成を同時実行できてしまう可能性があります。
一方で、生成開始時に消費する場合は、失敗時の返金処理が必要になります。
そこで、次のようなライフサイクルを考えます。
- 生成開始時に必要なクレジットを確保する
- AIプロバイダーにリクエストする
- 成功時は消費を確定する
- 失敗時は確保したクレジットを戻す
ここでは、残高の更新とジョブの作成を別々に処理しないことが重要です。
例えば、残高だけ減らした直後にサーバーが落ちると、対応するジョブが存在しない状態になります。
可能であれば、DBトランザクションでまとめて処理します。
4. Webhookは必ず重複する可能性がある
Webhookを導入すると、処理完了をサーバー側で受け取れるようになります。
ただし、Webhookは必ず一度だけ届くとは限りません。
例えば、次のケースが考えられます。
- プロバイダーが成功通知を送信する
- サーバーがDBの更新を完了する
- WebhookへのHTTPレスポンスがタイムアウトする
- プロバイダーが同じ通知を再送する
このとき、通知を受け取るたびにクレジットを加減算してしまうのは危険です。
必要なのは冪等性です。
同じジョブの同じ結果を何度受け取っても、最終的なDBの状態とクレジット残高が変わらないようにします。
具体的には、次のような対策が考えられます。
- Webhookの署名や認証情報を検証する
- プロバイダーのジョブIDと自サービスのジョブIDを対応付ける
- 完了済みジョブの重複更新を防ぐ
- クレジット返金をジョブごとに一度だけ実行する
- ジョブ更新と返金処理を同一トランザクションで扱う
特に、ジョブ状態の更新だけでなく、クレジットの変更も冪等にすることが重要だと思います。
5. Webhookだけに依存しない
もう一つ考えておきたいのが、Webhook自体が届かなかったケースです。
プロバイダー側では生成が完了しているのに、自分のDBではprocessingのまま残ってしまう可能性があります。
対策としては、一定時間更新されていないジョブを再確認する仕組みが考えられます。
例えば、定期的な処理で古いprocessingジョブを検出し、プロバイダーのジョブ状態取得APIがあれば問い合わせます。
- 成功していれば結果を保存する
- 失敗が確定していれば失敗処理を行う
- 状態が不明なら、すぐに返金せず再確認する
ここで気をつけたいのは、タイムアウトと生成失敗は同じではないことです。
自分のサーバーからのリクエストがタイムアウトしても、プロバイダー側では生成が続いているかもしれません。
そのため、単純にタイムアウトしただけで失敗扱いにすると、後から成功通知が届いて状態が矛盾する可能性があります。
6. フロントエンド側は状態を取得して表示する
フロントエンドでは、生成開始APIから返されたジョブIDを使って、処理状況を確認します。
最初の実装では、一定間隔のポーリングでも十分だと考えています。
例えば数秒おきにジョブ状態を取得し、completedまたはfailedになったらポーリングを止めます。
Webhookはプロバイダーからバックエンドへの通知、ポーリングはフロントエンドからバックエンドへの状態確認です。
役割を分けることで、ユーザーがブラウザを閉じてもバックエンドの処理を継続できます。
また、失敗した場合は単にエラーを表示するだけでなく、クレジットが返却されたかどうかもユーザーに分かるようにしておきたいです。
7. 実装前に確認しておきたいケース
最後に、最低限確認したいケースをまとめます。
| ケース | 期待する動作 |
|---|---|
| 生成成功 | 結果を保存し、クレジット消費を確定 |
| 生成失敗 | 失敗状態を保存し、必要なクレジットを返却 |
| Webhookの重複 | 結果や残高を二重更新しない |
| Webhookの遅延 | ジョブ状態を後から正しく更新できる |
| ブラウザを閉じる | サーバー側のジョブが継続する |
| 同時生成 | 利用可能残高を超えて消費できない |
| APIタイムアウト | プロバイダーの実際の状態を確認する |
| DB更新失敗 | 再試行しても状態が矛盾しない |
正常系だけを確認していると見落としやすい部分ですが、課金が関係するサービスでは、こうした異常系の確認が重要だと感じています。
まとめ
AI画像編集サービスを作り始めたときは、どの画像生成モデルを使うか、どれだけ高品質な画像を生成できるかに意識が向いていました。
しかし、サービスとして運用することを考えると、非同期処理、ジョブ管理、Webhook、クレジット補償などの設計も同じくらい重要です。
特に意識したいのは次の3つです。
- ジョブの状態をDBで管理し、ブラウザの接続状態に依存しない
- Webhookやクレジット処理を冪等にする
- タイムアウトと実際の生成失敗を区別する
小規模な個人開発でも、最初からすべてを複雑にする必要はありません。ただし、お金やクレジットが関わる部分だけは、早めに設計しておいたほうがよさそうです。
今回の設計を考えるきっかけになったのは、個人開発しているAI Photo Editorです。画像をアップロードし、テキストで編集内容を指定できるWebサービスです。
同じようにAI APIを使ったサービスを開発している方の参考になれば幸いです。
