문서로 건너뛰기
Ruby

재시도와 멱등성

무엇이 재시도되고, 무엇이 의도적으로 재시도되지 않으며, 재시도된 발송이 왜 중복될 수 없는지.

발송

클라이언트는 모든 발송(emails.send, emails.send_batch, templates.send, broadcasts.send)에 Idempotency-Key를 붙이며, 이 키는 **호출**마다 한 번 oe-와 무작위 UUID로 생성되어 그 호출의 재시도에서 재사용됩니다. API는 무엇이든 발송하기 전에 그 키를 선점하므로, 재시도는 두 번째 메시지를 보내는 대신 원래 메시지를 그대로 재생합니다. 반면 의도적으로 send를 두 번 호출하면 두 번 발송됩니다. 둘은 서로 다른 의도이며, 앞으로도 다르게 남습니다.

직접 만든 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은 재시도됩니다. 쓰기는 두 번째 동일 요청이 첫 번째와 다른 의미를 가질 수 없는 경우에만 재시도되며, 발송은 멱등성 키가 반복을 재생으로 바꾸기 때문에 여기에 해당합니다.

호출재시도 여부이유
모든 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예레이블 설정입니다. 두 번 적용해도 한 번 적용한 것과 같습니다.
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예각각 최종 결과를 지정하므로, 두 번째 호출은 첫 번째 호출이 남긴 상태를 그대로 남깁니다.
audiences.add_contact, audiences.add_contacts, audiences.remove_contacts, audiences.import_contacts, suppressions.add, domains.create_address예반복 호출은 첫 호출의 작업이 이미 끝난 것을 확인하고, 두 번 하지 않고 그 사실을 보고합니다.
contacts.set_photo, account.set_photo, branding.upload_image, domains.set_logo, domains.set_logo_certificate, domains.set_address_photo, imports.upload_chunk예다시 보낸 바이트가 첫 시도에서 저장된 것을 대체합니다.
drafts.create, labels.create, webhooks.create, templates.create, rules.create, roles.create, temp_mail.create, files.upload아니요재시도하면 객체가 두 개 남습니다.
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아니요두 번째 회전은 첫 번째 시도가 돌려준 시크릿을 무효화합니다.
webhooks.test아니요두 번째 테스트 전달이 발송됩니다.
webhooks.replay_delivery아니요이벤트를 수신 측에 한 번 더 보내게 됩니다.
emails.translate, emails.compose, emails.rewrite, emails.suggest_subject아니요각각 모델 호출 비용이 들므로, 응답 없는 요청 뒤에 재시도하면 같은 답을 두 번 사는 셈이 됩니다.
security.begin_step_up, security.verify_step_up아니요재시도하면 두 번째 이메일이 가거나 코드 시도 횟수를 한 번 더 쓸 수 있습니다.
GET이 아닌 그 밖의 모든 호출아니요한 번만 전송되며, 실패는 반복되지 않고 보고됩니다.

백오프

  • 클라이언트의 max_retries:로 제한되며, 기본값은 두 번의 추가 시도입니다. 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이므로 호출한 스레드도 함께 기다리며, 호출은 마지막 시도가 끝난 뒤에야 반환하거나 예외를 발생시킵니다.