Back to blog SMS Connectivity and Operations

How to Design Idempotent Retries in an SMS API to Avoid Duplicate Sends

An operational guide to handling timeouts and ambiguous SMS outcomes without turning a technical retry into a second message. Includes idempotency keys, persistent states, reconciliation through callbacks or DLRs, and OTP-specific controls.

Flowchart for idempotent retries in an SMS API

Why a timeout does not prove that the SMS was not accepted

A timeout indicates that the client did not receive a response within the configured time. By itself, it does not prove that the server did not receive the request, process it, or accept the message for later delivery.

In an HTTP operation, all request bytes may have been transmitted and the remote message may have been created before the response was lost. The connection may also fail before the provider receives the request. From the client application's perspective, both scenarios can look identical: there is no usable response.

For this reason, a timeout after transmission has started must be classified as an ambiguous outcome. Immediately repeating a message creation request with a new identity can produce a second SMS, even when the first one already exists or was accepted.

  • Do not treat the absence of a response as a definitive rejection.
  • When your library and telemetry allow it, distinguish between failure before sending the request, failure during transmission, and failure after it was sent.
  • Design the flow so that the same business intent can be queried, reconciled, or repeated without creating a new operation.
Why a timeout does not prove that the SMS was not accepted

The operational risk of duplicates

A duplicate is not merely an additional cost. In an OTP flow, it can confuse the user if they receive different codes, especially if the system invalidates the previous code when generating a new one. In operational alerts, two notifications can trigger repeated actions. In transactional notifications, the recipient may interpret the second message as an error or a fraudulent attempt.

Prevention should not be confused with indiscriminate suppression. Two genuinely different business actions may require two SMS messages to the same number with similar content. The goal is to deduplicate the technical repetition of the same intent, not to prevent legitimate, consented, and necessary communications.

  • A technical retry must retain the identity of the original intent.
  • A new OTP challenge, a new operation, or a deliberate new version of the message must have a new identity.
  • Deduplication should apply based on a documented time window and semantic definition, not only on the destination number.
The operational risk of duplicates

Define a state model that separates acceptance from delivery

A minimum model must distinguish what your application knows from what the provider confirms and, later, what the delivery lifecycle reports. Acceptance by a provider or upstream carrier does not necessarily mean final delivery to the recipient.

A practical internal sequence may include: created, attempt in progress, sent to provider, acceptance confirmed, outcome pending, and final outcome. The exact names matter less than keeping transitions persistent, auditable, and unambiguous.

The final outcome may represent reported delivery, non-delivery, a definitive failure, or the expiry of a reconciliation window. If you receive DLRs or callbacks, record the original event and its receipt time in addition to the consolidated status.

  • Created: the business intent is already persisted, but sending has not started.
  • Attempt in progress: an identifiable attempt has been reserved before opening the connection.
  • Acceptance confirmed: the provider responded and returned a confirmation or identifier according to its contract.
  • Outcome pending: acceptance exists or the outcome is ambiguous, and a query, callback, DLR, or other reconciliation is awaited.
  • Final outcome: the flow reached a terminal state under the documented rules for that integration.

Use an idempotency key for a specific intent

An idempotency key allows an API to distinguish a repeated earlier request from a new operation. For it to work, the client must reuse the same key when retrying exactly the same business intent.

The key should not represent only the phone number. The same recipient may receive several legitimate communications. It should be linked to a stable event: for example, the internal identifier of a notification, a specific OTP challenge, the recipient, the SMS channel, and the content version intended for sending.

If the provider supports an idempotency token, follow its contract: format, placement in the request, handling of conflicting parameters, deduplication scope, and retention period. If it does not support one, idempotency must be controlled primarily in your own system, and retries after ambiguity require even greater caution.

  • Generate the key once per intent, not once per network attempt.
  • Persist the key before the remote call.
  • Store a fingerprint or version of relevant parameters to detect attempts to reuse the key for a different operation.
  • Do not reuse the key for a new OTP challenge or an independent business communication.
  • Do not assume that all providers deduplicate for the same duration or with the same semantics.

Persist the intent before calling HTTP or SMPP

Persistence must happen before the network operation. If you call the API first and only save a record afterward, an interruption between those actions can leave a remote message without a local intent that can be tracked or reconciled.

In a local transaction, create the send record, assign the internal identifier, idempotency key, parameters needed to reproduce the request, and created state. A sending process can then take that record, mark the attempt as in progress, and call the provider.

When a valid response is received, persist the remote identifier and the meaning of the response. In SMPP, a successful submit_sm response returns a message_id assigned by the SMSC; this identifier must be retained to correlate later operations and receipts.

  • Internal intent identifier.
  • Idempotency key and request fingerprint.
  • Destination and content, or a secure reference to its authorized version.
  • Timestamp of creation, start, and end of each attempt.
  • Identifier returned by the provider or SMSC, when available.
  • Current state, transition history, and the reason for any terminal state.

Classify outcomes before deciding whether to retry

A safe retry policy is not based on the idea that every error should be sent again. It must separate definitive responses, recoverable failures, and ambiguous outcomes. The specific categories must be derived from the contract of each provider and the protocol in use.

Definitive responses usually require completion or returning control to the business flow: for example, invalid credentials, invalid parameters, unsupported format, or a destination address declared invalid. Retrying without changing the cause does not improve reliability and may increase unnecessary traffic.

Recoverable failures may include temporary unavailability, rate limiting, or explicitly documented temporary network errors. In SMPP, ESME_RTHROTTLED indicates that permitted message limits were exceeded; the appropriate response is to reduce pressure and apply controlled waiting, not to resend in a loop.

Ambiguous outcomes include timeouts, disconnections, and lost responses after sending has started. They should not automatically be treated as recoverable failures because the provider may already have accepted the message.

  • Definitive: finish, record the cause, and correct the request or flow before creating a new intent.
  • Recoverable: schedule a bounded retry with the same idempotency key.
  • Ambiguous: query when possible, wait for a reconciliation signal, and reuse the same identity only if the provider offers compatible deduplication.
  • Throttling: apply rate control and progressive waiting; do not concentrate retries at the same time.

Apply limits, progressive waiting, and a validity window

A retry must be limited by number of attempts, total time, and the validity of the business intent. Progressive waiting prevents requests from being concentrated after an incident or rate limit. You can add controlled random variation to prevent many workers from retrying in a synchronized way.

There is no universal number of attempts or waiting period that fits every route and use case. Define them based on message criticality, the provider's documented behavior, contracted or technical throughput limits, and the useful life of the content.

The policy must stop when a final state is reached, the business window expires, or the retry budget is exceeded. A retry after the message is no longer useful can be worse than a failure: a late alert or expired OTP does not address the original need.

  • Set a maximum number of attempts and a total time limit for each intent.
  • Increase the interval between retries after temporary failures or throttling.
  • Maintain a delayed queue instead of blocking the main flow with active waits.
  • Record every decision: why it was retried, how long it waited, and which rule stopped the process.
  • Do not turn a technical recovery into an indefinite sending process.

Treat OTPs as a security and user experience case

For OTPs, the intent is not simply to send text to a number, but to deliver a code associated with a specific challenge and a limited validity period. The idempotency key must correspond to that challenge, not to an individual HTTP request.

The system must clearly define whether a new verification attempt reuses the same challenge or creates another one. If it creates another code and leaves the previous one active, the user may receive several valid codes. If it invalidates the previous one, a delayed SMS may contain a code that no longer works. Both choices are product and security decisions, but they must be consistent with the sending policy.

The retry window must end before the challenge expires. It must also be coordinated with any provider-side queue validity period. It is not appropriate to continue trying to deliver an OTP once it can no longer be verified.

  • Associate each OTP with a persistent challenge identifier.
  • Retry the same send using the same key when it is the same intent.
  • Document when a new code is generated and what happens to previous codes.
  • Stop retries and pending sends when the challenge expires.
  • Measure duplicates, delays, and abandonment without storing more personal data than necessary.
FAQ

Frequently asked questions

Does a timeout in an SMS API mean the message was not sent?

No. It may mean that the connection failed before the provider received the request, but it may also mean that the provider received and processed it while the response was lost or delayed. Treat it as an ambiguous outcome until you can reconcile it.

What should an idempotency key for SMS contain?

It should identify a specific business intent. It can be linked to an internal event or OTP challenge identifier, recipient, channel, and content version. It should not be only the destination number or be regenerated for every retry.

Should I retry a throttling error?

It may be a candidate for a controlled retry if the provider contract classifies it as temporary. Apply rate control, progressive waiting, and limits. Do not resend immediately or without limits.

Does a DLR always confirm receipt on the phone?

Not necessarily. The meaning of each status depends on the contract and the information available across the delivery chain. You should distinguish between provider acceptance, sending toward a carrier, receipt of a DLR, and any reported delivery confirmation. A DLR should not be interpreted beyond its documented semantics.

Do callbacks replace the message creation response?

No. The synchronous response and callbacks serve different purposes. Some providers do not issue a callback for the initial status, so you must persist the creation response and reconcile subsequent changes through callbacks, queries, or DLRs.

What should I do if the provider does not support idempotency keys?

Control the intent and states in your own system before calling the provider. When there is an ambiguous timeout, prioritize querying with available identifiers and reconciling events. If no remote deduplication or query mechanism exists, document that limitation and be especially restrictive before creating a second request.

Sources consulted

  1. RFC 9110: HTTP SemanticsIETF / RFC Editor
  2. SMPP Protocol Specification v3.4, Issue 1.2SMPP Developers Forum
  3. Messages resourceTwilio
  4. Outbound Message Status in Status CallbacksTwilio
  5. Messaging ServicesTwilio
  6. Cloud Control API ReferenceAmazon Web Services
  7. AWS Well-Architected Framework: Reliability PillarAmazon Web Services