Ir a la documentación
Ruby

Reintentos e idempotencia

Qué se reintenta, qué no se reintenta a propósito y por qué un envío reintentado no puede duplicarse.

Envíos

El cliente adjunta un Idempotency-Key a cada envío (emails.send, emails.send_batch, templates.send y broadcasts.send), generado una vez por **llamada** como oe- seguido de un UUID aleatorio, y reutilizado por los reintentos de esa llamada. La API reserva esa clave antes de despachar nada, así que un reintento reproduce el mensaje original en lugar de enviar uno segundo, mientras que dos llamadas deliberadas a send siguen enviando dos veces. Son intenciones distintas y siguen siendo distintas.

Pasa tu propio idempotency_key: para extender esa garantía entre procesos, de modo que un trabajo que se cayó y volvió a ejecutarse reproduzca sus envíos en lugar de repetirlos. Una reproducción responde con replayed a true y el mensaje almacenado tal como está ahora.

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]

Dedúcela de aquello que hizo necesario el envío. Nunca de un reloj. Reutilizar una clave con un cuerpo distinto se rechaza con un 422 idempotency_key_reuse en lugar de reproducirse en silencio. Una clave tiene de 1 a 255 caracteres entre letras, dígitos, _, ., : o -, y cualquier otra cosa da un 400 invalid_idempotency_key.

Todo lo demás

Todo GET se reintenta. Una escritura solo se reintenta cuando una segunda solicitud idéntica no puede significar nada distinto de la primera, y un envío cumple esa condición porque su clave de idempotencia convierte una repetición en una reproducción.

LlamadaSe reintentaPor qué
Todo GETSíNo cambia nada.
emails.send, emails.send_batch, templates.send, broadcasts.sendSíUna clave de idempotencia convierte una repetición en una reproducción.
emails.cancel, emails.reschedule, broadcasts.cancel, forms.pause, forms.resume, threads.restoreSíUna asignación pura de un estado con nombre.
threads.update, threads.trashSíUna asignación de etiquetas. Aplicarla dos veces equivale a aplicarla una vez.
threads.snooze, threads.unsnoozeSíEl instante de reactivación viaja en el cuerpo; no se deriva de la hora de llegada.
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_activeSíUna asignación pura de campos con nombre.
members.grant_address, members.grant_domain, rules.reorder, threads.reorder_notesSíUna concesión es un upsert, y un orden se indica por completo.
templates.publish, forms.publish, imports.startSíPublicar lo que ya está publicado, o iniciar una importación que ya ha empezado, lo devuelve sin cambios.
templates.preview, templates.render, broadcasts.preview, rules.testSíRenderizan, cuentan o evalúan, y no escriben nada.
domains.verify, app_host.verify, senders.researchSíUna comprobación repetida no cambia nada salvo el momento en que se hizo.
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.moveSíCada una declara el resultado final, así que una segunda llamada deja lo que dejó la primera.
audiences.add_contact, audiences.add_contacts, audiences.remove_contacts, audiences.import_contacts, suppressions.add, domains.create_addressSíUna repetición encuentra ya hecho el trabajo de la primera llamada y lo informa en lugar de hacerlo dos veces.
contacts.set_photo, account.set_photo, branding.upload_image, domains.set_logo, domains.set_logo_certificate, domains.set_address_photo, imports.upload_chunkSíLos bytes enviados de nuevo reemplazan lo que guardó el primer intento.
drafts.create, labels.create, webhooks.create, templates.create, rules.create, roles.create, temp_mail.create, files.uploadNoUn reintento deja dos objetos.
drafts.updateNoLee el id del resultado de cada escritura en lugar de reutilizar el que enviaste.
drafts.delete, labels.delete, webhooks.delete, templates.delete, rules.delete, roles.delete, members.remove, members.revoke_address, temp_mail.delete, temp_mail.delete_messageNoUn reintento tras una respuesta perdida informa de un fallo en un trabajo que sí se completó.
webhooks.rotate_secretNoUna segunda rotación invalida el secreto que devolvió el primer intento.
webhooks.testNoEnviaría una segunda entrega sintética.
webhooks.replay_deliveryNoEnviaría el evento a tu receptor por segunda vez.
emails.translate, emails.compose, emails.rewrite, emails.suggest_subjectNoCada una consume llamadas al modelo, así que un reintento tras una solicitud sin respuesta paga dos veces la misma respuesta.
security.begin_step_up, security.verify_step_upNoUn reintento podría enviar un segundo correo o gastar un segundo intento del código.
Cualquier otra llamada que no sea un GETNoSe envía una sola vez, y un fallo se informa en lugar de repetirse.

El backoff

  • Limitado por max_retries: en el cliente, con dos intentos adicionales por defecto. max_retries: 0 desactiva los reintentos.
  • Solo tras un fallo de red o un 408, 500, 502, 503 o 504. Un 429 se reintenta únicamente cuando lleva un Retry-After, y esta API no lo envía, así que un límite de tasa lanza un error de inmediato. Cualquier otro status lanza al instante.
  • Exponencial, desde medio segundo hasta ocho, con jitter: cada espera es un punto aleatorio entre la mitad de ese tope y el tope entero, para que una flota no se resincronice al recuperarse el servicio.
  • Regulado por Retry-After en cualquiera de sus formas, delay-seconds y HTTP-date. Cuando el servidor indica una espera, el cliente espera exactamente ese tiempo en lugar de aplicar backoff.
  • Si el servidor pide más de un minuto, se interpreta como una orden de detenerse y no de esperar, así que el error se lanza con retry_after_seconds incluido. Volver antes de lo que pidió no es respetarlo.
  • Un tiempo de espera agotado es un fallo de red como cualquier otro, así que una llamada que se puede repetir sin riesgo se vuelve a intentar tras uno, y timeout: se aplica de nuevo a cada intento.
  • Las esperas son llamadas a sleep dentro de la llamada, así que el hilo que llama también espera, y la llamada solo devuelve o lanza una vez terminado su último intento.