A password-reset link may be described as “single use”, yet two requests arriving almost together can both see the token as unused. If each request then continues independently, the application can perform a security-sensitive action twice even though its data model contains a used flag.
This is a concurrency problem with security consequences. The same pattern can affect account invitations, email-verification links, recovery codes, approval links, and other credentials that are supposed to grant authority once.
The defensive rule is simple: checking that a token is unused and consuming it must be one atomic state transition. This article explains why separate check-and-update steps fail, how atomic consumption changes the outcome, and where the boundary should sit when the token authorizes a larger workflow.
“One-time” is a state transition, not a field
A one-time token normally has at least two relevant states:
UNUSED -> CONSUMEDThe security property is not merely that the database can record CONSUMED. It is that only one competing request can successfully move the token from UNUSED to CONSUMED.
Imagine a reset-token record with an identifier, an expiry time, and a consumption timestamp. A straightforward implementation might do this:
record = find_token(token)
if record == NONE:
reject
if record.expires_at <= now:
reject
if record.consumed_at != NONE:
reject
mark_consumed(record)
reset_password(record.user_id, new_password)For a single request, the logic looks reasonable. The problem appears when two requests execute concurrently.
Request A reads the record and sees consumed_at = NONE. Before A writes anything, request B reads the same record and sees the same value. Both requests have now passed the check. If the later update is unconditional, both can continue as though they were the unique consumer.
The bug exists because “is unused?” and “make consumed” are separate operations. The application made a decision using state that another request could change before the decision took effect.
Make the database decide which request wins
The smallest useful defense is a conditional state change. Instead of reading an unused token and later marking it used, ask the data store to perform the change only if the token is still eligible.
In SQL-like pseudocode:
UPDATE security_tokens
SET consumed_at = :now
WHERE token_id = :token_id
AND consumed_at IS NULL
AND expires_at > :now;Then inspect how many rows were changed:
1 row changed -> this request consumed the token
0 rows changed -> reject the tokenThe condition and update are evaluated as one database operation. If two requests race, they cannot both change the same row from consumed_at IS NULL under the normal atomic semantics of an individual update. One request can win the transition; after that transition, the other request’s predicate no longer matches.
This is the core mental model:
request A ----\
>-- conditional consume --> one winner
request B ----/ --> others failThe exact API differs across databases and persistence layers. Some systems use a conditional update, compare-and-set operation, transaction with row locking, or another concurrency primitive. The portable requirement is more important than the mechanism: there must be no gap in which multiple callers can all successfully claim the same one-time authority.
Validate the conditions that belong to consumption
Atomicity helps only if the atomic operation checks the state that determines whether consumption is allowed.
For a simple token, that commonly includes its identity, whether it has already been consumed, and whether it is still within its accepted lifetime. If the token is also bound to a purpose or account, those bindings need to be enforced before the authorized action proceeds as well.
Consider an application that first checks expiry in application code and then performs an unconditional consume:
if token.expires_at > now:
mark_consumed(token.id)There is still a boundary between the decision and the write. In many systems the practical race around expiry may be small, but security-sensitive state should not depend on timing luck when the data store can enforce the relevant condition directly.
Be precise about time semantics. If a token is valid strictly before its expiry instant, use the equivalent of expires_at > now. If the documented policy includes the exact expiry instant, the comparison changes. Pick one rule and use it consistently in validation, tests, and user-facing behavior.
The same principle applies to revocation. If a token can be revoked, a request should not be able to pass a stale “not revoked” check and consume it after another operation has revoked it. Put security-relevant eligibility conditions into the same protected transition when the storage model supports that design.
Separate possession from successful consumption
A valid token is evidence that the caller possesses a credential. Successful atomic consumption is a separate fact: this request obtained the right to use that credential now.
That distinction matters in application structure. Avoid code that performs a sensitive action merely because token parsing or cryptographic verification succeeded. The action should occur only after the request has successfully claimed the one-time state transition.
A useful flow is:
receive token
|
v
validate representation and authenticity
|
v
atomically claim eligible UNUSED -> CONSUMED
| |
success failure
| |
v v
perform authorized action rejectFor an opaque random token stored server-side, “validate representation and authenticity” may simply mean deriving the lookup value safely and finding the corresponding record. For a signed token, signature validation may happen before the database operation, but a signature alone does not make a token single-use. If single-use behavior matters, the server still needs authoritative state, or an equivalent mechanism, that records whether the grant has already been consumed.
Decide what must be atomic with the protected action
Consuming the token atomically solves the double-consumption race, but it introduces the next design question: what if token consumption succeeds and the action it authorizes fails?
Suppose a password-reset request consumes the token and then the password update fails because the database transaction is rolled back, a dependent service is unavailable, or an internal error occurs. If consumption and the password change are independent writes, the user may lose a valid reset token without getting a new password.
When the token state and protected state live in the same transactional data store, a transaction can often make the intended change clearer:
begin transaction
atomically claim token
if claim failed:
rollback and reject
update authorized account state
commitUnder this design, either both changes commit or neither does, subject to the guarantees of the chosen database and transaction configuration.
Do not generalize that guarantee across unrelated systems. A database transaction cannot normally make an external email provider, payment API, message broker, and database commit as one indivisible operation. If token use triggers remote side effects, you need a deliberate failure and retry model.
Often the right boundary is to commit the security-sensitive local state first, then arrange downstream work so retries are safe. An outbox or idempotent consumer can help in some architectures, but those are broader reliability patterns rather than properties of one-time tokens themselves.
Do not “fix” failures by making consumed tokens reusable
A tempting recovery strategy is to set the token back to unused whenever later work fails. That can recreate the race you were trying to remove and may be unsafe when you cannot prove whether the protected action partially succeeded.
For example, a remote operation can time out after the remote service accepted it. The caller sees a failure, but the effect may already exist. Re-enabling the same authorization blindly can allow a second execution.
Recovery should follow the semantics of the protected action. If all relevant changes are in one local transaction, rollback can restore the pre-consumption state coherently. If external effects are involved, record enough state to distinguish “not started”, “in progress”, “completed”, and cases that need reconciliation rather than simply turning a consumed credential back on.
For user-facing recovery flows, issuing a fresh token after a controlled retry or restart is often easier to reason about than resurrecting an old one. The exact choice depends on the action and threat model.
Treat retries as normal, not exceptional
Duplicate requests do not require a malicious actor. Browsers retry, mobile networks reconnect, users double-click, reverse proxies retry under some configurations, and clients can repeat a request after losing a response.
A sound one-time-token design should therefore have a defined result for a repeated request. Once the token is consumed, later attempts should not perform the protected transition again.
What the user sees is a product decision. A password-reset page might say that the link is no longer valid and offer a way to request a new one. An API may return a generic invalid-or-expired result to avoid exposing unnecessary token state. The security property is the same even when the presentation differs.
Be careful with automatic retries inside application code. Retrying a failed conditional consume is harmless only if the retry preserves the condition. Replacing a failed conditional operation with an unconditional write defeats the protection.
Test the race, not only the happy path
A unit test that consumes a token and then tries it again sequentially proves only this sequence:
consume -> finish -> consume againThe dangerous sequence is concurrent:
request A: attempt consume ----\
+--> overlap
request B: attempt consume ----/Add a concurrency test at the persistence boundary. Start multiple workers with the same valid token and arrange for their consume attempts to overlap. Assert that exactly one operation reports successful consumption.
Then test the boundary conditions separately: expired token, already consumed token, revoked token if supported, unknown token, and a successful consume followed by the protected state change. If consumption and the protected update share a transaction, test that a forced failure before commit leaves neither change committed.
Concurrency tests can be sensitive to the real data store and its isolation behavior, so an integration test is usually more informative than a mock for this property. A mock that executes calls sequentially may accidentally hide the race.
Know what atomic consumption does not protect
Atomic consumption addresses a narrow threat: multiple requests successfully exercising authority that was intended for one use. It does not make the token secret, authenticate the intended person by itself, or protect a token that has already been stolen.
If an attacker obtains a bearer reset token and consumes it before the legitimate user, atomicity simply ensures that the attacker wins once. Token confidentiality, sufficient randomness for opaque tokens, appropriate lifetimes, secure delivery, purpose binding, and careful recovery design remain separate controls.
Atomic consumption also does not stop an authorized one-time action from being dangerous. A token that grants excessive authority still grants excessive authority once. Bind the credential to the narrow action and subject it is meant to authorize.
Finally, this control does not provide exactly-once execution across arbitrary distributed systems. “Exactly one request can claim this database state transition” is a much narrower and more defensible guarantee.
Use the simplest primitive that preserves the invariant
You do not need elaborate distributed coordination when one authoritative data store owns the token state and provides an atomic conditional write. In that common case, a single conditional update plus a check of the affected-row count may be enough.
Use stronger coordination when the invariant spans multiple records, multiple state changes, or systems whose consistency model does not support the required conditional transition directly. The implementation may then need a transaction, lock, version check, or storage-specific compare-and-set primitive.
The decision should start from the invariant, not from a favorite concurrency tool:
For each one-time grant, at most one request may successfully claim the right to exercise it.
Write that property down, choose the smallest mechanism your authoritative store provides to enforce it, and test it with overlapping requests. A used field records history. An atomic transition is what makes the grant one-time.