HangfireIO/Hangfire · error · NotSupportedException

Only 'SqlServerTimeoutJob' type supports transactional ackno

Error message

Only 'SqlServerTimeoutJob' type supports transactional acknowledge, '{fetchedJob.GetType().Name}' given.

What it means

NotSupportedException thrown in SqlServerWriteOnlyTransaction.RemoveFromQueue (SqlServerWriteOnlyTransaction.cs:556) when the IFetchedJob passed in is not the internal SqlServerTimeoutJob type. This RemoveFromQueue overload is the transactional-acknowledge deletion path: SqlServerStorage registers the RemoveFromQueue(typeof(SqlServerTimeoutJob)) feature gated by UseTransactionalAcknowledge (SqlServerStorage.cs:517-519). Only SqlServerTimeoutJob carries the Queue/Id/FetchedAt fields needed for the parameterized DELETE and the SetTransaction wiring, so any other IFetchedJob implementation is rejected.

Source

Thrown at src/Hangfire.SqlServer/SqlServerWriteOnlyTransaction.cs:556

        public override void RemoveFromQueue(IFetchedJob fetchedJob)
        {
            if (fetchedJob == null) throw new ArgumentNullException(nameof(fetchedJob));

            if (fetchedJob is SqlServerTimeoutJob timeoutJob)
            {
                var query = _storage.GetQueryFromTemplate(static schemaName =>
$@"delete JQ from [{schemaName}].JobQueue JQ with (forceseek, rowlock) where Queue = @queue and Id = @id and FetchedAt = @fetchedAt");

                AddCommand(_queueCommands, timeoutJob.Queue, batch => batch.Create(query)
                    .AddParameter("@queue", timeoutJob.Queue, DbType.String)
                    .AddParameter("@id", timeoutJob.Id, DbType.Int64)
                    .AddParameter("@fetchedAt", timeoutJob.FetchedAt, DbType.DateTime));

                timeoutJob.SetTransaction(this);
            }
            else
            {
                throw new NotSupportedException(
                    "Only '" + nameof(SqlServerTimeoutJob) + "' type supports transactional acknowledge, '" + fetchedJob.GetType().Name + "' given.");
            }
        }

        private static void AppendBatch<TKey>(
            SortedDictionary<TKey, List<Func<DbConnection, DbCommand>>> collection,
            SqlCommandBatch batch)
        {
            foreach (var pair in collection)
            {
                foreach (var command in pair.Value)
                {
                    var dbCommand = command(batch.Connection);
                    batch.Append(dbCommand);
                }
            }
        }

View on GitHub (pinned to c236dd0f93)

Solutions

  1. Only enable UseTransactionalAcknowledge when all queues are served by the default SqlServerJobQueueProvider (so fetched jobs are SqlServerTimeoutJob).
  2. Disable UseTransactionalAcknowledge for any queues backed by external/custom providers.
  3. For a custom provider that must support transactional acknowledge, implement the removal within that provider rather than relying on SqlServerWriteOnlyTransaction.RemoveFromQueue.
  4. Register the RemoveFromQueue feature in your provider only for the IFetchedJob type it actually produces.

Example fix

// before
var options = new SqlServerStorageOptions { UseTransactionalAcknowledge = true };
// but an external/custom queue provider is registered for some queue
// after — disable transactional acknowledge when external providers are in use
var options = new SqlServerStorageOptions { UseTransactionalAcknowledge = false };
Defensive patterns

Strategy: type-guard

Type guard

// Guard before calling transactional RemoveFromQueue:
if (fetchedJob is Hangfire.SqlServer.SqlServerTimeoutJob sqlJob)
{
    transaction.RemoveFromQueue(fetchedJob);
}
else
{
    // Use the provider-appropriate removal path; do not pass foreign IFetchedJob instances.
    fetchedJob.RemoveFromQueue();
}

Prevention

When it happens

Trigger: Enable options.UseTransactionalAcknowledge = true and have a worker fetch a job from a non-default queue provider (MSMQ or a custom IPersistentJobQueueProvider) so the fetched job is not a SqlServerTimeoutJob; the transactional-acknowledge machinery then calls RemoveFromQueue with that foreign IFetchedJob and throws. Also reproducible by passing a custom IFetchedJob instance into RemoveFromQueue directly.

Common situations: Mixing the default SQL Server queue with MSMQ or custom queue providers while transactional acknowledge is on; a custom queue provider whose IFetchedJob implementation is fed into the SQL transaction's RemoveFromQueue; enabling UseTransactionalAcknowledge globally without checking that all queues are SQL-backed.

Understand the failure class

Related errors


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