ドキュメント本文へスキップ
PHP

リトライと冪等性

何がリトライされ、何をあえてリトライしないのか、そしてリトライされた送信が重複しない理由。

送信

クライアントはすべての送信(emails->send、emails->sendBatch、templates->send、broadcasts->send)に Idempotency-Key を付与します。キーは呼び出しごとに oe- とランダムな UUID で 1 回生成され、その呼び出しのリトライで再利用されます。API は何かを配送する前にそのキーを確保するため、リトライは 2 通目を送るのではなく元のメッセージを再生します。一方、意図的に send を 2 回呼べば 2 通送られます。この 2 つは異なる意図であり、区別されたままです。

自分の idempotencyKey: を渡すと、この保証をプロセスをまたいで広げられます。クラッシュして再実行されたジョブは、送信を繰り返すのではなく再生します。再生の応答では replayed が true になり、保存されているメッセージが現在の状態で返ります。

idempotency.php
$invoiceId = 'inv_2026_09_4192'; $sent = $client->emails->send([    'from' => '[email protected]',    'to' => '[email protected]',    'subject' => 'Your September invoice',    'text' => 'Invoice attached.',], idempotencyKey: 'invoice:' . $invoiceId); echo $sent['id'], ' ', ($sent['replayed'] ?? false) ? 'replayed' : 'sent', PHP_EOL;

キーは、その送信が必要になった理由から導き出してください。時刻から作ってはいけません。異なるボディでキーを再利用すると、黙って再生されるのではなく 422 idempotency_key_reuse で拒否されます。キーは英字、数字、_、.、:、- からなる 1〜255 文字で、それ以外は 400 invalid_idempotency_key になります。

その他すべて

GET はすべてリトライされます。書き込みは、同一のリクエストを 2 回送っても 1 回目と異なる意味になり得ない場合に限ってリトライされます。送信がこれに当てはまるのは、冪等性キーによって繰り返しが再生に変わるからです。

呼び出しリトライ理由
すべての GETする何も変化しないため。
emails->send, emails->sendBatch, templates->send, broadcasts->sendする冪等性キーが繰り返しを再生に変えるため。
emails->cancel, emails->reschedule, broadcasts->cancel, forms->pause, forms->resume, threads->restoreする名前の付いた状態を設定するだけの操作であるため。
threads->update, threads->trashするラベルの設定であり、2 回適用することは 1 回適用することに等しいため。
threads->snooze, threads->unsnoozeする復帰時刻はボディに含まれており、到着時刻から導出されないため。
emails->update, labels->update, webhooks->update, settings->update, roles->update, members->update, domains->update, domains->updateAddress, domains->updateAddressForward, contacts->update, audiences->update, keys->update, forms->update, branding->update, threads->updateNote, chats->rename, account->setEmailNotification, account->setPushMuted, appHost->set, workspaces->setActiveする名前の付いたフィールドを設定するだけの操作であるため。
members->grantAddress, members->grantDomain, rules->reorder, threads->reorderNotesする付与は upsert であり、順序は完全な形で指定されます。
templates->publish, forms->publish, imports->startする公開済みのものを公開しても、開始済みのインポートを開始しても、変更されずにそのまま返ります。
templates->preview, templates->render, broadcasts->preview, rules->testする描画、集計または評価を行うだけで、何も書き込まないため。
domains->verify, appHost->verify, senders->researchするチェックを繰り返しても、変わるのは実行時刻だけです。
contacts->save, contacts->setAudiences, contacts->removePhoto, contacts->block, contacts->unblock, contacts->deleteMany, keys->revoke, files->revokeLink, files->revokeAllLinks, appHost->delete, account->removePhoto, branding->removeImage, domains->removeLogo, domains->removeLogoCertificate, domains->removeAddressPhoto, account->acceptInvitation, account->declineInvitation, forms->approveSubmission, subscriptions->moveするいずれも最終的な結果を指定する操作であり、2 回目の呼び出しは 1 回目が残した状態をそのまま残すため。
audiences->addContact, audiences->addContacts, audiences->removeContacts, audiences->importContacts, suppressions->add, domains->createAddressする繰り返しの呼び出しは 1 回目の処理がすでに済んでいることを検出し、2 回行うのではなくそれを報告するため。
contacts->setPhoto, account->setPhoto, branding->uploadImage, domains->setLogo, domains->setLogoCertificate, domains->setAddressPhoto, imports->uploadChunkする再送されたバイト列が、1 回目の試行で保存されたものを置き換えるため。
drafts->create, labels->create, webhooks->create, templates->create, rules->create, roles->create, tempMail->create, files->uploadしないリトライするとオブジェクトが 2 つ残るため。
drafts->updateしない送った id を再利用するのではなく、各書き込みの結果から id を読み取ること。
drafts->delete, labels->delete, webhooks->delete, templates->delete, rules->delete, roles->delete, members->remove, members->revokeAddress, tempMail->delete, tempMail->deleteMessageしない応答が失われた後にリトライすると、成功した処理について失敗が報告されるため。
webhooks->rotateSecretしない2 回目のローテーションが、1 回目の試行で返されたシークレットを無効にするため。
webhooks->testしない2 つ目のテスト配信を送ってしまうため。
webhooks->replayDeliveryしないイベントが受信側にもう一度送られてしまうため。
emails->translate, emails->compose, emails->rewrite, emails->suggestSubjectしないどれもモデル呼び出しを消費するため、応答のなかったリクエストのあとにリトライすると、同じ答えに 2 回支払うことになります。
security->beginStepUp, security->verifyStepUpしないリトライによって 2 通目のメールが送られたり、コードの試行を 2 回分消費したりするおそれがあります。
GET 以外のその他すべての呼び出ししない1 回だけ送信され、失敗は繰り返されるのではなく報告されるため。

バックオフ

  • クライアントの maxRetries: で上限が決まり、既定では追加で 2 回試行します。maxRetries: 0 でリトライを無効にします。
  • リトライするのは、ネットワーク障害の後、または 408、500、502、503、504 が返った場合だけである。429 は Retry-After が付いている場合にのみリトライされるが、この API はそれを送らないため、レート制限は即座に例外となる。その他のステータスも直ちに例外となる。
  • 0.5 秒から 8 秒までの指数バックオフで、ジッターを加えます。各待機はその上限の半分から上限までの間のランダムな値なので、多数のクライアントが復旧時に再び同期することはありません。
  • Retry-After があればその指示に従う。delay-seconds 形式と HTTP-date 形式のどちらにも対応する。サーバーが待機時間を指定した場合、クライアントはバックオフではなくその時間だけ正確に待つ。
  • サーバーが 1 分を超える待機を求めた場合は、待機ではなく中止の指示として扱い、retryAfterSeconds を付けた例外をスローします。求められたより早く戻ることは、その指示に従ったことにはなりません。
  • タイムアウトも他と同じネットワーク障害なので、繰り返しても安全な呼び出しはタイムアウト後に再試行され、timeout: は試行ごとに新たに適用されます。
  • 待機は呼び出し内部の sleep と usleep なので、スクリプトも待ちます。呼び出しが戻るか例外をスローするのは、最後の試行が終わってからです。人が待っている Web リクエストの中では、timeout: と maxRetries: を小さく保ってください。