ガイド

Webアプリでバックグラウンドジョブを動かす方法:キュー、スケジューラー、cron

Webアプリでバックグラウンドジョブを動かすには、まず作業を2種類に分けます。トリガー型のジョブ(ようこそメールの送信、アップロードの処理など)は、ジョブキューに保存できます。多くの場合、データベースのテーブルです。ワーカーが再試行を含めて処理します。定期実行ジョブ(毎晩のレポート、15分ごとのリマインダーなど)は、決まった時刻に起動する仕組みが必要です。ホストのcron機能か、アプリ内の保護されたエンドポイントを呼び出す外部スケジューラーです。ジョブは必ず2回実行されても安全にし、失敗は記録し、スリープしたり再起動したりするサーバー内のタイマーには頼らないでください。

このガイドでは、それぞれの選択肢、どの場合に向いているか、ゼロにスケールするホストを含め、AIアプリビルダーで設定する方法を説明します。

バックグラウンドジョブに当たるもの

遅いもの、不安定なもの、ユーザーの即時の応答に必要ないものすべてです。

  • メール、通知、領収書の送信
  • 画像のリサイズ、動画のトランスコード、PDFの生成
  • 遅いサードパーティAPIの呼び出し(長時間かかるAIモデルの処理を含む)
  • 他のシステムからのデータの取り込みや同期
  • 夜間のクリーンアップ、レポート、リマインダー

これらをリクエストの中で行うと、ページが遅くなり、壊れやすくなります。メールプロバイダーが停止していれば、ユーザーのサインアップが失敗してしまいます。バックグラウンドに移すと、ユーザーは即座に応答を受け取り、ジョブは再試行できます。

選択肢

選択肢仕組みメリットデメリット
同じプロセス内での実行(fire-and-forget)応答を返し、サーバーで処理を続ける設定が不要サーバーが停止すると失われる。再試行がない
プロセス内のタイマー(setInterval、setTimeout)実行中のサーバー内のタイマー非常に簡単スリープ中は動かない。複数のコピーがあると重複する
データベースベースのキュージョブを行として保存し、ワーカーが取り出して実行する既にあるデータベースを使える。永続的で、確認しやすい再試行やロックは自分で作る。ワーカーを動かす仕組みが必要
専用のキュー(Redisベースまたはマネージド)キューサービスとワーカー再試行やスケジューリングの機能が成熟している運用・支払いするサービスがもう1つ増える
ホスト型のジョブやワークフローサービスサードパーティが、再試行付きであなたのエンドポイントを呼び出すスケジュールと再試行を任せられるベンダーへの依存。アカウントとキューが増える
エンドポイントを呼び出す外部スケジューラー別の場所のタイマーが、スケジュールどおりにHTTPリクエストを送る任意のホストで動く。ゼロスケールにも対応する最短間隔や遅延がある。エンドポイントを保護する必要がある
ホストの組み込みcronプラットフォームが、スケジュールどおりにコマンドを実行する対応していれば簡単すべてのホストが提供しているわけではない

唯一の正解はありません。小さなアプリなら、データベースのキューと外部スケジューラーの組み合わせで十分なことがよくあります。

ゼロにスケールするホストで状況が変わる理由

多くの最近のホストは、誰も使っていないときにアプリをスリープさせ、次のリクエストで起動します。これは費用を節約しますが、スリープ中のサーバーはコードを実行しません。タイマーは発火せず、ワーカーもポーリングしません。このようなホストでは、定期的な処理は受信したリクエストをきっかけに開始する必要があり、その最も簡単なきっかけが外部スケジューラーです。呼び出しのたびにアプリ(多くの場合データベースも)が起動するため、1分ごとの呼び出しは15分ごとより費用がかかります。必要を満たす中で、最も遅い間隔を選んでください。

パターン1:データベースのジョブキュー

トリガー型のジョブの場合は次のとおりです。

  1. jobsテーブルを作る。項目は、種類、ペイロード(JSON)、ステータス(pending、running、done、failed)、試行回数、実行可能時刻(run_after)、最後のエラー、タイムスタンプ。
  2. 何かが起きたら、その変更を引き起こしたのと同じトランザクション内でジョブの行を挿入します。こうすると、両方が一緒に成功するか、一緒に失敗します。
  3. ワーカーが実行期限の来たジョブを取り出し、runningに変更し、処理を行い、doneに変更します。
  4. 失敗したら、試行回数を増やし、run_afterを後ろにずらし(たとえば1分、5分、30分後)、エラーを保存します。上限に達したらfailedにして、誰かに通知します。

複数のワーカーが同時に動く可能性があるなら、ジョブは安全に取得されなければなりません。Postgresでは、SELECT ... FOR UPDATE SKIP LOCKEDを使うと、各ワーカーが衝突せずに異なる行を取得できます。

ワーカーは何で動かすのでしょうか。常時稼働のサーバーであれば、別プロセスのループです。ゼロにスケールするホストでは、パターン2の定期実行エンドポイントが、呼び出しごとに期限の来たジョブをまとめて処理できます。

パターン2:アプリを呼び出す外部スケジューラー

定期実行ジョブの場合は次のとおりです。

  1. POST /api/cron/send-remindersのようなエンドポイントを追加する。
  2. 保護する。 ヘッダーに秘密のトークンを要求し、そのトークンは両側でシークレットとして保存します。それ以外のリクエストは拒否してください。
  3. 冪等にする。 同じ分に2回実行しても、リマインダーが2通送られてはいけません。送信済みの記録を残してください。
  4. 各呼び出しは短く保つ。1回で一定数(たとえば100件)をまとめて処理し、残りは次の呼び出しで処理させます。
  5. スケジューラーをそのエンドポイントに向ける。

スケジューラーの選択肢は次のとおりです。

  • GitHub Actionsのスケジュールされたワークフロー。 GitHubの利用上限内であれば無料です。GitHubのドキュメントによると、2026年9月時点で、最短の間隔は5分に1回です。混雑時(特に毎時の始まり)には実行が遅れることがあります。スケジュールはデフォルトでUTCを使います。公開リポジトリのスケジュールされたワークフローは、リポジトリに60日間活動がないと無効になります。
  • ホスト型のcronサービス。 URLを決まった時刻に呼び出すことだけを目的としたサービスが多くあります。依存する前に、信頼性、最短間隔、アラート機能を確認してください。
  • クラウドプロバイダーのスケジューラー。 大手クラウドプロバイダーをすでに使っているなら、HTTPSのエンドポイントを呼び出せるマネージドのスケジューラーがあるはずです。
  • ホスト型のジョブやワークフローサービス。 スケジューリング、キュー、再試行を組み合わせ、各ステップであなたのエンドポイントを呼び出します。料金と上限は、各ベンダーのサイトで比較してください。

パターン3:プロバイダーに任せる

バックグラウンドジョブが、まったく必要ない場合もあります。

  • 決済。 Stripeはサブスクリプションの支払い失敗を再試行し、Webhookを送ります。こちらはポーリングするのではなく、それに反応すればよいだけです。Webhookとはをご覧ください。
  • メール。 メッセージを渡せば、プロバイダーが配信をキュー管理し、再試行します。
  • 大量のAI処理。 OpenAIとAnthropicは、緊急ではないリクエストを送信して後から結果を回収する、バッチAPIを提供しています。

AIビルダーへの指示例

キューの場合:

データベース内のjobsテーブル(種類、ペイロードJSON、ステータス、試行回数、run_after、最後のエラー)を使って、バックグラウンドジョブのキューを追加してください。ユーザーがサインアップしたら、その場でメールを送る代わりに、同じトランザクション内でsend_welcome_emailジョブを挿入してください。SELECT ... FOR UPDATE SKIP LOCKEDで期限の来たジョブを取得してから実行するprocessJobs(limit)関数を追加し、失敗は1分後、5分後、30分後に再試行して、4回失敗したらfailedにしてください。

定期的なきっかけの場合:

POST /api/cron/tickを追加してください。秘密のCRON_SECRETから得たベアラートークンで保護し、トークンのないリクエストはすべて拒否してください。各呼び出しでprocessJobs(100)を実行し、今後24時間以内に送るべきで、まだ送っていない予約リマインダーを送ってください。送ったリマインダーは記録して、二重に送られないようにしてください。実行結果のJSONの要約を返してください。そのうえで、このエンドポイントを15分ごとに呼び出すGitHub Actionsのワークフローを書き、URLとトークンはリポジトリのシークレットから読み込むようにしてください。

よくある間違い

  • setIntervalに頼る。 サーバーがスリープしたり再起動したりすると、静かに止まります。また、2つのコピーが動くと二重に実行されます。
  • 保護されていないcronエンドポイント。 見つけた誰もがジョブを起動でき、メールの請求額を増やされてしまいます。
  • 冪等でないジョブ。 再試行や、スケジューラーの重複呼び出しは必ず起きます。それを前提に設計してください。
  • 失敗が見えない。 1週間静かに失敗し続けるジョブは、大きな声で失敗するジョブより悪いものです。エラーを記録し、繰り返しの失敗はアラートにしましょう。
  • 1回のリクエストで長い処理を行う。 リクエストはタイムアウトします。バッチに分けて処理してください。
  • 頻繁にスケジュールしすぎる。 ゼロスケールのホストでは、呼び出しのたびにアプリが起動し、課金されます。間隔は実際の必要に合わせてください。
  • タイムゾーン。 「午前9時」は、スケジューラー(多くの場合UTC)とユーザーで意味が異なります。時刻はUTCで保存し、ユーザーごとに変換してください。
  • ワークフローファイルに秘密情報を書く。 トークンはスケジューラーのシークレットストアに保管してください。APIキーを安全に管理する方法をご覧ください。

チェックリスト

  • 遅い処理はユーザーのリクエストから切り離されている
  • ジョブはメモリだけでなく、永続的に保存されている
  • 再試行は間隔を広げながら行われ、上限がある
  • どのジョブも、2回実行されても安全である
  • 定期実行のエンドポイントは秘密のトークンで保護されている
  • 各実行は、上限のあるバッチを処理する
  • 失敗したジョブは見えるようになっており、アラートがある
  • ホスティング費用を考慮して、実行間隔を決めている
  • 時刻はUTCで保存している

Mythexでのバックグラウンドジョブ

Mythexのドキュメントでは、定期実行やcronジョブについては説明されていません。また、公開したアプリとそのデータベースは、アイドル時にスリープし、次のリクエストで起動します(ホスティングの上限をご覧ください)。そのため、アプリ内のタイマーには頼らないでください。機能するのは、上記の方法です。エージェントに、ジョブのテーブルと、トークンで保護されたエンドポイントを作るよう依頼し、外部スケジューラーを公開URLに向けてください。トークンはプロジェクトのシークレットとして保存し、呼び出しのたびにアプリとデータベースが起動している時間分のクレジットを消費することを覚えておいてください。適切な間隔を選んでください。アプリでこのためのAPIバックエンドが必要なら、REST APIバックエンドのレシピが出発点になります。エンドポイントの設計については、REST APIの作り方で説明しています。製品の中核が常時稼働するワーカーやボットなら、常駐プロセス向けのホストのほうが適しているかもしれません。

よくある質問

バックグラウンドジョブとは何ですか?

バックグラウンドジョブとは、ユーザーが待っているリクエストの外側で、アプリが行う処理のことです。たとえば、メールの送信、画像のリサイズ、レポートの作成、データの同期などです。ユーザーは素早く応答を受け取り、ジョブは別に実行され、失敗した場合は再試行できます。

ホストにcronがない場合、定期的なタスクはどう実行すればよいですか?

処理を行う保護されたエンドポイントを追加し、外部のスケジューラーがそれを決まった時刻に呼び出すようにします。選択肢には、スケジュールされたGitHub Actionsのワークフロー、ホスト型のcronサービス、クラウドプロバイダーのスケジューラーがあります。エンドポイントは秘密のトークンで保護してください。

定期実行ジョブにsetIntervalやsetTimeoutを使ってもよいですか?

小さく重要でないタスクを、常に動いているサーバーで行う場合に限ります。サーバーが再起動したり、ゼロにスケールしたり、複数のコピーが動いたりすると、タイマーは失われるか、複数回実行されてしまいます。定期的な処理はデータベースに保存するか、スケジューラーを使ってください。

Mythexは cronジョブに対応していますか?

Mythexのドキュメントでは、定期実行やcronジョブについては説明されていません。また、公開したアプリはアイドル時にスリープします。確実な方法は、外部のスケジューラーがアプリ内の保護されたエンドポイントを呼び出すことです。そうすればアプリが起動し、処理が実行されます。

Mythex · Pricing · AIでなんでも作ろう · ウェブサイト・アプリのテンプレート · Mythexを比較 · AIアプリ開発用語集 · AIアプリ開発の無料ツール · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Deutsch Ελληνικά Español Español (España) Eesti فارسی Suomi Français Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska Italiano ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål Nederlands ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Română Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)