ory/hydra · error · fosite.RFC6749Error
server_error
server_error
Error message
serialization failure
What it means
deleteSessionByRequestID translates SQL write errors during token deletion (refresh-token rotation via strict/gracefulRefreshRotation, RevokeRefreshToken, RevokeAccessToken) into fosite.ErrSerializationFailure when the driver reports a concurrent-update conflict or an InnoDB deadlock (MySQL "Error 1213"). The hint message is "serialization failure" with code server_error, telling the OAuth2 layer the DELETE lost a race and should be retried.
Source
Thrown at persistence/sql/persister_oauth2.go:364
return err
}
func (p *Persister) deleteSessionByRequestID(ctx context.Context, id string, table tableName) (err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.deleteSessionByRequestID")
defer otelx.End(span, &err)
err = p.QueryWithNetwork(ctx).
Where("request_id=?", id).
Delete(OAuth2RequestSQL{Table: table}.TableName())
if errors.Is(err, sql.ErrNoRows) {
return errors.WithStack(fosite.ErrNotFound)
}
if err := sqlcon.HandleError(err); err != nil {
if errors.Is(err, sqlcon.ErrConcurrentUpdate()) {
return fosite.ErrSerializationFailure.WithWrap(err)
}
if strings.Contains(err.Error(), "Error 1213") { // InnoDB Deadlock?
return errors.Wrap(fosite.ErrSerializationFailure, err.Error())
}
return err
}
return nil
}
func (p *Persister) flushInactiveTokens(ctx context.Context, notAfter time.Time, limit int, batchSize int, table tableName, lifespan time.Duration) (err error) {
/* #nosec G201 table is static */
// The value of notAfter should be the minimum between input parameter and token max expire based on its configured age
requestMaxExpire := time.Now().Add(-lifespan)
if requestMaxExpire.Before(notAfter) {
notAfter = requestMaxExpire
}
totalDeletedCount := 0
for deletedRecords := batchSize; totalDeletedCount < limit && deletedRecords == batchSize; {
d := batchSize
if limit-totalDeletedCount < batchSize {View on GitHub (pinned to 4174065ffb)
Solutions
- Retry the token refresh/revocation request with backoff — serialization failures are expected to be transient.
- Enable/increase Hydra's SQL retry settings (max_concurrent_retries) so concurrent update errors are retried internally.
- Serialize refresh-token usage client-side: never issue parallel refreshes with the same refresh token.
- On MySQL, inspect deadlocks (SHOW ENGINE INNODB STATUS) and consider indexing/locking tuning on the session tables.
Defensive patterns
Strategy: retry
Try / catch
if errors.Is(err, fosite.ErrSerializationFailure) {
time.Sleep(backoff)
// retry RevokeRefreshToken / refresh rotation
} Prevention
- Never issue parallel refresh or revocation calls with the same token; single-flight client-side.
- Enable SQL retry configuration (max_concurrent_retries) in deployments.
- On MySQL, monitor deadlocks and ensure proper indexes on session tables.
When it happens
Trigger: Concurrent refresh-token rotation: two refresh requests using the same grant simultaneously, causing racing DELETEs of the old session rows; a deadlocked DELETE on the oauth2 flow/session table under MySQL; two replicas revoking the same token concurrently.
Common situations: Aggressive client token refresh (parallel requests), graceful refresh rotation windows with overlapping refreshes, MySQL default isolation with lock contention, load tests hammering refresh endpoint.
Related errors
- server_error
- invalid_request
- issuer URL must be set unless development mode is enabled
- issuer URL scheme must be HTTPS unless development mode is e
- The DSN connection string looks like a SQLite connection, bu
AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03).
Data as JSON: /api/errors/bf210be85ae34881.
Report an issue: GitHub.