HangfireIO/Hangfire · error · InvalidOperationException

Storage transaction class must inherit the JobStorageTransac

Error message

Storage transaction class must inherit the JobStorageTransaction class to use transactional acknowledge

What it means

Thrown by BackgroundJobStateChanger when a state change requests transactional acknowledge (CompleteJob set) but the storage's write transaction does not inherit from JobStorageTransaction. Transactional acknowledge requires RemoveFromQueue on the same transaction that applies the state, so only storages whose transaction is a JobStorageTransaction can participate. A plain IWriteOnlyTransaction cannot.

Source

Thrown at src/Hangfire.Core/States/BackgroundJobStateChanger.cs:159

                    // State changing process can fail due to an exception in state filters themselves,
                    // and DisableFilters property will cause state machine to perform a state transition
                    // without calling any filters. This is required when all the other state change
                    // attempts failed and we need to remove such a job from the processing pipeline.
                    // In this case all the filters are ignored, which may lead to confusion, so it's
                    // highly recommended to use the DisableFilters property only when changing state
                    // to the FailedState.
                    var stateMachine = context.DisableFilters ? _stateMachine.InnerStateMachine : _stateMachine;
                    var appliedState = stateMachine.ApplyState(applyContext);

                    if (context.CompleteJob != null)
                    {
                        if (transaction is JobStorageTransaction jobStorageTransaction)
                        {
                            jobStorageTransaction.RemoveFromQueue(context.CompleteJob);
                        }
                        else
                        {
                            throw new InvalidOperationException("Storage transaction class must inherit the " + nameof(JobStorageTransaction) + " class to use transactional acknowledge");
                        }
                    }

                    if (context.Transaction == null)
                    {
                        transaction.Commit();
                    }

                    context.ProcessedJob = backgroundJob;
                    return appliedState;
                }
            }
        }

        private static JobData GetJobData(StateChangeContext context)
        {
            // This code was introduced as a fix for an issue, which appeared when an
            // external queue implementation was used together with a non-linearizable

View on GitHub (pinned to c236dd0f93)

Solutions

  1. If you control the storage, make its write-transaction class inherit JobStorageTransaction and implement RemoveFromQueue.
  2. Disable transactional acknowledge for that storage (don't set CompleteJob / use a non-transactional-ack configuration).
  3. Use a storage that supports transactional acknowledge (e.g. SqlServer, Redis with proper transaction).

Example fix

// before — custom transaction that is only IWriteOnlyTransaction
class MyTransaction : IWriteOnlyTransaction { ... }
// → throws when CompleteJob is set

// after — inherit JobStorageTransaction and implement RemoveFromQueue
class MyTransaction : JobStorageTransaction
{
    public override void RemoveFromQueue(string jobId) { /* dequeue */ }
}
Defensive patterns

Strategy: type-guard

Validate before calling

if (context.CompleteJob != null && !(transaction is JobStorageTransaction))
{
    throw new InvalidOperationException("Enable transactional acknowledge only with a JobStorageTransaction-based storage.");
}

Type guard

bool SupportsTransactionalAcknowledge(JobStorage storage)
    => storage.GetConnection().CreateWriteTransaction() is JobStorageTransaction;

Prevention

When it happens

Trigger: Using a StateChangeContext with CompleteJob set (enabling transactional ack) against a storage whose CreateWriteTransaction returns a type that is not a JobStorageTransaction. This is determined at state-change time in the performer.

Common situations: A custom or third-party storage that implements IWriteOnlyTransaction directly instead of deriving from JobStorageTransaction, combined with transactional-acknowledge enabled processing. Mixing an external-queue storage without the transactional-ack contract.

Related errors


AI-assisted analysis of HangfireIO/Hangfire@c236dd0f93 (2026-08-13). Data as JSON: /api/errors/7892fd11f4af9176. Report an issue: GitHub.