OrchardCMS/OrchardCore · error · OpenIddictExceptions.ConcurrencyException

The token was concurrently updated and cannot be persisted…

Error message

The token was concurrently updated and cannot be persisted in its current state.
Reload the token from the database and retry the operation.

What it means

OpenIddict's token store wraps YesSql optimistic-concurrency failures (ConcurrencyException from ISession.FlushAsync) into an OpenIddictExceptions.ConcurrencyException. It means another request or process modified the same token document between this store's read and write, so YesSql rejected the update to protect consistency. The operation is intentionally not retried automatically; the caller is expected to reload the token and redo the change.

Solutions

  1. Retry the operation: reload the token via the OpenIddict token manager (or TryRevokeAsync/TryRedeemAsync, which already tolerate concurrency) and re-apply the update.
  2. Prefer OpenIddict manager-level APIs (ITokenManager.TryRevokeAsync/TryRedeemAsync/TryPruneAsync) instead of raw store updates; they catch ConcurrencyException and return false instead of throwing.
  3. Reduce concurrent writes to the same token by avoiding redundant revocation/redemption calls and shortening request lifetime.
  4. If it happens persistently, check for duplicate token issuance (e.g., same refresh token used from multiple clients/instances) and enable single-use refresh tokens consistently.

Example fix

// before: raw update that throws on concurrent writes
var token = await tokenManager.FindByReferenceIdAsync(refreshToken);
await tokenManager.TryRevokeAsync(token); // may surface ConcurrencyException

// after: atomic tolerant call
var token = await tokenManager.FindByReferenceIdAsync(refreshToken);
if (!await tokenManager.TryRevokeAsync(token))
{
    // token was concurrently revoked/redeemed; reload and decide
}
Defensive patterns

Strategy: retry

Validate before calling

// No pre-call validation possible; verify the token's current state right before writing:
var token = await tokenManager.FindAsync(tokenId, ct);
if (token == null) { /* already gone; skip update */ }

Try / catch

try
{
    await tokenManager.UpdateAsync(token, ct);
}
catch (OpenIddictExceptions.ConcurrencyException)
{
    token = await tokenManager.FindAsync(tokenId, ct); // reload
    // re-apply change or give up; do not blindly retry forever
}

Prevention

When it happens

Trigger: Calling OpenIdTokenStore.UpdateAsync (typically indirectly through OpenIddict's token manager during token revocation, redemption, or extension) while a concurrent request updates the same token row, causing _session.FlushAsync to throw ConcurrencyException at commit time.

Common situations: Two simultaneous requests redeeming or revoking the same refresh token; multiple tenants/instances behind a load balancer touching the same token; background token-pruning or cleanup racing with an active authorization flow; long-running flows holding a session open while another request commits first.

Related errors


AI-assisted analysis of OrchardCMS/OrchardCore@4306c0717f (2026-09-13). Data as JSON: /api/errors/86947b99916365ea. Report an issue: GitHub.

Appendix: source

Thrown at src/OrchardCore/OrchardCore.OpenId.Core/YesSql/Stores/OpenIdTokenStore.cs:687

        return default;
    }

    /// <inheritdoc/>
    public virtual async ValueTask UpdateAsync(TToken token, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(token);

        cancellationToken.ThrowIfCancellationRequested();

        await _session.SaveAsync(token, checkConcurrency: true, collection: OpenIdCollection, cancellationToken: cancellationToken);

        try
        {
            await _session.FlushAsync(cancellationToken);
        }
        catch (ConcurrencyException exception)
        {
            throw new OpenIddictExceptions.ConcurrencyException(new StringBuilder()
                .AppendLine("The token was concurrently updated and cannot be persisted in its current state.")
                .Append("Reload the token from the database and retry the operation.")
                .ToString(), exception);
        }
    }
}

View on GitHub (pinned to 4306c0717f)