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
- Only enable UseTransactionalAcknowledge when all queues are served by the default SqlServerJobQueueProvider (so fetched jobs are SqlServerTimeoutJob).
- Disable UseTransactionalAcknowledge for any queues backed by external/custom providers.
- For a custom provider that must support transactional acknowledge, implement the removal within that provider rather than relying on SqlServerWriteOnlyTransaction.RemoveFromQueue.
- 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
- Enable UseTransactionalAcknowledge only when every queue is served by the default SQL Server provider.
- Before mixing in an external/custom queue provider, disable transactional acknowledge for those queues.
- Custom providers should implement their own transactional removal instead of relying on SqlServerWriteOnlyTransaction.RemoveFromQueue.
- Register the RemoveFromQueue feature in a provider only for the concrete IFetchedJob type it produces.
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- `DisableTransactionScope` option does not support external q
- Could not get a value of the job parameter `{name}`. See inn
- The type `{method.DeclaringType}` must be derived from the `
- Storage transaction class must inherit the JobStorageTransac
AI-assisted analysis of HangfireIO/Hangfire@c236dd0f93 (2026-08-13).
Data as JSON: /api/errors/b1f75f68b591a64b.
Report an issue: GitHub.