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

  1. Retry the token refresh/revocation request with backoff — serialization failures are expected to be transient.
  2. Enable/increase Hydra's SQL retry settings (max_concurrent_retries) so concurrent update errors are retried internally.
  3. Serialize refresh-token usage client-side: never issue parallel refreshes with the same refresh token.
  4. 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

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


AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03). Data as JSON: /api/errors/bf210be85ae34881. Report an issue: GitHub.