Apexの@futureで書く非同期処理の制約と使い分け
同期処理は呼び出した側の処理が終わるまで画面を止めます。@futureを付けると、処理を後回しにして呼び出し元をすぐ返せます。書き方と、はまりやすい制約を説明します。
同期処理と非同期処理の違い
Apexのメソッドは、何もしなければ同期で動きます。呼び出した側は、そのメソッドの処理が終わるまで次に進めません。重い処理を同期のまま呼ぶと、画面やAPIの応答がその分だけ遅くなります。
@futureを付けたメソッドは非同期になります。呼び出した瞬間にキューへ積まれ、呼び出し元はすぐに処理を続けられます。実際の処理は、システムのリソースが空いたタイミングで実行されます。いつ実行されるかは保証されません。
@futureの書き方
メソッド宣言の直前に@futureを付けるだけです。ただし、戻り値と引数には制約があります。
public with sharing class AccountBulkCreator { public static void createAccounts(List<String> names) { List<Account> accounts = new List<Account>(); for (String name : names) { accounts.add(new Account(Name = name)); } insert accounts; } @future public static void createAccountsAsync(List<String> names) { createAccounts(names); } }
戻り値はvoidだけです。引数にはプリミティブ型かそのコレクションしか使えません。
同期版のcreateAccountsを呼ぶと、呼び出し側はinsertが終わるまで待たされます。createAccountsAsyncを呼んだ場合は、呼び出し側はすぐ戻り、Accountの作成は後で行われます。画面やAPIのレスポンスを速くしたいときに使う効果は、ここで出ます。
⚠️ DMLはループの中で呼びません。上のコードは、Accountのリストをループの外で1回だけinsertしています。名前の件数が増えても、DMLの発行回数は変わりません。
sObjectを引数にできない理由
@futureの引数は、プリミティブ型・プリミティブ型の配列・プリミティブ型のコレクションに限られます。sObjectそのものは渡せません。Accountのリストを直接渡すことはできず、List<String>やList<Id>にする必要があります。
理由は、キューに積まれてから実際に実行されるまでの間に、渡したレコードの内容が変わっている可能性があるためです。公式ドキュメントは、IDだけを渡して、実行時にあらためてクエリで取得することをすすめています。
public with sharing class AccountFollowUpAsync { @future public static void notify(List<Id> accountIds) { List<Account> accounts = [ SELECT Id, Name FROM Account WHERE Id IN :accountIds ]; for (Account acc : accounts) { System.debug('非同期処理: ' + acc.Name); } } }
トリガはレコードではなくIDのリストを渡します。実行時にSOQLで最新の値を取り直します。
trigger AccountTrigger on Account (after insert) { List<Id> newIds = new List<Id>(); for (Account acc : Trigger.new) { newIds.add(acc.Id); } AccountFollowUpAsync.notify(newIds); }
トリガはレコードごとに呼ばず、IDをためてから1回だけ呼びます。
トリガの中でレコード1件ごとにnotifyを呼ぶと、一括登録の件数が増えたときに次の上限へ当たります。Trigger.newをループしてIDだけをためて、最後に1回で呼び出します。
呼び出し回数の上限
| 呼び出し元のコンテキスト | 1トランザクションで呼べる回数 |
|---|---|
| 同期Apex(画面・API・トリガなど) | 50回 |
| Queueable Apex | 50回 |
別の@futureメソッド・バッチApexの中 | 0回(呼び出せません) |
@futureメソッドから、別の@futureメソッドは呼べません。バッチApexの中から呼ぶこともできません。
24時間あたりの上限もあります。公式ドキュメントには「250,000 or the number of applicable user licenses in your org multiplied by 200, whichever is greater」とあり、バッチApex・@future・Queueable・スケジュールApexの実行回数を合算した、組織全体のローリング制限です。
外部サービスを呼ぶ場合
@futureメソッドの中からHTTPコールアウトをする場合は、callout=trueを付けます。付け忘れると、実行時に例外になります。
public with sharing class ExternalNotifier { @future(callout=true) public static void notifyExternalService(List<Id> accountIds) { HttpRequest req = new HttpRequest(); req.setEndpoint('callout:External_Service/notify'); req.setMethod('POST'); new Http().send(req); } }
外部サービスを呼び出すfutureメソッドには、callout=trueを付けます。
エンドポイントは指定ログイン情報(Named Credential)を指すのが安全です。URLやトークンをコードへ直接書きません。
Queueableとの使い分け
| 観点 | @future | Queueable |
|---|---|---|
| 引数 | プリミティブ型とそのコレクションだけ | sObjectやカスタムApex型も渡せます |
| ジョブの状況 | 取得できません | System.enqueueJobの戻り値でジョブIDを取得し、追跡できます |
| 連鎖実行 | できません | 実行中のジョブから次のジョブを開始できます |
| ヒープサイズなどの上限 | 非同期Apexの上限(同期Apexより緩和) | 非同期Apexの上限(同期Apexより緩和) |
公式ドキュメントは「Salesforce recommends that you use Queueable Apex instead of Apex future methods」と明記しており、新規に書く非同期処理は、@futureではなくQueueableを第一候補にするという立場です。既存の@futureをそのまま置き換える判断は、影響範囲を確認してから進めます。
ここで間違えやすい
| 間違い | 何が起きるか |
|---|---|
@futureメソッドの引数にsObjectを渡す | コンパイルエラーになります |
トリガでレコードごとに@futureを呼ぶ | 51件以上の一括登録で、1トランザクションの上限(50回)を超えます |
@futureメソッドから別の@futureメソッドを呼ぶ | 実行時エラーになります |
コールアウトのある@futureにcallout=trueを付け忘れる | 実行時に例外になります |
確認した環境
- 2026年9月 / Salesforce Summer '26(APIバージョン 67.0)時点の公式ドキュメントで、
@futureの制約と呼び出し回数の上限、Queueableとの比較を確認しています
まとめ
@futureを付けると、呼び出し元はすぐ戻り、処理は後で実行されます。実行タイミングは保証されません- 戻り値はvoidのみ、引数はプリミティブ型とそのコレクションだけです。sObjectは渡せません
- トリガから呼ぶときは、IDをためて1回で呼び出します。レコードごとに呼ぶと呼び出し回数の上限に当たります
- 1トランザクションで呼べる回数は同期・Queueableで50回、
@futureやバッチApexの中では0回です - コールアウトをする
@futureにはcallout=trueが必要です。新規の非同期処理はQueueableが公式の推奨です
Salesforceの導入・運用についてご相談ください
導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。