WJb vs Quartz vs Hangfire Benchmarks
Performance comparison of WJb, Quartz.NET, and Hangfire using validated benchmarks with normalized concurrency (1 worker/thread per framework).
Benchmark Environment
BenchmarkDotNet v0.15.8
.NET 10.0
Windows 11
Intel Core i7-12700K
20 Logical Cores
Scenario
Each framework processes the same payload:
public sealed record EchoPayload(int Id, string Name);
Job implementation:
Interlocked.Increment(ref Completed);
Validation tests verify:
- All jobs complete successfully.
- Completion counts match expectations.
- Enqueue-only benchmarks do not execute jobs.
- End-to-end benchmarks execute every job exactly once.
Concurrency Configuration
To ensure an apples-to-apples comparison:
WJb : 1 worker
Hangfire : WorkerCount = 1
Quartz.NET : MaxConcurrency = 1
Enqueue Benchmark
Measures only job submission cost.
- No workers running.
- No job execution.
- Clean storage created for every iteration.
Results
| JobCount | WJb | Quartz.NET | Hangfire |
|---|---|---|---|
| 1 | 24.12 µs | 30.63 µs | 379.25 µs |
| 100 | 330.59 µs | 482.30 µs | 31.53 ms |
| 1,000 | 3.68 ms | 4.39 ms | 233.22 ms |
| 10,000 | 25.54 ms | 33.88 ms | 2,381.42 ms |
Enqueue Memory Usage
| JobCount | WJb | Quartz.NET | Hangfire |
|---|---|---|---|
| 1 | 1.59 KB | 4.58 KB | 26.19 KB |
| 100 | 138.1 KB | 371.4 KB | 2.14 MB |
| 1,000 | 1.35 MB | 3.77 MB | 21.41 MB |
| 10,000 | 13.41 MB | 36.24 MB | 213.83 MB |
End-to-End Benchmark
Measures the complete pipeline:
Enqueue → Schedule → Execute → Complete
Results
| JobCount | WJb | Quartz.NET | Hangfire |
|---|---|---|---|
| 100 | 668.6 µs | 11.24 ms | 40.86 ms |
| 1,000 | 7.44 ms | 26.41 ms | 259.72 ms |
| 10,000 | 39.94 ms | 89.09 ms | 2,598.00 ms |
End-to-End Memory Usage
| JobCount | WJb | Quartz.NET | Hangfire |
|---|---|---|---|
| 100 | 235.6 KB | 762.9 KB | 7.23 MB |
| 1,000 | 2.37 MB | 7.53 MB | 71.50 MB |
| 10,000 | 23.92 MB | 70.92 MB | 714.67 MB |
Notes
- In-memory storage only.
- Startup and shutdown costs are excluded.
- Validation tests are included in the benchmark suite.
- Concurrency is normalized across frameworks.
- Results reflect lightweight job workloads and framework overhead rather than heavy business logic execution.


Top comments (6)
Publishing the BenchmarkDotNet suite with a validation step that checks completion counts is good practice. Reading JobEnqueueBenchmarks.cs, though, the three enqueue benchmarks aren't measuring the same thing yet:
Starting no workers in any framework during the enqueue benchmark (or starting them in all three), and clearing the stores in IterationSetup, would make the enqueue column an apples-to-apples comparison. The JobExecuteBenchmarks file would then be the place for end-to-end throughput.
Thanks for the detailed review and suggestions.
I've updated the benchmark suite based on your feedback:
Enqueue and End-to-End benchmarks are now separated.
No workers are running during enqueue benchmarks.
Storages are recreated in IterationSetup, so every iteration starts from a clean state.
Validation tests were added to verify:
all jobs execute successfully,
completion counts match expectations,
enqueue-only benchmarks do not execute jobs,
end-to-end benchmarks execute jobs exactly once.
Concurrency is now explicitly normalized to a single worker/thread across all frameworks:
WJb: 1 worker
Hangfire: WorkerCount = 1
Quartz.NET: thread pool limited to 1 worker
I reran both benchmark suites after these changes. The updated results are available in the repository together with the revised methodology and validation tests.
Thank you again for taking the time to review the original benchmark. The feedback helped improve the benchmark design and make the comparisons much more apples-to-apples.
Best regards,
Alexander
github.com/UkrGuru/WJb.Demo/tree/main
Good changes, the validation counts are what make the numbers believable now. One difference left in the end-to-end suite that I'd state in the README: Hangfire's BackgroundJobServer and the Quartz scheduler are started in IterationSetup, so they start executing while the enqueue loop is still running (two threads overlapping). WJb enqueues all N and only then runs ExecuteLoopAsync on the benchmark thread, with no overlap and no thread hand-off. Each is a fair description of how the library is used, but they time different pipelines. Two ways to make it like-for-like: start the Hangfire server (and call Quartz Start) after the enqueue loop, or add a second WJb variant that runs its loop concurrently with enqueue. Either one, or a note, means readers won't take the gap at 10,000 jobs as pure per-job overhead.
maybe test 1M will more interested for you
Results
dev.to/ukrguru/1m-end2end-wjb-vs-q...
Thanks, the 1M suite fixes what I flagged: all three now start their workers before the enqueue loop, so the pipelines overlap the same way. Two things I'd still note next to the table, because they change how the numbers read:
GC.GetTotalMemory(false)taken after completion, minus a baseline from before. That is heap still held at the end (including garbage not yet collected), not peak. With 1M payload records plus per-job objects, the order can change if you force a collect first or report peak working set. Running each framework 3 times and giving the median would also show the run-to-run spread.ScheduleJobeach time, and Hangfire serializes the expression perEnqueue. That is each library's real per-job cost, so the table is a fair "how it's used" comparison, but 299 s for Hangfire looks like contention between the single worker and the enqueue thread on MemoryStorage rather than serialization alone. I haven't profiled it, so I'd call it a hypothesis; a variant that enqueues all 1M first and then starts the server would separate the two.Thanks.
Good points.
For memory reporting, the current numbers are managed heap usage at the end of the run, not peak memory. I'll clarify that in the README.
Regarding Hangfire, that is a reasonable hypothesis. The current large-scale benchmark intentionally measures enqueue + processing under a running worker, since all three frameworks are configured that way. A separate "enqueue all first, then start workers" variant could be interesting as a future experiment.
Thanks again for the review and for helping improve the benchmark suite.