Hangfire
Hangfire is one of the most widely used background job processing libraries in the .NET community, featuring a built-in monitoring dashboard. Brighter provides first-class integration with Hangfire for scheduler functionality, making it an excellent choice for production systems requiring easy setup, visual monitoring, and persistent scheduling.
Production Recommendation
Hangfire is highly recommended for production use alongside Quartz.NET as one of the two primary production schedulers for Brighter.
Why Choose Hangfire?
✅ Built-in Dashboard: Web UI for monitoring jobs, queues, and servers
✅ Easy Setup: Simple configuration and minimal boilerplate
✅ Persistent: Jobs stored in databases (SQL Server, PostgreSQL, Redis, etc.)
✅ Reliable: Jobs survive application restarts and crashes
✅ Cancellation: Full support for cancelling scheduled jobs
✅ Automatic Retries: Configurable retry policies for failed jobs
✅ Multiple Storage: SQL Server, PostgreSQL, MySQL, Redis, MongoDB
✅ Background Processing: Beyond scheduling - fire-and-forget, delayed, recurring jobs
⚠️ Important: Strong Naming Limitation
The Paramore.Brighter.MessageScheduler.Hangfire assembly is NOT strong-named.
This is due to Hangfire itself not using strong naming. If your application requires all assemblies to be strong-named (for example, for enterprise policy compliance), you will need to use an alternative scheduler (Quartz.NET or AWS/Azure schedulers).
Why this matters:
Cannot mix strong-named and non-strong-named assemblies in some enterprise environments
May cause issues with certain security policies
Use Quartz.NET if strong naming is a hard requirement
Hangfire Overview
Hangfire is a comprehensive background job processing library that provides:
Persistent Job Storage: Store jobs in various backends
Automatic Retry: Retry failed jobs with exponential backoff
Dashboard: Web-based monitoring UI
Multiple Queue Support: Organize jobs into different queues
Job Continuations: Chain jobs together
Recurring Jobs: Cron-based scheduling
Job Filters: Intercept job execution
Distributed Processing: Multiple servers can process jobs
For more information, visit the Hangfire documentation.
How Brighter Integrates with Hangfire
Brighter integrates with Hangfire through:
BrighterHangfireSchedulerJob: A Hangfire job that executes scheduled Brighter messages
HangfireMessageSchedulerFactory: Factory that creates Brighter's message scheduler backed by Hangfire
JobActivator Integration: Uses Brighter's DI container to resolve jobs
When you schedule a message with Brighter:
Brighter creates a Hangfire background job with the message payload
Hangfire persists the job to its storage backend
At the scheduled time, Hangfire fires the job
BrighterHangfireSchedulerJob receives the job execution
BrighterHangfireSchedulerJob dispatches the message via Brighter's Command Processor
Your handler executes
Job appears in Hangfire dashboard with status
NuGet Packages
Install the required NuGet packages:
dotnet add package Paramore.Brighter.MessageScheduler.Hangfire
dotnet add package Hangfire
dotnet add package Hangfire.AspNetCore # For dashboardFor persistence, add a Hangfire storage package:
# SQL Server
dotnet add package Hangfire.SqlServer
# PostgreSQL
dotnet add package Hangfire.PostgreSql
# MySQL
dotnet add package Hangfire.MySql
# Redis
dotnet add package Hangfire.Redis.StackExchange
# MongoDB
dotnet add package Hangfire.MongoConfiguration
Basic Configuration
Configure Brighter with Hangfire scheduler:
using Paramore.Brighter.Extensions.DependencyInjection;
using Paramore.Brighter.MessageScheduler.Hangfire;
using Hangfire;
using Hangfire.SqlServer;
var builder = WebApplication.CreateBuilder(args);
// Configure Hangfire
builder.Services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(
builder.Configuration.GetConnectionString("Hangfire"),
new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true
}));
// Add Hangfire server
builder.Services.AddHangfireServer();
// Register Brighter's Hangfire job
builder.Services.AddSingleton<BrighterHangfireSchedulerJob>();
// Configure Brighter with Hangfire scheduler
builder.Services.AddBrighter(options =>
{
options.HandlerLifetime = ServiceLifetime.Scoped;
})
.UseScheduler(new HangfireMessageSchedulerFactory())
.AutoFromAssemblies();
var app = builder.Build();
// Add Hangfire dashboard
app.UseHangfireDashboard();
app.Run();Configuration with PostgreSQL
using Hangfire.PostgreSql;
builder.Services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UsePostgreSqlStorage(options =>
{
options.UseNpgsqlConnection(builder.Configuration.GetConnectionString("Hangfire"));
}));
builder.Services.AddHangfireServer();
builder.Services.AddSingleton<BrighterHangfireSchedulerJob>();
builder.Services.AddBrighter(options =>
{
options.HandlerLifetime = ServiceLifetime.Scoped;
})
.UseScheduler(new HangfireMessageSchedulerFactory())
.AutoFromAssemblies();Configuration with Redis
using Hangfire.Redis.StackExchange;
builder.Services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseRedisStorage(
builder.Configuration.GetConnectionString("Redis"),
new RedisStorageOptions
{
Prefix = "brighter:",
ExpiryCheckInterval = TimeSpan.FromHours(1)
}));
builder.Services.AddHangfireServer();
builder.Services.AddSingleton<BrighterHangfireSchedulerJob>();
builder.Services.AddBrighter(options =>
{
options.HandlerLifetime = ServiceLifetime.Scoped;
})
.UseScheduler(new HangfireMessageSchedulerFactory())
.AutoFromAssemblies();Configuration with Custom Queue
Organize Brighter jobs into a specific queue:
builder.Services.AddBrighter(options =>
{
options.HandlerLifetime = ServiceLifetime.Scoped;
})
.UseScheduler(new HangfireMessageSchedulerFactory
{
Queue = "brighter-scheduler" // Custom queue for Brighter jobs
})
.AutoFromAssemblies();Benefits:
Isolate Brighter jobs from other Hangfire jobs
Configure different server instances for different queues
Monitor Brighter jobs separately in dashboard
Storage Options
Hangfire supports multiple persistent storage backends:
SQL Server
using Hangfire.SqlServer;
configuration.UseSqlServerStorage(
connectionString,
new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true,
SchemaName = "Hangfire" // Optional custom schema
});PostgreSQL
using Hangfire.PostgreSql;
configuration.UsePostgreSqlStorage(options =>
{
options.UseNpgsqlConnection(connectionString);
options.SchemaName = "hangfire"; // Optional custom schema
});MySQL
using Hangfire.MySql;
configuration.UseStorage(new MySqlStorage(
connectionString,
new MySqlStorageOptions
{
TransactionIsolationLevel = IsolationLevel.ReadCommitted,
QueuePollInterval = TimeSpan.FromSeconds(15),
JobExpirationCheckInterval = TimeSpan.FromHours(1),
CountersAggregateInterval = TimeSpan.FromMinutes(5),
PrepareSchemaIfNecessary = true,
DashboardJobListLimit = 50000,
TransactionTimeout = TimeSpan.FromMinutes(1),
TablesPrefix = "Hangfire"
}));Redis
using Hangfire.Redis.StackExchange;
configuration.UseRedisStorage(
connectionString,
new RedisStorageOptions
{
Prefix = "hangfire:",
ExpiryCheckInterval = TimeSpan.FromHours(1),
InvisibilityTimeout = TimeSpan.FromMinutes(30)
});In-Memory (Development Only)
using Hangfire.MemoryStorage;
configuration.UseMemoryStorage();⚠️ Warning: In-memory storage loses all jobs on restart. Use persistent storage for production.
Dashboard
Hangfire includes a powerful web-based dashboard for monitoring jobs.
Basic Dashboard Setup
var app = builder.Build();
// Add dashboard at /hangfire
app.UseHangfireDashboard();
app.Run();Dashboard with Authentication
Secure the dashboard with authorization:
using Hangfire.Dashboard;
public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
// Allow authenticated users
return httpContext.User.Identity?.IsAuthenticated ?? false;
}
}
// Apply filter
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new HangfireAuthorizationFilter() },
DashboardTitle = "Brighter Scheduler Jobs"
});Dashboard with Custom Path and Options
app.UseHangfireDashboard("/admin/jobs", new DashboardOptions
{
Authorization = new[] { new HangfireAuthorizationFilter() },
DashboardTitle = "Brighter Message Scheduler",
StatsPollingInterval = 2000, // Update every 2 seconds
DisplayStorageConnectionString = false
});Dashboard Features
The Hangfire dashboard provides:
Jobs: View all jobs (scheduled, processing, succeeded, failed)
Retries: Monitor automatic retry attempts
Recurring Jobs: Manage recurring jobs
Servers: View active Hangfire servers
Succeeded/Failed: Historical job execution data
Real-time Updates: Live job status updates
Job Details: Inspect job arguments and exceptions
Manual Actions: Requeue, delete, or retry jobs
Code Examples
Basic Scheduling
public class OrderService
{
private readonly IAmACommandProcessor _commandProcessor;
public async Task CreateOrder(Order order)
{
await _repository.SaveAsync(order);
// Schedule order processing for 1 hour later
var schedulerId = await _commandProcessor.SendAsync(
TimeSpan.FromHours(1),
new ProcessOrderCommand { OrderId = order.Id }
);
// Store scheduler ID for potential cancellation
order.ProcessSchedulerId = schedulerId;
await _repository.UpdateAsync(order);
// Job will appear in Hangfire dashboard
}
}Cancelling a Scheduled Job
public class OrderService
{
private readonly IMessageScheduler _scheduler;
public async Task CancelOrder(Guid orderId)
{
var order = await _repository.GetAsync(orderId);
// Cancel the scheduled processing
if (!string.IsNullOrEmpty(order.ProcessSchedulerId))
{
await _scheduler.CancelAsync(order.ProcessSchedulerId);
_logger.LogInformation("Cancelled scheduled processing for order {OrderId}", orderId);
// Job will be removed from Hangfire dashboard
}
order.Status = OrderStatus.Cancelled;
await _repository.UpdateAsync(order);
}
}Scheduling with Absolute Time
public class ReportService
{
private readonly IAmACommandProcessor _commandProcessor;
public async Task ScheduleDailyReport()
{
// Schedule for tomorrow at 9 AM
var tomorrow9AM = DateTimeOffset.UtcNow.Date.AddDays(1).AddHours(9);
var schedulerId = await _commandProcessor.SendAsync(
tomorrow9AM,
new GenerateDailyReportCommand { Date = DateTime.UtcNow.Date }
);
_logger.LogInformation("Scheduled daily report: {SchedulerId}", schedulerId);
// View in dashboard: Jobs -> Scheduled
}
}High Availability with Multiple Servers
Hangfire supports multiple server instances for high availability:
// Server 1, 2, 3... all with same configuration
builder.Services.AddHangfire(configuration => configuration
.UseSqlServerStorage(sharedConnectionString) // Shared database
);
builder.Services.AddHangfireServer(options =>
{
options.ServerName = Environment.MachineName, // Unique server name
options.WorkerCount = Environment.ProcessorCount * 5, // Worker threads
options.Queues = new[] { "default", "brighter-scheduler" } // Process these queues
});How HA works:
Multiple servers connect to the same storage backend
Jobs are distributed among active servers
If a server fails, other servers take over its jobs
Each server processes jobs independently
Dashboard shows all servers and their status
Best practices for HA:
Use shared persistent storage (SQL Server, PostgreSQL, etc.)
Configure unique server names
Monitor server health in dashboard
Set appropriate worker counts per server
Use queues to control job distribution
Monitoring and Observability
Job Filters
Monitor job execution with filters:
using Hangfire.Common;
using Hangfire.Server;
using Hangfire.States;
public class BrighterJobLogFilter : IServerFilter, IApplyStateFilter
{
private readonly ILogger<BrighterJobLogFilter> _logger;
public BrighterJobLogFilter(ILogger<BrighterJobLogFilter> logger)
{
_logger = logger;
}
public void OnPerforming(PerformingContext context)
{
_logger.LogInformation("Starting Brighter job {JobId}: {JobType}",
context.BackgroundJob.Id,
context.BackgroundJob.Job.Type.Name);
}
public void OnPerformed(PerformedContext context)
{
if (context.Exception != null)
{
_logger.LogError(context.Exception,
"Brighter job {JobId} failed",
context.BackgroundJob.Id);
}
else
{
_logger.LogInformation("Brighter job {JobId} completed successfully",
context.BackgroundJob.Id);
}
}
public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
_logger.LogInformation("Brighter job {JobId} state changed to {State}",
context.BackgroundJob.Id,
context.NewState.Name);
}
public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
// Optional: log state changes
}
}
// Register filter
GlobalJobFilters.Filters.Add(new BrighterJobLogFilter(loggerFactory.CreateLogger<BrighterJobLogFilter>()));Health Checks
Monitor Hangfire health:
public class HangfireHealthCheck : IHealthCheck
{
private readonly JobStorage _storage;
public HangfireHealthCheck(JobStorage storage)
{
_storage = storage;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var monitoringApi = _storage.GetMonitoringApi();
var servers = monitoringApi.Servers();
if (!servers.Any())
{
return Task.FromResult(HealthCheckResult.Unhealthy("No Hangfire servers running"));
}
var stats = monitoringApi.GetStatistics();
var data = new Dictionary<string, object>
{
{ "Servers", servers.Count },
{ "Queues", stats.Queues },
{ "Scheduled", stats.Scheduled },
{ "Processing", stats.Processing },
{ "Succeeded", stats.Succeeded },
{ "Failed", stats.Failed }
};
return Task.FromResult(HealthCheckResult.Healthy("Hangfire is running", data));
}
catch (Exception ex)
{
return Task.FromResult(HealthCheckResult.Unhealthy("Hangfire check failed", ex));
}
}
}
// Register health check
builder.Services.AddHealthChecks()
.AddCheck<HangfireHealthCheck>("hangfire");Best Practices
1. Always Use Persistent Storage in Production
// Good - Persistent storage
configuration.UseSqlServerStorage(connectionString);
// Bad - In-memory storage in production
configuration.UseMemoryStorage();2. Secure the Dashboard
// Good - Require authentication
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new HangfireAuthorizationFilter() }
});
// Bad - Publicly accessible dashboard
app.UseHangfireDashboard(); // No authorization3. Use Custom Queues for Organization
// Good - Separate queue for Brighter jobs
.UseScheduler(new HangfireMessageSchedulerFactory
{
Queue = "brighter-scheduler"
})
// Consider - Everything in default queue (harder to manage)
.UseScheduler(new HangfireMessageSchedulerFactory())4. Store Scheduler IDs for Cancellation
// Good - Store ID for later cancellation
var schedulerId = await _commandProcessor.SendAsync(delay, command);
entity.SchedulerId = schedulerId;
await _repository.SaveAsync(entity);
// Bad - Lost ability to cancel
await _commandProcessor.SendAsync(delay, command); // ID discarded5. Configure Appropriate Worker Counts
// Good - Based on workload
builder.Services.AddHangfireServer(options =>
{
options.WorkerCount = Environment.ProcessorCount * 5; // Adjust based on I/O vs CPU
});
// Avoid - Too many workers (resource exhaustion)
options.WorkerCount = 1000;
// Avoid - Too few workers (slow processing)
options.WorkerCount = 1;6. Monitor the Dashboard Regularly
// Good - Check dashboard periodically
// Navigate to /hangfire to view:
// - Failed jobs (investigate and retry)
// - Long-running jobs (potential issues)
// - Server status (ensure all servers active)7. Handle Timezone Correctly
// Good - Use UTC for consistency
var scheduledTime = DateTimeOffset.UtcNow.AddHours(1);
// Bad - Local time (ambiguous across servers)
var scheduledTime = DateTimeOffset.Now.AddHours(1);8. Configure Automatic Retry Policies
// Good - Automatic retry for transient failures
builder.Services.AddHangfireServer(options =>
{
options.SchedulePollingInterval = TimeSpan.FromSeconds(15);
});
// Hangfire automatically retries failed jobs
// View retry attempts in dashboardTroubleshooting
Jobs Not Executing
Symptom: Jobs scheduled but never execute
Possible Causes:
Hangfire server not started
Database connectivity issues
BrighterHangfireSchedulerJob not registered
No workers processing the queue
Solutions:
// Verify Hangfire server is started
builder.Services.AddHangfireServer();
// Check BrighterHangfireSchedulerJob is registered
builder.Services.AddSingleton<BrighterHangfireSchedulerJob>();
// Check dashboard for server status
// Navigate to /hangfire/serversJobs Executing Multiple Times
Symptom: Same job executes multiple times
Cause: Multiple servers or configuration issues
Solution:
// Ensure unique job IDs
// Hangfire uses job IDs to prevent duplicates
// Check dashboard for duplicate jobs
// Verify storage is shared across servers
// All servers must use the same connection stringDashboard Not Loading
Symptom: Dashboard URL returns 404
Possible Causes:
Dashboard not registered
Incorrect URL path
Authentication blocking access
Solutions:
// Ensure UseHangfireDashboard is called
app.UseHangfireDashboard("/hangfire");
// Check the path matches your URL
// Navigate to: http://localhost:5000/hangfire
// Verify authorization filter allows access
// Temporarily disable auth to test:
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new AllowAllDashboardAuthorizationFilter() }
});Migration from Other Schedulers
From InMemory Scheduler
// Before - InMemory
services.UseScheduler(new InMemorySchedulerFactory());
// After - Hangfire
services.AddHangfire(config => config.UseSqlServerStorage(connectionString));
services.AddHangfireServer();
services.AddSingleton<BrighterHangfireSchedulerJob>();
services.UseScheduler(new HangfireMessageSchedulerFactory());From Quartz
If migrating from Quartz, you can run both schedulers during transition:
// Run both schedulers temporarily
services.UseScheduler(provider =>
{
// Choose based on feature flag or configuration
if (Configuration.GetValue<bool>("UseHangfire"))
{
return new HangfireMessageSchedulerFactory();
}
else
{
var factory = provider.GetRequiredService<ISchedulerFactory>();
return new QuartzSchedulerFactory(factory.GetScheduler().Result);
}
});Comparison: Hangfire vs Quartz
Dashboard
✅ Built-in web UI
⚠️ Limited/third-party
Setup Complexity
✅ Easy
⚠️ Moderate
Persistence
✅ Multiple options
✅ Multiple options
Clustering
✅ Yes
✅ Yes
Cancellation
✅ Yes
✅ Yes
Strong Naming
❌ No
✅ Yes
Automatic Retry
✅ Built-in
⚠️ Manual
Recurring Jobs
✅ Native support
⚠️ Via triggers
Monitoring
✅ Excellent (dashboard)
⚠️ Limited
Choose Hangfire when:
You want a built-in monitoring dashboard
Easy setup is a priority
Strong naming is not required
You need automatic retry handling
Choose Quartz when:
Strong naming is required
You need advanced trigger types (Cron, Calendar)
You prefer more control over scheduling logic
Dashboard is not essential
Related Documentation
Brighter Scheduler Support - Overview of scheduling in Brighter
InMemory Scheduler - Lightweight scheduler for testing
Quartz Scheduler - Alternative production scheduler with strong naming
AWS Scheduler - Cloud-native AWS scheduling
Azure Scheduler - Cloud-native Azure scheduling
Hangfire Documentation - Official Hangfire documentation
Summary
Hangfire is an excellent production scheduler for Brighter offering:
✅ Dashboard: Built-in web UI for monitoring and management
✅ Easy Setup: Minimal configuration required
✅ Persistent: Jobs survive restarts with multiple storage options
✅ Reliable: Automatic retries and distributed processing
✅ Visual Monitoring: Real-time job status and history
⚠️ Strong Naming: NOT strong-named (use Quartz if required)
Use Hangfire when you need a production scheduler with a built-in dashboard and easy setup. If strong naming is required, use Quartz.NET instead.
Last updated
Was this helpful?
