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

リトライと冪等性

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

送信

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

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

idempotency.rb
invoice_id = "inv_2026_09_4192" sent = client.emails.send(  from: "[email protected]",  to: "[email protected]",  subject: "Your September invoice",  text: "Invoice attached.",  idempotency_key: "invoice:#{invoice_id}") puts sent[:id], sent[:replayed]

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

その他すべて

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

呼び出しリトライ理由
すべての GETする何も変化しないため。
emails.send, emails.send_batch, 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.update_address, domains.update_address_forward, contacts.update, audiences.update, keys.update, forms.update, branding.update, threads.update_note, chats.rename, account.set_email_notification, account.set_push_muted, app_host.set, workspaces.set_activeする名前の付いたフィールドを設定するだけの操作であるため。
members.grant_address, members.grant_domain, rules.reorder, threads.reorder_notesする付与は upsert であり、順序は完全な形で指定されます。
templates.publish, forms.publish, imports.startする公開済みのものを公開しても、開始済みのインポートを開始しても、変更されずにそのまま返ります。
templates.preview, templates.render, broadcasts.preview, rules.testする描画、集計または評価を行うだけで、何も書き込まないため。
domains.verify, app_host.verify, senders.researchするチェックを繰り返しても、変わるのは実行時刻だけです。
contacts.save, contacts.set_audiences, contacts.remove_photo, contacts.block, contacts.unblock, contacts.delete_many, keys.revoke, files.revoke_link, files.revoke_all_links, app_host.delete, account.remove_photo, branding.remove_image, domains.remove_logo, domains.remove_logo_certificate, domains.remove_address_photo, account.accept_invitation, account.decline_invitation, forms.approve_submission, subscriptions.moveするいずれも最終的な結果を指定する操作であり、2 回目の呼び出しは 1 回目が残した状態をそのまま残すため。
audiences.add_contact, audiences.add_contacts, audiences.remove_contacts, audiences.import_contacts, suppressions.add, domains.create_addressする繰り返しの呼び出しは 1 回目の処理がすでに済んでいることを検出し、2 回行うのではなくそれを報告するため。
contacts.set_photo, account.set_photo, branding.upload_image, domains.set_logo, domains.set_logo_certificate, domains.set_address_photo, imports.upload_chunkする再送されたバイト列が、1 回目の試行で保存されたものを置き換えるため。
drafts.create, labels.create, webhooks.create, templates.create, rules.create, roles.create, temp_mail.create, files.uploadしないリトライするとオブジェクトが 2 つ残るため。
drafts.updateしない送った id を再利用するのではなく、各書き込みの結果から id を読み取ること。
drafts.delete, labels.delete, webhooks.delete, templates.delete, rules.delete, roles.delete, members.remove, members.revoke_address, temp_mail.delete, temp_mail.delete_messageしない応答が失われた後にリトライすると、成功した処理について失敗が報告されるため。
webhooks.rotate_secretしない2 回目のローテーションが、1 回目の試行で返されたシークレットを無効にするため。
webhooks.testしない2 つ目のテスト配信を送ってしまうため。
webhooks.replay_deliveryしないイベントが受信側にもう一度送られてしまうため。
emails.translate, emails.compose, emails.rewrite, emails.suggest_subjectしないどれもモデル呼び出しを消費するため、応答のなかったリクエストのあとにリトライすると、同じ答えに 2 回支払うことになります。
security.begin_step_up, security.verify_step_upしないリトライによって 2 通目のメールが送られたり、コードの試行を 2 回分消費したりするおそれがあります。
GET 以外のその他すべての呼び出ししない1 回だけ送信され、失敗は繰り返されるのではなく報告されるため。

バックオフ

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