Cómo diseñar reintentos idempotentes en una API SMS para evitar envíos duplicados
Guía operativa para tratar timeouts y resultados ambiguos en SMS sin convertir un reintento técnico en un segundo mensaje. Incluye claves de idempotencia, estados persistentes, reconciliación con callbacks o DLR y controles específicos para OTP.

Por qué un timeout no demuestra que el SMS no se haya aceptado
Un timeout indica que el cliente no recibió una respuesta dentro del tiempo configurado. No demuestra, por sí solo, que el servidor no recibiera la solicitud, que no la procesara o que no aceptara el mensaje para su posterior envío.
En una operación HTTP, pueden haberse transmitido todos los bytes de la solicitud y haberse creado el mensaje remoto antes de perderse la respuesta. También puede ocurrir que la conexión falle antes de que el proveedor reciba la petición. Desde el punto de vista de la aplicación cliente, ambos escenarios pueden parecer idénticos: no hay respuesta utilizable.
Por este motivo, un timeout posterior al inicio de la transmisión debe clasificarse como resultado ambiguo. Repetir de inmediato una creación de mensaje con una nueva identidad puede producir un segundo SMS, aunque el primero ya exista o haya sido aceptado.
- No trate la ausencia de respuesta como un rechazo definitivo.
- Diferencie, cuando su biblioteca y telemetría lo permitan, entre fallo antes de enviar la solicitud, fallo durante la transmisión y fallo después de enviarla.
- Diseñe el flujo para que una misma intención de negocio pueda consultarse, reconciliarse o repetirse sin crear una operación nueva.

El riesgo operativo de los duplicados
Un duplicado no es únicamente un coste adicional. En un flujo OTP puede confundir a la persona usuaria si recibe códigos distintos, especialmente si el sistema invalida el código anterior al generar uno nuevo. En alertas operativas, dos avisos pueden desencadenar actuaciones repetidas. En notificaciones transaccionales, el destinatario puede interpretar el segundo mensaje como un error o un intento fraudulento.
La prevención no debe confundirse con la supresión indiscriminada. Dos acciones de negocio realmente distintas pueden requerir dos SMS al mismo número y con contenido similar. El objetivo es deduplicar la repetición técnica de una misma intención, no impedir comunicaciones legítimas, consentidas y necesarias.
- Un reintento técnico debe conservar la identidad de la intención original.
- Un nuevo desafío OTP, una nueva operación o una nueva versión deliberada del mensaje debe tener una identidad nueva.
- La deduplicación debe aplicarse sobre una ventana temporal y una semántica documentadas, no solo sobre el número de destino.

Defina un modelo de estados que separe aceptación y entrega
Un modelo mínimo debe distinguir lo que sabe su aplicación de lo que confirma el proveedor y, más adelante, de lo que informa el ciclo de entrega. La aceptación por un proveedor o por un carrier ascendente no equivale necesariamente a entrega final al destinatario.
Una secuencia interna práctica puede incluir: creado, intento en curso, enviado a proveedor, aceptación confirmada, resultado pendiente y resultado final. El nombre exacto es menos importante que mantener transiciones persistentes, auditables y sin ambigüedad.
El resultado final puede representar una entrega informada, una no entrega, un fallo definitivo o el agotamiento de una ventana de reconciliación. Si recibe DLR o callbacks, registre el evento original y su momento de recepción, además del estado consolidado.
- Creado: la intención de negocio ya está persistida, pero no se ha iniciado el envío.
- Intento en curso: se ha reservado un intento identificable antes de abrir la conexión.
- Aceptación confirmada: el proveedor respondió y devolvió una confirmación o identificador conforme a su contrato.
- Resultado pendiente: existe aceptación o un resultado ambiguo y se espera consulta, callback, DLR u otra reconciliación.
- Resultado final: el flujo alcanzó un estado terminal según las reglas documentadas para esa integración.
Use una clave de idempotencia para una intención concreta
Una clave de idempotencia permite que una API distinga la repetición de una solicitud anterior de una nueva operación. Para que funcione, el cliente debe reutilizar la misma clave al reintentar exactamente la misma intención de negocio.
La clave no debería representar solo el número de teléfono. Un mismo destinatario puede recibir varias comunicaciones legítimas. Debe quedar vinculada a un evento estable: por ejemplo, el identificador interno de una notificación, un desafío OTP concreto, el destinatario, el canal SMS y la versión de contenido que se pretende enviar.
Si el proveedor admite un token de idempotencia, respete su contrato: formato, ubicación en la solicitud, tratamiento de parámetros discrepantes, alcance de la deduplicación y periodo de retención. Si no lo admite, la idempotencia debe controlarse principalmente en su propio sistema, y los reintentos tras ambigüedad requieren todavía más prudencia.
- Genere la clave una vez por intención, no una vez por intento de red.
- Persista la clave antes de la llamada remota.
- Guarde una huella o versión de los parámetros relevantes para detectar si alguien intenta reutilizar la clave con una operación distinta.
- No reutilice la clave para un nuevo desafío OTP ni para una comunicación de negocio independiente.
- No suponga que todos los proveedores deduplican con la misma duración o semántica.
Persista la intención antes de llamar a HTTP o SMPP
La persistencia debe ocurrir antes de la operación de red. Si primero llama a la API y solo después guarda un registro, un corte entre ambas acciones puede dejar un mensaje remoto sin una intención local que permita rastrearlo o reconciliarlo.
En una transacción local, cree el registro de envío, asigne el identificador interno, la clave de idempotencia, los parámetros necesarios para reproducir la solicitud y el estado creado. Después, un proceso de envío puede tomar ese registro, marcar el intento como en curso y realizar la llamada al proveedor.
Al recibir una respuesta válida, persista el identificador remoto y el significado de la respuesta. En SMPP, una respuesta satisfactoria a submit_sm devuelve un message_id asignado por el SMSC; este identificador debe conservarse para correlacionar operaciones y recibos posteriores.
- Identificador interno de la intención.
- Clave de idempotencia y huella de la solicitud.
- Destino y contenido o referencia segura a su versión autorizada.
- Marca de tiempo de creación, inicio y fin de cada intento.
- Identificador devuelto por el proveedor o SMSC, cuando exista.
- Estado actual, historial de transiciones y motivo de cualquier estado terminal.
Clasifique los resultados antes de decidir si reintentar
Una política de reintentos segura no se basa en “cualquier error vuelve a enviarse”. Debe separar respuestas definitivas, fallos recuperables y resultados ambiguos. Las categorías concretas deben derivarse del contrato de cada proveedor y del protocolo utilizado.
Las respuestas definitivas suelen requerir finalizar o devolver el control al flujo de negocio: por ejemplo, credenciales inválidas, parámetros inválidos, formato no admitido o una dirección de destino declarada inválida. Reintentar sin cambiar la causa no aporta fiabilidad y puede aumentar tráfico innecesario.
Los fallos recuperables pueden incluir indisponibilidad temporal, limitación de caudal o errores temporales de red explícitamente documentados. En SMPP, ESME_RTHROTTLED señala que se excedieron límites de mensajes permitidos; la reacción apropiada es reducir presión y aplicar espera controlada, no reenviar en bucle.
Los resultados ambiguos incluyen timeouts, desconexiones y respuestas perdidas después de iniciar el envío. No deben tratarse automáticamente como fallos recuperables, porque el proveedor podría haber aceptado el mensaje.
- Definitivo: finalice, registre la causa y corrija la solicitud o el flujo antes de crear una nueva intención.
- Recuperable: programe un reintento acotado con la misma clave de idempotencia.
- Ambiguo: consulte si es posible, espere una señal de reconciliación y reutilice la misma identidad solo si el proveedor ofrece deduplicación compatible.
- Throttling: aplique control de tasa y espera progresiva; no concentre reintentos en el mismo instante.
Aplique límites, espera progresiva y una ventana de validez
Un reintento debe estar limitado por número de intentos, tiempo total y vigencia de la intención de negocio. La espera progresiva evita concentrar solicitudes tras una incidencia o una limitación de caudal. Puede añadir variación aleatoria controlada para evitar que muchos trabajadores reintenten de forma sincronizada.
No existe un número universal de intentos ni una espera válida para todas las rutas y casos de uso. Defínalos a partir de la criticidad del mensaje, el comportamiento documentado del proveedor, los límites de caudal contratados o técnicos y la vida útil del contenido.
La política debe detenerse al alcanzar un estado final, al expirar la ventana de negocio o cuando se exceda el presupuesto de reintentos. Un reintento posterior a la utilidad del mensaje puede ser peor que un fallo: una alerta tardía o un OTP vencido no resuelve la necesidad original.
- Establezca un máximo de intentos y un límite de tiempo total por intención.
- Aumente el intervalo entre reintentos ante fallos temporales o throttling.
- Mantenga una cola diferida en lugar de bloquear el flujo principal con esperas activas.
- Registre cada decisión: por qué se reintentó, cuánto se esperó y qué regla detuvo el proceso.
- No convierta una recuperación técnica en un envío indefinido.
Trate los OTP como un caso de seguridad y experiencia de usuario
En OTP, la intención no es “enviar un texto a un número”, sino entregar un código asociado a un desafío concreto y con vigencia limitada. La clave de idempotencia debe corresponder a ese desafío, no a una solicitud HTTP individual.
El sistema debe definir claramente si un nuevo intento de verificación reutiliza el mismo desafío o crea otro. Si crea otro código y deja el anterior activo, el usuario puede recibir varios códigos válidos. Si invalida el anterior, un SMS retrasado puede contener un código que ya no funciona. Ambas decisiones son de producto y seguridad, pero deben ser coherentes con la política de envío.
La ventana de reintentos debe terminar antes de que el desafío expire. También debe coordinarse con cualquier periodo de validez de cola configurado en el proveedor. No es apropiado seguir intentando entregar un OTP una vez que ya no puede verificarse.
- Asocie cada OTP a un identificador de desafío persistente.
- Reintente el mismo envío con la misma clave cuando se trate de la misma intención.
- Documente cuándo se genera un código nuevo y qué ocurre con los anteriores.
- Detenga reintentos y envíos pendientes al expirar el desafío.
- Mida los duplicados, los retrasos y los abandonos sin almacenar más datos personales de los necesarios.
Preguntas frecuentes
¿Un timeout en una API SMS significa que el mensaje no se envió?
No. Puede significar que la conexión falló antes de que el proveedor recibiera la solicitud, pero también que el proveedor la recibió y procesó mientras se perdió o retrasó la respuesta. Trátelo como un resultado ambiguo hasta poder reconciliarlo.
¿Qué debe contener una clave de idempotencia para SMS?
Debe identificar una intención de negocio concreta. Puede vincularse a un identificador interno de evento o desafío OTP, destinatario, canal y versión de contenido. No debe ser solo el número de destino ni regenerarse en cada reintento.
¿Debo reintentar un error de throttling?
Puede ser candidato a reintento controlado si el contrato del proveedor lo clasifica como temporal. Aplique control de tasa, espera progresiva y límites. No reenvíe inmediatamente ni de forma ilimitada.
¿Un DLR confirma siempre la recepción en el teléfono?
No necesariamente. El significado de cada estado depende del contrato y de la información disponible en la cadena de entrega. Debe distinguir entre aceptación por el proveedor, envío hacia un carrier, DLR recibido y cualquier confirmación de entrega informada. Un DLR no debe interpretarse más allá de su semántica documentada.
¿Los callbacks sustituyen la respuesta de creación del mensaje?
No. La respuesta síncrona y los callbacks cumplen funciones distintas. Algunos proveedores no emiten callback para el estado inicial, por lo que debe persistir la respuesta de creación y reconciliar los cambios posteriores mediante callbacks, consultas o DLR.
¿Qué hacer si el proveedor no admite claves de idempotencia?
Controle la intención y los estados en su propio sistema antes de llamar al proveedor. Ante un timeout ambiguo, priorice la consulta por identificadores disponibles y la reconciliación de eventos. Si no existe un mecanismo remoto de deduplicación o consulta, documente ese límite y sea especialmente restrictivo al crear una segunda solicitud.
Fuentes consultadas
- RFC 9110: HTTP SemanticsIETF / RFC Editor
- SMPP Protocol Specification v3.4, Issue 1.2SMPP Developers Forum
- Messages resourceTwilio
- Outbound Message Status in Status CallbacksTwilio
- Messaging ServicesTwilio
- Cloud Control API ReferenceAmazon Web Services
- AWS Well-Architected Framework: Reliability PillarAmazon Web Services