.NET Job Scheduling — TickerQ and Modern Architecture
Your SaaS platform processes tens of thousands of background jobs daily—user-triggered reports, scheduled data synchronization, recurring billing cycles. Performance matters: every millisecond spent in reflection overhead compounds across job volume. We measured it once: a 2ms reflection penalty per job execution meant an extra 40 seconds of CPU time daily across 20,000 jobs. Not catastrophic, but not free either.
Observability matters: product managers need real-time dashboards showing job states without deploying custom monitoring solutions. Safety matters: configuration errors should surface at compile time, not in production when a misspelled job name causes silent failures. We’ve all been there—typo in a cron expression, job never runs, customer discovers it three weeks later.
TickerQ addresses these demands using modern .NET primitives: source generators eliminate reflection, Entity Framework Core provides persistence, and SignalR powers real-time dashboards. Jobs are defined with attributes, generating boilerplate code at compile time. The scheduler is async-first, stateless at its core, and integrates seamlessly with ASP.NET Core’s dependency injection. The result: a framework that feels contemporary, performs efficiently, and surfaces errors early.
The trade-off: as a newer entrant, the ecosystem and community remain smaller compared to long-established alternatives. For teams building new systems prioritizing performance and modern patterns, the architectural approach offers compelling advantages. For teams requiring battle-tested stability or extensive plugin ecosystems, maturity considerations become relevant.
Architecture: Source Generation and Stateless Core
TickerQ’s architecture centers on compile-time code generation. Jobs are defined as methods decorated with [TickerFunction] attributes. During compilation, source generators discover these methods, validate their signatures, and generate registration code that wires them into the scheduler without reflection.
Consider a job definition:
public class ReportJobs
{
private readonly IReportService _reportService;
public ReportJobs(IReportService reportService) => _reportService = reportService;
[TickerFunction(functionName: "GenerateMonthlyReport", cronExpression: "0 0 1 * *")]
public async Task GenerateMonthlyReport(CancellationToken cancellationToken)
{
await _reportService.GenerateAsync(cancellationToken);
}
}
At compile time, the source generator:
- Discovers the
GenerateMonthlyReportmethod. - Validates the cron expression
0 0 1 * *(monthly at midnight). - Generates registration code mapping
"GenerateMonthlyReport"to the method. - Injects dependency resolution logic for
IReportService.
At runtime, the scheduler invokes jobs via generated delegates—no reflection, no MethodInfo.Invoke(), no dictionary lookups. This yields:
- Performance: Reflection overhead eliminated, reducing invocation latency.
- Compile-time safety: Invalid cron expressions or missing dependencies cause build errors, not runtime exceptions.
- Tooling support: IDEs detect errors, provide IntelliSense, and enable refactoring tools to work correctly.
The stateless core design means job state lives in the database (via Entity Framework Core), not in-memory. The scheduler queries the database for jobs whose execution times have arrived, claims them atomically, and dispatches them to workers. This architecture supports clustering naturally: multiple instances coordinate via the database without custom locking logic.
Configuration and Entity Framework Integration
Integrating TickerQ requires configuring Entity Framework Core for persistence. Install the packages:
dotnet add package TickerQ
dotnet add package TickerQ.EntityFrameworkCore
dotnet add package TickerQ.Dashboard
Configure services in Program.cs:
builder.Services.AddTickerQ(options =>
{
options.SetMaxConcurrency(10);
options.AddOperationalStore<AppDbContext>(ef =>
{
ef.UseModelCustomizerForMigrations();
ef.CancelMissedTickersOnApplicationRestart();
});
options.AddDashboard(dash =>
{
dash.BasePath = "/tickerq-dashboard";
dash.AddDashboardBasicAuth();
});
});
var app = builder.Build();
app.UseTickerQ();
app.Run();
The AddOperationalStore method integrates TickerQ with your existing DbContext. TickerQ creates tables for job definitions (TimeTicker, CronTicker) and execution history. The UseModelCustomizerForMigrations() option applies TickerQ’s entity configurations only during migrations, keeping your domain model clean.
Generate and apply migrations:
dotnet ef migrations add TickerQInitial -c AppDbContext
dotnet ef database update
TickerQ’s tables store:
- CronTickers: Recurring jobs with cron expressions.
- TimeTickers: One-time jobs scheduled for specific execution times.
- CronTickerOccurrences: Execution history for audit trails and retry tracking.
This database-backed persistence ensures jobs survive application restarts. If a job should have executed while the application was down, TickerQ handles it upon restart based on configuration—either executing missed jobs or skipping them.
Job Definitions: Cron and Time Tickers
TickerQ supports two job types: cron-based recurring jobs and time-based one-time jobs.
Cron jobs execute repeatedly:
public class MaintenanceJobs
{
[TickerFunction(functionName: "CleanupLogs", cronExpression: "0 0 * * *")]
public async Task CleanupLogs()
{
await DeleteOldLogsAsync();
}
}
The source generator validates "0 0 * * *" at compile time. If the expression is invalid—say, "0 25 * * *" (invalid hour)—the build fails with a descriptive error.
Time tickers execute once at a specified time:
public class NotificationJobs
{
[TickerFunction(functionName: "SendReminder")]
public async Task SendReminder(TickerFunctionContext<int> context, CancellationToken cancellationToken)
{
var userId = context.Request;
await SendReminderEmailAsync(userId, cancellationToken);
}
}
Schedule time tickers programmatically:
await _timeTickerManager.AddAsync(new TimeTicker
{
Function = "SendReminder",
ExecutionTime = DateTime.UtcNow.AddHours(2),
Request = TickerHelper.CreateTickerRequest<int>(userId),
Retries = 3,
RetryIntervals = new[] { 60, 300, 900 } // 1min, 5min, 15min
});
This schedules a reminder to send in two hours. If execution fails, TickerQ retries up to three times with increasing intervals. The Request parameter passes data (here, userId) to the job, serialized as JSON in the database.
Real-Time Dashboard with SignalR
TickerQ’s dashboard provides live visibility into job states using SignalR for real-time updates. Administrators view:
- Active jobs: Currently executing, with elapsed time and progress indicators.
- Scheduled jobs: Pending execution with countdown timers.
- Execution history: Completed jobs with duration, outcome, and error details.
- Cron tickers: Recurring jobs with last/next execution times.
The dashboard also supports:
- Manual triggering: Execute recurring jobs on-demand.
- Job cancellation: Stop long-running jobs mid-execution.
- Live updates: Job states update in real-time via SignalR, no page refreshes required.
Configure basic authentication to protect the dashboard:
{
"TickerQBasicAuth": {
"Username": "admin",
"Password": "securepassword"
}
}
For production deployments, integrate with your authentication system—ASP.NET Core Identity, OAuth, or Azure AD—by implementing custom authorization filters.
The dashboard’s Vue.js-based UI is modern and responsive, tailored for operational teams monitoring background processing health. Compare this to Hangfire’s dashboard, which uses server-rendered HTML with periodic polling. TickerQ’s SignalR approach reduces latency and provides instant feedback when job states change.
Retry Policies, Throttling, and Distributed Coordination
TickerQ supports per-job retry policies:
await _timeTickerManager.AddAsync(new TimeTicker
{
Function = "ImportData",
ExecutionTime = DateTime.UtcNow,
Retries = 5,
RetryIntervals = new[] { 30, 60, 120, 300, 600 }, // Exponential backoff
});
Failed jobs retry based on the specified intervals. After exhausting retries, jobs transition to Failed state, visible in the dashboard with full error details.
Throttling limits concurrent execution. If your database supports 50 concurrent connections and you schedule 100 jobs simultaneously, throttling prevents connection exhaustion:
options.SetMaxConcurrency(10); // Max 10 concurrent jobs
TickerQ queues excess jobs until workers become available, preventing resource contention.
Distributed coordination works via Entity Framework Core’s optimistic concurrency. When a scheduler instance queries for jobs, it claims them with an atomic update:
UPDATE TimeTicker
SET State = 'Processing', Instance = 'server-01'
WHERE State = 'Pending' AND ExecutionTime <= GETUTCDATE();
Only one instance succeeds per job. If an instance crashes mid-execution, orphaned jobs remain in Processing state until a recovery mechanism detects and resets them—configurable via timeout policies.
This coordination is simpler than Quartz.NET’s pessimistic locking but sufficient for most scenarios. Teams running dozens of instances in high-throughput environments may need to tune timeout settings to balance recovery speed and false-positive detection.
Batch Jobs and Dependency Workflows
TickerQ supports batch jobs—groups of related tasks that execute as a unit:
var batchId = Guid.NewGuid();
await _timeTickerManager.AddAsync(new TimeTicker
{
Function = "ImportUsers",
ExecutionTime = DateTime.UtcNow,
BatchParent = batchId,
BatchRunCondition = BatchRunCondition.Always
});
await _timeTickerManager.AddAsync(new TimeTicker
{
Function = "TransformUsers",
ExecutionTime = DateTime.UtcNow,
BatchParent = batchId,
BatchRunCondition = BatchRunCondition.OnSuccess // Only if ImportUsers succeeds
});
TickerQ executes ImportUsers first. If it succeeds, TransformUsers runs; if it fails, TransformUsers is skipped. This declarative workflow removes custom orchestration logic from application code.
Batch conditions include:
- Always: Execute regardless of parent outcomes.
- OnSuccess: Execute only if all previous batch jobs succeeded.
- OnFailure: Execute only if any previous job failed (error handling workflows).
This feature mirrors Hangfire’s continuations and Quartz.NET’s job chaining but integrates more naturally with Entity Framework Core’s transactional boundaries.
When TickerQ Fits
TickerQ excels when:
Performance matters: High job volumes benefit from reflection-free execution and async-first design.
Compile-time safety is valued: Teams that prefer catching configuration errors during builds rather than runtime.
Modern tooling is prioritized: Source generation, SignalR, and Entity Framework Core integration appeal to teams comfortable with current .NET patterns.
Real-time observability is required: The dashboard’s live updates provide operational visibility without custom monitoring infrastructure.
TickerQ is less suitable when:
Battle-tested stability is critical: Hangfire (13+ years) and Quartz.NET (20+ years) have larger user bases and more production validation.
Extensive plugins are needed: TickerQ’s ecosystem is smaller. Hangfire and Quartz.NET offer more storage backends, monitoring integrations, and community extensions.
Legacy .NET Framework support is required: TickerQ targets modern .NET (6+). Teams on .NET Framework should use Hangfire or Quartz.NET.
Operational Considerations
TickerQ’s reliance on Entity Framework Core couples job scheduling to your database strategy. Teams already using EF Core benefit from unified migration workflows and tooling. Teams preferring Dapper, raw SQL, or NoSQL databases face friction—TickerQ’s operational store requires EF Core.
The source generation approach requires recompilation when job definitions change. This aligns with modern CI/CD practices (deploy code, not configuration) but contrasts with Hangfire or Quartz.NET, where jobs can be scheduled dynamically at runtime without redeployment.
TickerQ’s dashboard consumes resources—SignalR connections, server memory for real-time updates. In resource-constrained environments, disable the dashboard and rely on application logging.
Practical Takeaways
TickerQ represents modern .NET job scheduling: source generation, async-first design, and real-time monitoring. It bridges the gap between simplicity (NCronJob, Coravel) and enterprise features (Quartz.NET), offering persistence and performance without operational complexity.
Consider TickerQ if:
- You’re building new systems on modern .NET (6+).
- Performance and compile-time safety are priorities.
- You use Entity Framework Core and value tooling integration.
- Real-time dashboards enhance operational workflows.
Avoid TickerQ if:
- Your system runs on .NET Framework or older .NET Core versions.
- You need extensive ecosystem support or community plugins.
- You prefer runtime configuration over compile-time code generation.
The final article synthesizes the series into comparative guidance, presenting a feature matrix, rating framework suitability across dimensions, and offering decision heuristics for selecting the right scheduler based on system maturity, infrastructure, and team priorities.
