TickerQ: The Source-Generated Job Scheduler That Makes Hangfire Feel Ancient

TickerQ: The Source-Generated Job Scheduler That Makes Hangfire Feel Ancient

Your job scheduler is probably the oldest pattern in your codebase. Hangfire or Quartz.NET, running in production, doing its thing. It works. But every time you touch the scheduling code, you wince a little. Magic strings mapping to methods. Reflection-based serialization that blows up after a rename. XML config files nobody wants to read. And that question you keep putting off: will any of this survive AOT publishing?

With .NET pushing native AOT and trimming harder than ever, your scheduler shouldn't be the thing holding you back. TickerQ is a new open-source .NET job scheduler built from scratch for this era. Source-generated at compile time, fully trimmable, with a free real-time dashboard. I set it up with PostgreSQL persistence last week and the entire configuration was under 30 lines of meaningful code.


What TickerQ Actually Is

TickerQ isn't a fork of Hangfire or a wrapper around Quartz. It's what job scheduling looks like when you design it in 2025 instead of 2014.

The core idea is source generation. Decorate a method with [TickerFunction("SendWelcomeEmail")], and the TickerQ.SourceGenerator package registers that function at compile time. No reflection scanning at startup. No runtime method resolution via magic strings. The compiler knows about your jobs before a single line executes, which means full AOT compatibility and trimming safety for free.

Persistence uses your existing database. TickerQ ships an EF Core provider that supports PostgreSQL, SQL Server, SQLite, and MySQL. Point it at your app's database (or a dedicated one), and it stores job state in a ticker schema alongside your data. If you prefer Redis, the TickerQ.Caching.StackExchangeRedis package handles in-memory persistence and distributed coordination.

The dashboard is free and built-in. This is where Hangfire's model starts to feel dated. Hangfire's dashboard is fine until you need Pro features (batches, continuations, job management) and hit a paid license. TickerQ ships a SignalR-powered real-time dashboard at no cost. Job status, execution history, active management, all included.

For multi-node deployments, TickerQ uses Redis heartbeats for coordination, with automatic dead-node cleanup. If a node crashes mid-execution, the remaining nodes detect it and can pick up orphaned work.

Here's how it stacks up:

Feature TickerQ Hangfire Quartz.NET
Job Registration Source-generated (compile time) Reflection (runtime) Reflection + XML/code config
AOT / Trimming Fully compatible Not compatible Not compatible
Persistence EF Core (Postgres/SQL Server/SQLite/MySQL) or Redis SQL Server, Redis, etc. ADO.NET (many DBs)
Dashboard Free, real-time (SignalR) Free basic / Paid Pro None built-in
Distributed Coordination Redis heartbeats Hangfire.Pro or custom Clustered mode (DB-based)
DI Support Constructor injection Constructor injection Limited (job factory)
License MIT LGPL (Pro: commercial) Apache 2.0

Full Setup: TickerQ + PostgreSQL in Under 5 Minutes

Here are the NuGet packages you need:

dotnet add package TickerQ
dotnet add package TickerQ.EntityFrameworkCore
dotnet add package TickerQ.Dashboard

The full Program.cs configuration:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTickerQ(options =>
{
    options.AddOperationalStore(ef =>
    {
        ef.UseTickerQDbContext<TickerQDbContext>(opt =>
            opt.UseNpgsql(builder.Configuration.GetConnectionString("TickerQ")));
    });

    options.AddDashboard();
});

var app = builder.Build();
app.UseTickerQ();
app.Run();

That's it. No XML. No JSON config files. No IJobFactory implementations. The AddOperationalStore call tells TickerQ to use EF Core with PostgreSQL, creating its tables in a ticker schema by default. AddDashboard() enables the SignalR-powered UI. UseTickerQ() activates the middleware.

If you'd rather use your existing DbContext instead of a dedicated one, TickerQ supports that too:

options.AddOperationalStore(ef =>
{
    ef.UseApplicationDbContext<AppDbContext>(ConfigurationType.Merge);
});

Now the job class. TickerQ uses [TickerFunction] attributes on methods, and because your job class is resolved through DI, you get full constructor injection:

public class EmailJobs
{
    private readonly IEmailService _emailService;

    public EmailJobs(IEmailService emailService)
    {
        _emailService = emailService;
    }

    [TickerFunction("SendWelcomeEmail")]
    public async Task SendWelcomeEmail(
        TickerFunctionContext context,
        CancellationToken cancellationToken)
    {
        var userId = context.Request<WelcomeEmailRequest>()?.UserId;
        await _emailService.SendWelcomeAsync(userId, cancellationToken);
    }

    [TickerFunction("DailyDigest")]
    public async Task SendDailyDigest(
        TickerFunctionContext context,
        CancellationToken cancellationToken)
    {
        await _emailService.SendDailyDigestToAllUsersAsync(cancellationToken);
    }
}

The TickerFunctionContext gives you access to the request payload via .Request<T>(). The CancellationToken is the scheduler's. It signals when a job should stop, whether from shutdown or manual cancellation through the dashboard.

Scheduling a one-off job (e.g., send a welcome email 5 minutes after registration):

public class UserRegistrationService(ITimeTickerManager<TimeTickerEntity> timeManager)
{
    public async Task OnUserRegistered(Guid userId)
    {
        await timeManager.AddAsync(new TimeTickerEntity
        {
            Function = "SendWelcomeEmail",
            ExecutionTime = DateTime.UtcNow.AddMinutes(5),
            Request = new WelcomeEmailRequest { UserId = userId }
        });
    }
}

Scheduling a recurring cron job (e.g., daily digest at 8 AM):

public class ScheduleSetup(ICronTickerManager<CronTickerEntity> cronManager)
{
    public async Task ConfigureRecurringJobs()
    {
        await cronManager.AddAsync(new CronTickerEntity
        {
            Function = "DailyDigest",
            Expression = "0 0 8 * * *" // 6-part cron: sec min hour day month weekday
        });
    }
}

TickerQ uses 6-part cron expressions (including seconds), so you get sub-minute scheduling precision if you need it. The Function property maps directly to the string you passed to [TickerFunction("...")]. Because the source generator validates this at compile time, a typo here won't silently fail at runtime. That alone is worth the switch if you've ever lost an hour debugging a misspelled job name.

Count the lines of meaningful code across Program.cs, the job class, and the scheduling calls. Under 30.


The Dashboard: Your Free Ops Window

Two lines gave us the dashboard: options.AddDashboard() in configuration and app.UseTickerQ() in the middleware pipeline. Navigate to /tickerq (the default route) and you get a real-time operations view.

The dashboard runs on SignalR, so it updates live. You can see which jobs are queued, running, completed, or failed without refreshing. Execution history shows timing data and error details. You can cancel running jobs directly from the UI.

I appreciate when backend tools have decent UX, and this dashboard actually feels like someone thought about the developer using it. Compare that to Hangfire: the free dashboard shows job state and history, but job management features (retries, batch operations) require a Hangfire Pro license starting at $500/year per organisation. Quartz.NET ships no dashboard at all. You either build your own or use something like CrystalQuartz.

TickerQ also ships a TickerQ.Instrumentation.OpenTelemetry package for tracing integration. If you're already using OpenTelemetry with Jaeger or Grafana, your scheduled jobs show up in the same trace pipeline as your HTTP requests. That's how observability should work.

Here's the full NuGet package ecosystem:

Package Purpose
TickerQ Core scheduler engine
TickerQ.Utilities Shared types, entities, interfaces
TickerQ.SourceGenerator Compile-time function registration
TickerQ.EntityFrameworkCore EF Core persistence (Postgres/SQL Server/SQLite/MySQL)
TickerQ.Caching.StackExchangeRedis Redis persistence + distributed coordination
TickerQ.Dashboard Real-time SignalR dashboard
TickerQ.Instrumentation.OpenTelemetry OpenTelemetry tracing

When to Pick TickerQ Over Hangfire or Quartz

Pick TickerQ if you're starting a new project on .NET 10+, you care about AOT and trimming, and you want a scheduler without a paid tier. The source generation approach is architecturally aligned with where .NET is headed. The runtime is shedding reflection dependencies across the board, and your job scheduler should follow.

If you're already running PostgreSQL with EF Core, TickerQ slots in naturally. Same database, same ORM, same connection string. No separate infrastructure.

Stick with Hangfire if you've invested heavily in its ecosystem. Hangfire Pro's batch jobs, continuations, and server-side filtering are genuinely good features that TickerQ doesn't replicate yet. If your production system depends on those, migration isn't worth the disruption today.

Stick with Quartz.NET if you need complex scheduling logic like calendar-based exclusions, job chaining with data passing, or trigger priority systems. Quartz has 20+ years of scheduling algebra baked in. TickerQ covers 90% of real-world scheduling needs (delayed jobs + cron), but it's not trying to be a full job orchestration engine.

I'll be direct about the trade-offs. TickerQ is newer and has a smaller community. You won't find five-year-old Stack Overflow answers for edge cases. The documentation at tickerq.net is solid but still growing. If you need years of production mileage above all else, Hangfire and Quartz have that and you can't shortcut it.

But architecturally? TickerQ is right. Source generation over reflection. Free tooling over paid tiers. Compile-time safety over runtime surprises. This is where .NET is going, and TickerQ got there first for job scheduling.


The 75% Easier Takeaway

I set up a complete job scheduling system with PostgreSQL persistence, recurring cron jobs, delayed one-off jobs, a real-time dashboard, and DI throughout, in under 30 lines of code. No XML config. No reflection scanning. No paid license for basic operational visibility. No AOT headaches.

If you're starting a new .NET project and need background job scheduling, TickerQ is where I'd start. The GitHub repo is active and the documentation at tickerq.net covers everything from basic setup to multi-node deployment.

One more thing: pair TickerQ with .NET Aspire for local development. Aspire can orchestrate your PostgreSQL container and your web app together, so you go from git clone to a running scheduler with dashboard in a single dotnet run.

That's the bar for developer experience in 2026. Your job scheduler should clear it.

Read more