Concurrency in SMS OTP Sending: States, Locks, and Retries
A practical guide to coordinating concurrent OTP requests, reducing duplicates, and handling timeouts carefully when the sending outcome is uncertain.

The problem: multiple requests for a single verification
A repeated tap, two open tabs, or several requests arriving almost simultaneously can start more than one OTP flow. Two workers may also process equivalent requests at the same time. Possible results include generating different codes, creating multiple sending attempts, or leaving the user with a code that no longer matches the application’s state.
Rate limits and temporary locks can help control request frequency, but they do not, by themselves, guarantee that two concurrent processes will not claim the same operation. Coordination must also be handled in the storage or shared mechanism used by the application.
- Check whether concurrent requests belong to the same verification, not just whether they came from the same number.
- Separate protection against excessive requests from prevention of duplicate processing.
- Do not interpret the absence of an immediate response as proof that sending did not start.

Separate the verification, the code, and the sending attempt
Model three elements separately: the authentication request, the OTP associated with that request, and each attempt to send the message. This separation makes it easier to distinguish a new verification from a resend and to preserve the history even when the code’s state changes.
Before defining rules, establish what “the same operation” means in your product. A repeated request for an active verification may need to be handled differently from a new verification started after the previous one is completed, canceled, or expires.
- Associate each verification with a stable internal ID.
- Record each sending attempt with its own ID and a reference to the verification.
- Do not use the phone number as the only deduplication signal: the same user may legitimately start verifications for different purposes or sessions.

Define an operation key with a clear scope
An operation key lets equivalent requests converge on the same control record. It may consist of a user or session ID, the authentication purpose, and a time window defined by the service policy. The specific choice depends on how sessions and verifications are scoped in the application; there is no universally applicable combination.
The time window should group repetitions of an operation, not block new verifications indefinitely. Keep the deduplication key separate from rate-limit or temporary-lock rules: they address related but different goals.
- Define which elements make two requests equivalent.
- Use an explicit window and document when a new operation is considered to have started.
- Avoid including unnecessary sensitive data in a visible key or operational logs.
Use explicit states and atomic claiming
Represent the lifecycle with clear states, for example: requested, processing, accepted by the provider, outcome uncertain, completed, failed, or expired. Adapt the names and transitions to the information your system and provider actually return; do not mark a send as accepted merely because an internal task was created.
The transition that claims an operation must be atomic. In practice, a worker tries to change the record from a claimable state to “processing” and continues only if that transition succeeds. If another worker has already claimed it, the second worker should check the existing state and follow the defined policy rather than starting another send by default.
Atomicity depends on the coordination mechanism in use. An in-memory lock in a single process does not, by itself, coordinate multiple application instances. Make sure the selected control works across all workers that could receive the same operation.
- Define the states and permitted transitions before implementing retries.
- Make claiming and recording the state an indivisible operation in the shared mechanism.
- Decide what response a repeated request receives when the operation is already in progress: wait, check the status, or return an “in progress” response.
- Provide a controlled way to recover abandoned operations without automatically turning every old operation into a new send.
Define when to reuse the code and when to start another
A duplicate request during a still-valid verification can be treated as a query or repetition of the same operation. If the security and user-experience policy allows it, the application can keep the current OTP and avoid generating another code. This decision should be coordinated with expiration and the system’s invalidation rules.
Generating a new code may be appropriate when a new verification begins, when the previous code is no longer valid, or when a documented policy requires replacing it. In that case, decide what happens to previous codes and ensure validation applies that rule consistently. There is no single policy that suits every application.
- Document which event constitutes a new verification and which is only a repeated request.
- Specify how a valid OTP behaves when a request is repeated.
- If a new code is issued, explicitly define whether the previous one remains valid or is invalidated.
- Align the interface response with the actual state: do not say a new code was sent if the application has not started a new attempt.
Treat timeouts as an uncertain outcome
If the connection is interrupted or the wait time expires after a request to the provider has started, it may not be possible to determine whether the provider received or accepted it. A timeout describes what your application observed; it does not, by itself, prove that the SMS was not sent.
Record the attempt as uncertain when you cannot confirm the outcome. Before retrying, check whether the sending API or connection provides an idempotency key, a status lookup, or another documented mechanism for recognizing the same request. If there is no verifiable way to resolve the outcome, apply an explicit risk policy and communicate the uncertainty to the requesting system; avoid blind automatic retries.
- Keep the internal attempt ID and any reference returned by the provider.
- Distinguish “no response was received” from “the provider rejected the request.”
- If a new attempt is authorized, record it as such and link it to the uncertain attempt.
- Do not promise that the user will receive only one SMS when the first attempt’s outcome cannot be confirmed.
Internal deduplication and provider idempotency are not the same
Internal deduplication prevents your platform from processing an operation more than once when it considers requests equivalent. Idempotency in a sending API, when documented and available, lets the provider recognize repeated requests according to that API’s rules. One does not imply the other.
Before relying on an external guarantee, consult the technical documentation for the API or connection in use. Verify the key’s scope, how long it is retained, what response is returned for a repeat, and whether it actually covers the timeout scenario you need to resolve. Do not assume that HTTP or SMPP, by themselves, provide business-level deduplication for an OTP.
- Keep an internal control even if the provider offers idempotency.
- Do not blindly resend the same key without knowing its documented scope and behavior.
- If there is no documented guarantee, treat repeated sends as potentially duplicative.
Preserve traceability and limits on interpretation
Record the operation ID, the ID of each attempt, state transitions, the reason for a resend, and the result reported by the provider. Limit the personal data recorded and apply access and retention controls appropriate to your obligations. These records help reconstruct what each component decided when concurrent requests arrived.
Describe a provider-reported state as such. Do not turn it into a claim that the message reached the phone unless there is evidence to demonstrate that. Sending records are useful for operations and diagnosis; they do not guarantee that the user received the SMS or completed verification.
- Keep separate the result of the request to your API, the result reported by the provider, and the outcome of OTP verification.
- Record the reason for a resend and which person or component authorized it.
- Use correlatable IDs in internal logs without exposing the OTP in telemetry.
- Test repeated and parallel requests, failures between transitions, and timeouts with unconfirmed outcomes.
Frequently asked questions
Does an operation key guarantee that only one SMS will be sent?
Not on its own. It can help prevent your application from processing an equivalent operation more than once if claiming is coordinated atomically. It does not guarantee that a provider received only one request or that the message will reach the handset only once.
Should I generate a new OTP every time the user taps “Resend”?
There is no universal rule. Define whether the action repeats an active verification or starts a new one, and specify how the previous code is handled. The policy should be consistent across generation, expiration, invalidation, and validation.
What should I do when sending times out?
Treat the outcome as uncertain if you cannot determine whether the provider received the request. Check the documented mechanisms available and do not assume the message was not accepted. If another attempt is allowed, record the decision and link it to the previous attempt.
Does a delivery status confirm that the SMS reached the phone?
Do not equate the two without specific evidence. Record the status as a result reported by the provider, and distinguish that signal from confirmed handset receipt and successful authentication.