Ir a la documentación
Python

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** 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 siéndolo.

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.

idempotency.py
invoice_id = 'inv_4192' client.emails.send(    {'from': sender, 'to': recipient, 'subject': subject, 'text': text},    idempotency_key=f'invoice:{invoice_id}',)

Dedúcelo de aquello que hizo necesario el envío. Nunca de un reloj. Reutilizar una clave con un cuerpo distinto se rechaza con idempotency_key_reuse en lugar de reproducirse en silencio.

Todo lo demás

Toda lectura se reintenta. Una escritura solo se reintenta cuando una segunda petición 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é
Toda lecturaSíNo cambia nada.
emails.send, emails.send_batch, templates.send y broadcasts.sendSíUna clave de idempotencia convierte una repetición en una reproducción.
emails.cancel, emails.reschedule, broadcasts.cancel, forms.publish, forms.pause, forms.resume, forms.approve_submission, account.accept_invitation y account.decline_invitationSíUna asignación pura de un estado con nombre.
threads.update, threads.trash y threads.restoreSí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.
labels.update, webhooks.update, settings.update, roles.update, members.update, domains.update, contacts.update, audiences.update, keys.update, emails.update, forms.update, branding.update, chats.rename, threads.update_note, domains.update_address, domains.update_address_forward, account.set_email_notification, account.set_push_muted, app_host.set y workspaces.set_activeSíUna asignación pura de campos con nombre.
members.grant_address, members.grant_domain, rules.reorder y threads.reorder_notesSíLa concesión es un upsert, y el orden se indica por completo.
templates.publish, imports.startSíPublicar un head que ya está publicado, o iniciar una importación que ya ha empezado, lo devuelve sin cambios.
templates.preview, templates.render, broadcasts.preview y rules.testSíRenderizan, cuentan o evalúan, y no escriben nada.
domains.verify, app_host.verifySíRepetir una comprobación no cambia nada salvo la hora de la comprobación.
contacts.save, contacts.set_audiences, contacts.remove_photo, contacts.block, contacts.unblock, contacts.delete_many, keys.revoke, account.remove_photo, branding.remove_image, domains.remove_logo, domains.remove_logo_certificate, domains.remove_address_photo, app_host.delete, files.revoke_link, files.revoke_all_links y 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_address y senders.researchSíUna repetición encuentra ya hecho el trabajo de la primera llamada y lo informa en lugar de hacerlo dos veces.
contacts.set_photo, imports.upload_chunk, account.set_photo, branding.upload_image, domains.set_logo, domains.set_logo_certificate y domains.set_address_photoSí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.upload, templates.design y forms.designNoUn 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 y 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.translateNoConsume llamadas al modelo, así que un reintento tras una solicitud sin respuesta paga dos veces la misma respuesta.
emails.compose, emails.rewrite y emails.suggest_subjectNoCada intento gasta otra acción de IA y vuelve con una respuesta distinta.
Cualquier otra llamadaNoSe envía una sola vez, y un fallo se informa en lugar de repetirse.

forms.update se reintenta incluso con expectedUpdatedAt, así que un reintento tras una respuesta perdida puede volver con un 409 version_conflict porque el primer intento sí se completó. Lee el formulario antes de volver a intentarlo.

client.raw.request reintenta un GET y envía cualquier otra cosa una sola vez, salvo que pases repeatable=True.

El backoff

  • Limitado por max_retries en el cliente, con dos intentos adicionales por defecto.
  • 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 una excepción de inmediato. Cualquier otro estado la lanza al instante.
  • Exponencial, desde medio segundo hasta ocho, con jitter, 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.
  • timeout limita cada intento, así que una llamada que agote sus dos reintentos puede tardar tres tiempos de espera más las esperas entre ellos.
  • Una llamada de AsyncOpenEmail cancelada nunca se reintenta. La cancelación se propaga de inmediato, ya sea desde la solicitud o desde la espera previa al siguiente intento.