Note: The following is incomplete and unofficial; just something that I have been experimenting with for my own purposes, and that might be useful to others.
I have seen occasional requests for the ability to search for jobs in the Hangfire dashboard, and I would find that useful as well. So far that capability doesn’t exist in Hangfire (that I know of). I now have a preliminary version of a web page (outside of the Hangfire dashboard pages) that allows me to search and sort “succeeded” jobs. I am posting the SQL queries that I used here in case it’s helpful to anyone else.
Notes:
- This is separate from any of the Hangfire dashboard pages (not an addition to, or extension for, Hangfire); I am using it in a standalone web page with a basic grid (sortable and searchable).
- This is only the SQL code, not a complete web page. You should be able to use this in any grid or component that you use to display tables of data in your environment.
- This works in the latest version of Hangfire as of April-2026 (v1.8.23), but it will probably require adjusting for some future versions.
- The JSON functions used below require SQL Server 2016 or a later version.
- The code below only queries “Succeeded” jobs, but it could be extended to other job states. [We typically have very few jobs in other states like Failed, Deleted, etc., so I only needed to search/sort the “Succeeded” jobs.]
- I developed the SQL queries partly by looking at queries sent by Hangfire in “SQL Server Profiler” and partly by reviewing the Hangfire tables and data.
First, some Hangfire date/time values are stored as Unix epoch (UTC) timestamps (seconds since 1-Jan-1970), so I have a function like the following to convert epoch values to DATETIME2 values for a specified local time zone:
CREATE FUNCTION dbo.DateTimeEpochTimestampLocal
(
@EpochTime AS BIGINT,
@LocalTimeZone AS VARCHAR(50)
)
RETURNS DATETIME2(3)
AS
BEGIN
RETURN DATEADD(MILLISECOND, @EpochTime % 1000, DATEADD(second, @EpochTime / 1000, '1-Jan-1970')) AT TIME ZONE 'UTC' AT TIME ZONE @LocalTimeZone;
END;
For Example:
SELECT dbo.DateTimeEpochTimestampLocal(1776875837462, 'Eastern Standard Time');
Note: Time zones in SQL Server are always “Standard” time zones, but they automatically adjust for daylight saving time. In SQL Server, use this query to see all the available time zones:
SELECT name FROM sys.time_zone_info;
Note that some columns contain data in JSON format, such as this example from the State.Data column:
{“SucceededAt”:“1776875837462”,“PerformanceDuration”:“8312”,“Latency”:“2327357046”}
In SQL Server 2016 and later, you can retrieve individual elements from these values with a query like:
JSON_VALUE(HFState.Data, '$.Latency') AS Latency
The InvocationData and Arguments columns (in the “Job” table) also contain JSON-style data, but with more variability in the structure, for example:
{“t”:“System.Diagnostics.Debug, mscorlib”,“m”:“WriteLine”,“p”:[“System.String”]}
{“t”:“AppHangfire.Code.ImportRSSFeed, AppHangfire”,“m”:“ExecuteAsync”,“p”:[“System.String”,“System.String”,“Hangfire.Server.PerformContext, Hangfire.Core”,“System.Threading.CancellationToken, mscorlib”]}
[““Hello, world!””]
[““ImportRSSFeed””,““AQAAANCMn””,null,null]
Note also that some of these embedded values are arrays, such as:
“p”:[“System.String”,“System.String”,“Hangfire.Server[…]”]
You can retrieve these individual array items with SQL such as:
JSON_VALUE(HFJob.InvocationData, 'lax $.p[0]') AS InvocationP0
JSON_VALUE(HFJob.InvocationData, 'lax $.p[1]') AS InvocationP1
JSON_VALUE(HFJob.Arguments, 'lax $[0]') AS Argument0
Note the “lax” qualifier above - this means that the query will return NULL if the array element doesn’t exist. The alternative is “strict” which will raise an error if the element defined in the query doesn’t exist.
Here is the SQL query that I’m currently using to retrieve data for “Succeeded” jobs:
;WITH cte
AS (SELECT HFJob.Id,
ROW_NUMBER() OVER (ORDER BY HFJob.Id DESC) AS row_num
FROM [HangFire].Job AS HFJob WITH (NOLOCK, FORCESEEK)
WHERE HFJob.StateName = 'Succeeded')
SELECT HFJob.Id,
HFJob.StateId,
HFJob.StateName,
HFJob.CreatedAt,
HFJob.ExpireAt,
HFState.CreatedAt AS StateChanged,
JSON_VALUE(HFState.Data, '$.SucceededAt') AS SucceededAt,
dbo.DateTimeEpochTimestampLocal(JSON_VALUE(HFState.Data, '$.SucceededAt'), 'Eastern Standard Time') AS SucceededAtLocalTime,
JSON_VALUE(HFState.Data, '$.PerformanceDuration') AS PerformanceDuration,
JSON_VALUE(HFState.Data, '$.Latency') AS Latency,
HFState.Reason AS StateReason,
JSON_VALUE(HFJob.InvocationData, '$.t') AS InvocT,
JSON_VALUE(HFJob.InvocationData, '$.m') AS InvocM,
JSON_VALUE(HFJob.InvocationData, 'lax $.p[0]') AS InvocP0,
JSON_VALUE(HFJob.InvocationData, 'lax $.p[1]') AS InvocP1,
JSON_VALUE(HFJob.InvocationData, 'lax $.p[2]') AS InvocP2,
JSON_VALUE(HFJob.InvocationData, 'lax $.p[3]') AS InvocP3,
JSON_VALUE(HFJob.Arguments, 'lax $[0]') AS Arg0,
JSON_VALUE(HFJob.Arguments, 'lax $[1]') AS Arg1,
JSON_VALUE(HFJob.Arguments, 'lax $[2]') AS Arg2,
JSON_VALUE(HFJob.Arguments, 'lax $[3]') AS Arg3,
HFState.Data AS StateData,
HFJob.InvocationData,
HFJob.Arguments
FROM [HangFire].Job AS HFJob WITH (NOLOCK, FORCESEEK)
INNER JOIN cte
ON cte.Id = HFJob.Id
LEFT JOIN [HangFire].State AS HFState WITH (NOLOCK, FORCESEEK)
ON HFJob.StateId = HFState.Id
AND HFJob.Id = HFState.JobId;
Depending on how you use this query, you may want to add an “ORDER BY” clause at the end of the query.
And, if you are using this with paging in your grid, you may also need to add something like the following to the end of the query (before the “ORDER BY” clause):
WHERE cte.row_num BETWEEN @start AND @end
And have your grid code set the “@start” and “@end” values appropriately to page through the results (for example, to 21 and 40 to retrieve the second set of 20 records).