Two of the most-viewed threads on the Hangfire forum are "Enqueued jobs won't start processing" and "Jobs being created in database, stuck as Enqueued." Between them, around 40,000 views. The pattern is always the same: the dashboard shows a growing list of jobs in Enqueued, no Processing, no Failed, no error anywhere — and nobody notices until a client asks why their nightly report didn't run.
Jobs staying Enqueued means one thing: no worker is picking them up. The job rows exist in storage and wait their turn, but the worker pool that should drain them is gone, stuck, or never started. There's no Failed event because nothing actually tried and failed — the queue just isn't being drained. The most common root causes:
BackgroundJobServer isn't running anymore, so there's nothing to dequeue. Hangfire doesn't emit a "worker stopped" event; the queue just stops draining.WorkerCount misconfiguration. A server configured with a worker pool of 0 (sometimes seen as a typo in deployment config) silently enqueues everything and processes nothing. No error, no log entry."critical", the server only listens on "default". No worker will ever pick it up. Subtle and common — particularly when different teams own enqueue and worker.The instinct is to check the Hangfire dashboard. The problem is that the dashboard runs in the same process as the jobs — if the process is sick, the dashboard is sick too. If the app pool recycled, the dashboard is gone. If the worker pool is saturated, the dashboard's own requests compete with the same saturated worker pool. A dashboard that requires you to remember to load it, in a process that may not be running, isn't a monitoring solution — it's a debugging tool you use after someone has already noticed.
What you actually need is an external observer — something that runs in a different process and watches the event stream for "jobs are being enqueued but nothing is moving to Processing." That's the queue-backlog alert pattern: an alert fires when the ratio of Enqueued to Processing goes wrong over a window of time, regardless of why.
Before you build anything, here's the SQL that tells you whether you have a backlog right now (SQL Server variant; PostgreSQL is the same idea with different date syntax):
SELECT TOP 20
j.Id,
j.StateName,
s.Data,
s.CreatedAt
FROM Hangfire.Job j
LEFT JOIN Hangfire.State s ON s.Id = j.StateId
WHERE j.StateName = 'Enqueued'
ORDER BY s.CreatedAt DESC;
If the newest Enqueued row is hours old and no Processing rows exist in the same window, you have a backlog. The query works because it bypasses the dashboard and goes straight to the storage tables — but someone still has to run it. That's the gap.
The clean split is the same as for history retention: don't ask Hangfire's own process to tell you that Hangfire's own process is broken. Stream the state changes out to something that runs independently and let it alert on the pattern.
That's where QueueHawk fits. The agent streams every Enqueued, Processing, Succeeded, and Failed transition out over HTTPS as it happens — independent of whether the Hangfire server process itself is healthy. An alert rule watching the stream can fire on "N events enqueued in the last M minutes, zero moved to Processing" — exactly the pattern that means a worker is gone. The alert lands in Slack, Teams, or email before the client notices.
WorkerCount misconfiguration, assembly-load failure on a non-referencing server, queue-name mismatch.See also: getting Slack/Teams/email alerts when a Hangfire job fails or goes silent for the alert-delivery side of this, and monitoring Hangfire across multiple client apps for why this gets worse with every additional project.
Free for one application, no card required.