Configuration Reference
This is a complete reference for all EverTask configuration options.
Table of Contents
- Service Configuration
- Queue Configuration
- Rate Limiting Configuration
- Storage Configuration
- Logging Configuration
- Monitoring Configuration
- Storage Provider Details
- Handler Configuration
- Complete Examples
- Configuration Validation
- Performance Tuning Guidelines
Service Configuration
Use the fluent API in AddEverTask() to configure EverTask’s core behavior.
SetChannelOptions
Controls how many tasks can be queued and what happens when the queue fills up.
Signatures:
SetChannelOptions(int capacity)
SetChannelOptions(BoundedChannelOptions options)
Parameters:
capacity(int): Maximum number of tasks that can be queuedoptions(BoundedChannelOptions): Fully configured channel options instance
Default: Environment.ProcessorCount * 200 (minimum 1000)
Examples:
// Simple capacity
opt.SetChannelOptions(5000)
// Custom configuration (keep FullMode = Wait; see warning below)
opt.SetChannelOptions(new BoundedChannelOptions(5000)
{
FullMode = BoundedChannelFullMode.Wait
})
FullMode Options:
Wait: Block until space is available (default). The only mode EverTask’s queue-full handling supportsDropWrite/DropOldest/DropNewest: ⚠ Not recommended. EverTask’s queue-full detection and the scheduler’s backoff/QueueFullBehaviorrely onTryWriterejecting when the channel is full. WithDrop*modesTryWritenever rejects, so a write is treated as a successful enqueue even when the channel silently drops the item: theQueueFullsignal (and the scheduler backoff that depends on it) never fires. A dropped task is not silently lost, though: the channel’sitemDroppedcallback releases the delivery registration and reverts the victim’s storage row toWaitingQueue, so startup recovery re-queues it later, but it will not run in the current process and there is no immediate backpressure. UseWait(and tune capacity /MaxDegreeOfParallelism) instead of aDrop*mode.
SetMaxDegreeOfParallelism
Controls how many tasks can run at the same time.
Signature:
SetMaxDegreeOfParallelism(int parallelism)
Parameters:
parallelism(int): Number of concurrent workers
Default: Environment.ProcessorCount * 2 (minimum 4)
Examples:
// Fixed parallelism
opt.SetMaxDegreeOfParallelism(16)
// Scale with CPUs
opt.SetMaxDegreeOfParallelism(Environment.ProcessorCount * 4)
Notes:
- Use higher values for I/O-bound tasks like API calls or database operations
- Use lower values for CPU-intensive tasks
- Setting to 1 will log a warning since it’s generally a bad idea in production
SetDefaultRetryPolicy
Sets how tasks should retry when they fail (applies to all tasks unless overridden).
Signature:
SetDefaultRetryPolicy(IRetryPolicy policy)
Parameters:
policy(IRetryPolicy): Retry policy implementation
Default: LinearRetryPolicy(3, TimeSpan.FromMilliseconds(500))
Examples:
// Linear retry with fixed delay
opt.SetDefaultRetryPolicy(new LinearRetryPolicy(5, TimeSpan.FromSeconds(1)))
// Linear retry with custom delays
opt.SetDefaultRetryPolicy(new LinearRetryPolicy(new[]
{
TimeSpan.FromMilliseconds(100),
TimeSpan.FromMilliseconds(500),
TimeSpan.FromSeconds(2)
}))
// Custom retry policy (your own IRetryPolicy implementation; see
// resilience/retry-policies.md for an exponential backoff example)
opt.SetDefaultRetryPolicy(new MyExponentialBackoffPolicy())
Notes:
LinearRetryPolicyis the only built-in policy;retryCountis the number of retries AFTER the initial attempt (e.g.LinearRetryPolicy(3, ...)= up to 4 executions), and bothretryCountandretryDelaymust be greater than zero.- Retries cannot be disabled via
LinearRetryPolicy: to disable them, implement a trivialIRetryPolicythat invokes the action once; see Custom Retry Policies.
Exception filtering (LinearRetryPolicy, fluent): by default every exception is retried except OperationCanceledException and TimeoutException, which are always fail-fast (hardcoded, cannot be overridden by a filter). Configure which exceptions retry with one of these modes (whitelist and blacklist cannot be mixed: doing so throws InvalidOperationException):
.Handle<DbException>().Handle<HttpRequestException>() // whitelist: retry ONLY these (+ derived)
.DoNotHandle<ArgumentException>() // blacklist: retry all EXCEPT these
.HandleWhen(ex => ex is HttpRequestException h && (int?)h.StatusCode >= 500) // predicate (highest priority)
.HandleTransientDatabaseErrors() // preset: DbException (+ TimeoutException, but it's blocked by the fail-fast guard → effectively DbException only)
.HandleTransientNetworkErrors() // preset: HttpRequestException, SocketException, WebException, TaskCanceledException (⚠ TaskCanceledException : OperationCanceledException → blocked by the fail-fast guard, never actually retried)
.HandleAllTransientErrors() // both presets combined
Resolution priority: OCE/TimeoutException fail-fast → HandleWhen → whitelist → blacklist → retry-all. Because the OCE/TimeoutException guard runs first, any preset entry that is (or derives from) those types (TaskCanceledException, TimeoutException) is never retried even though it appears in the preset. See Resilience › Exception Filtering.
SetDefaultTimeout
Sets a maximum execution time for tasks (applies globally unless overridden).
Signature:
SetDefaultTimeout(TimeSpan? timeout)
Parameters:
timeout(TimeSpan?): Maximum execution time, ornullfor no timeout
Default: null (no timeout)
Examples:
// 5 minute timeout
opt.SetDefaultTimeout(TimeSpan.FromMinutes(5))
// 30 second timeout
opt.SetDefaultTimeout(TimeSpan.FromSeconds(30))
// No timeout (explicit)
opt.SetDefaultTimeout(null)
Notes:
- When the timeout is reached, the
CancellationTokengets cancelled - Your handler needs to check the token for this to work (cooperative cancellation)
- You can override this per handler or per queue
SetDefaultAuditLevel
Sets the default audit trail level for all tasks (controls database bloat from high-frequency tasks).
Signature:
SetDefaultAuditLevel(AuditLevel auditLevel)
Parameters:
auditLevel(AuditLevel): Audit verbosity levelFull(default): Complete audit trail:StatusAuditfor all status transitions andRunsAuditfor every runMinimal:StatusAuditonly on real errors;RunsAuditis still written for every recurring run (so run-frequency history is preserved) andQueuedTask.LastExecutionUtcis updatedErrorsOnly: aStatusAudit/RunsAuditrow is written only for a run that records a non-empty exception string or ends in statusFailed(successful runs write neither;QueuedTaskstatus is still updated toCompleted)None: noStatusAudit/RunsAuditrows at all
MinimalandErrorsOnlydiffer only inRunsAudit:Minimalrecords every recurring run,ErrorsOnlyonly the runs with a non-empty exception string or statusFailed. The authoritative rules are inAuditPolicy.ShouldCreateStatusAudit/ShouldCreateRunsAudit.
Default: AuditLevel.Full
Examples:
// Full audit (default)
opt.SetDefaultAuditLevel(AuditLevel.Full)
// Minimal audit for high-frequency tasks
opt.SetDefaultAuditLevel(AuditLevel.Minimal)
// Only audit errors
opt.SetDefaultAuditLevel(AuditLevel.ErrorsOnly)
// No audit trail
opt.SetDefaultAuditLevel(AuditLevel.None)
Notes:
- For a recurring task running every 5 minutes:
Full≈ 1,152 audit records/day (StatusAudit + RunsAudit);Minimal≈ 288 RunsAudit/day (one per successful run, no StatusAudit);ErrorsOnly/None≈ 0 when executions succeed - You can override this per task when dispatching
- Use lower levels (Minimal/ErrorsOnly/None) for high-frequency recurring tasks
- See Audit Configuration for detailed usage guide
Audit & Execution-Log Retention (AddAuditCleanup)
Configure automatic retention to prevent unbounded growth of the audit and execution-log tables. Retention is enforced by the optional AuditCleanupHostedService, registered with AddAuditCleanup(policy, cleanupIntervalHours), the single entry-point that actually applies the policy.
Note: retention is applied only by
AddAuditCleanup(policy, cleanupIntervalHours). An earlierSetAuditRetentionPolicy(...)on the builder never took effect (the service reads its policy only fromAuditCleanupOptions) and has been removed; if you used it, pass the policy toAddAuditCleanupinstead.
Non-positive knobs are disabled. Every day/count knob uses the same convention:
null= unlimited/disabled, and any value<= 0is also treated as disabled (a no-op, logged as a warning), never as a “now”/future cutoff. This keeps a typo or a missingIConfigurationbinding (an absent env var binds to0) from turning a cleanup cycle into a mass deletion.
Default: null (unlimited retention)
Factory Methods:
// Uniform retention: same TTL for all audit types
AuditRetentionPolicy.WithUniformRetention(int retentionDays)
// Error priority: keep errors longer than successful executions
AuditRetentionPolicy.WithErrorPriority(int successRetentionDays, int errorRetentionDays)
Examples:
Basic Setup (Uniform Retention):
var policy = AuditRetentionPolicy.WithUniformRetention(30);
builder.Services.AddEverTask(opt => opt
.RegisterTasksFromAssembly(typeof(Program).Assembly))
.AddSqlServerStorage(connectionString);
// AddAuditCleanup extends IServiceCollection (EverTask.Storage.EfCore package),
// NOT the EverTask builder: call it as a separate statement. It is the only
// entry-point that applies the policy.
builder.Services.AddAuditCleanup(policy, cleanupIntervalHours: 24);
Advanced Setup (Keep Errors Longer):
var policy = AuditRetentionPolicy.WithErrorPriority(
successRetentionDays: 7,
errorRetentionDays: 90);
builder.Services.AddEverTask(opt => opt
.RegisterTasksFromAssembly(typeof(Program).Assembly))
.AddSqlServerStorage(connectionString);
builder.Services.AddAuditCleanup(policy, cleanupIntervalHours: 24);
Custom Policy:
var policy = new AuditRetentionPolicy
{
StatusAuditRetentionDays = 14, // Status changes retained for 14 days
RunsAuditRetentionDays = 7, // Execution history retained for 7 days
ErrorAuditRetentionDays = 90, // Errors retained for 90 days
ExecutionLogRetentionDays = 30, // Captured execution logs trimmed after 30 days
MaxExecutionLogsPerTask = 1000, // Keep at most the latest 1000 logs per task
DeleteCompletedTasksAfterRetention = true // Purge completed task rows once aged out (see below)
};
builder.Services.AddEverTask(opt => opt
.RegisterTasksFromAssembly(typeof(Program).Assembly))
.AddSqlServerStorage(connectionString);
builder.Services.AddAuditCleanup(policy, cleanupIntervalHours: 12);
Retention Policy Properties:
| Property | Type | Default | Description |
|---|---|---|---|
StatusAuditRetentionDays | int? | null | Days to retain status audit records (Queued → InProgress → Completed/Failed) |
RunsAuditRetentionDays | int? | null | Days to retain execution audit records (recurring task runs) |
ErrorAuditRetentionDays | int? | null | Days to retain error audit records (overrides above for failures) |
ExecutionLogRetentionDays | int? | null | Days to retain captured execution logs (TaskExecutionLog), trimmed independently of the parent task (anchored on TimestampUtc) |
MaxExecutionLogsPerTask | int? | null | Per-task, cross-run cap: keep at most the latest N execution logs per task and delete the oldest beyond N |
DeleteCompletedTasksAfterRetention | bool | false | Hard-delete a completed non-recurring task once it is older than the longest retention window and has no audit rows |
DeleteCompletedTasksWithAuditsis[Obsolete]: a legacy alias that forwards toDeleteCompletedTasksAfterRetention. Don’t use it in new code; it remains only for source compatibility with pre-rename configs.When a completed task is deleted: when it is older than the longest of
StatusAuditRetentionDays/RunsAuditRetentionDays/ErrorAuditRetentionDays(measured fromLastExecutionUtc, falling back toCreatedAtUtc) and has no remaining StatusAudit/RunsAudit rows. If no retention window is configured, no completed tasks are deleted (a non-positive window counts as disabled). When a log-retention window or cap (ExecutionLogRetentionDays/MaxExecutionLogsPerTask) is active, a task that still has surviving logs is preserved, so its logs are never cascade-deleted before their own window expires; once those logs age out the task is purged. With no log retention configured, deleting the task cascades to everything it owns, captured execution logs included.Execution-log retention.
ExecutionLogRetentionDaysandMaxExecutionLogsPerTasktrimTaskExecutionLogrows on their own, without deleting the task, so a long-running service (recurring tasks especially) never accumulates logs without bound. Both default tonull(unlimited), so enabling persistent logging never starts deleting logs on its own. They are separate fromPersistentLoggerOptions.MaxLogsPerTask, which caps a single execution’s logs at capture time; these two trim logs across all past runs. When both are set, a log is deleted if it breaks either rule. Both are enforced byAddAuditCleanup(policy, …).
Cleanup Service Registration:
The AddAuditCleanup(policy, …) method (an IServiceCollection extension from the EverTask.Storage.EfCore package) registers a hosted service that periodically deletes old audit records:
builder.Services.AddAuditCleanup(
retentionPolicy, // The retention policy to apply (required)
cleanupIntervalHours: 24); // Cleanup frequency (default: 24 hours)
The service waits AuditCleanupOptions.InitialDelay (default 1 minute) after startup before its first sweep. InitialDelay is not a parameter of AddAuditCleanup; override it via services.Configure<AuditCleanupOptions>(o => o.InitialDelay = ...) if needed.
Important Notes:
- Single Entry-Point:
AddAuditCleanup(policy, ...)is the only place that applies the policy. (The formerSetAuditRetentionPolicy()builder method, which never applied it, has been removed.) - Cleanup Service Required: Retention is enforced by
AddAuditCleanup(policy, …)- without it, policy has no effect - Recurring Tasks: Never auto-deleted, even with
DeleteCompletedTasksAfterRetention = true(they need to reschedule) - Failed/Cancelled Tasks: Preserved for visibility, even with
DeleteCompletedTasksAfterRetention = true - Database Impact: Cleanup runs in background, deletes only tasks past the retention cutoff (no immediate hard-delete)
Monitoring Cleanup:
Check cleanup service logs:
[02:00:15 INF] AuditCleanupHostedService: Starting audit cleanup cycle
[02:00:16 INF] Deleted 1,543 status audit records older than 30 days
[02:00:16 INF] Deleted 8,921 runs audit records older than 30 days
[02:00:16 INF] Deleted 234 completed tasks with no remaining audits
[02:00:16 INF] AuditCleanupHostedService: Cleanup cycle completed in 1.2s
Recommended Settings by Workload:
| Workload Type | Success Retention | Error Retention | Cleanup Interval |
|---|---|---|---|
| Development | 7 days | 30 days | 24 hours |
| Production (Low Volume) | 30 days | 90 days | 24 hours |
| Production (High Volume) | 7 days | 90 days | 12 hours |
| Compliance/Audit | 365 days | 365 days | 24 hours |
SetThrowIfUnableToPersist
Controls what happens when a task can’t be saved to storage.
Signature:
SetThrowIfUnableToPersist(bool value)
Parameters:
value(bool): Whether to throw on persistence failure
Default: true
Examples:
// Throw on persistence failure (recommended)
opt.SetThrowIfUnableToPersist(true)
// Don't throw (tasks may be lost)
opt.SetThrowIfUnableToPersist(false)
Notes:
- When
true, the dispatch fails immediately if the task can’t be saved - When
false, the task might run but won’t be saved (risky!) - Keep this
trueunless you have a good reason not to
UseShardedScheduler
Enables a sharded scheduler that can handle extremely high loads by distributing work across multiple internal schedulers.
Signature:
UseShardedScheduler(int shardCount = 0)
Parameters:
shardCount(int): Number of shards;0(default) auto-scales toMath.Max(4, ProcessorCount)
Default: Not enabled (uses PeriodicTimerScheduler)
Examples:
// Auto-scale based on CPUs
opt.UseShardedScheduler()
// Fixed shard count
opt.UseShardedScheduler(8)
// Scale with CPUs
opt.UseShardedScheduler(Environment.ProcessorCount)
When to Use: You probably need this if you’re seeing:
- Sustained load above 10,000
Schedule()calls/second - Burst spikes above 20,000
Schedule()calls/second - More than 100,000 tasks scheduled at once
- High lock contention showing up in your profiler
RegisterTasksFromAssembly
Scans an assembly and registers all task handlers it finds.
Signature:
RegisterTasksFromAssembly(Assembly assembly)
Parameters:
assembly(Assembly): Assembly containing task handlers
Examples:
// Current assembly
opt.RegisterTasksFromAssembly(typeof(Program).Assembly)
// Specific assembly
opt.RegisterTasksFromAssembly(typeof(MyTask).Assembly)
// Assembly by name
opt.RegisterTasksFromAssembly(Assembly.Load("MyTasksAssembly"))
RegisterTasksFromAssemblies
Scans multiple assemblies and registers all task handlers from them.
Signature:
RegisterTasksFromAssemblies(params Assembly[] assemblies)
Parameters:
assemblies(Assembly[]): Assemblies containing task handlers
Examples:
opt.RegisterTasksFromAssemblies(
typeof(CoreTask).Assembly,
typeof(ApiTask).Assembly,
typeof(BackgroundTask).Assembly)
SetUseLazyHandlerResolution
Controls whether EverTask uses lazy handler resolution for scheduled and recurring tasks. When enabled (default), handlers are disposed after dispatch and recreated at execution time based on task scheduling characteristics.
Signature:
SetUseLazyHandlerResolution(bool enabled)
DisableLazyHandlerResolution() // Convenience method for disabling
Parameters:
enabled(bool): True to enable lazy resolution (default), false to disable
Default: true (enabled with adaptive algorithm)
Examples:
// Keep default (recommended - adaptive lazy resolution)
opt.RegisterTasksFromAssembly(typeof(Program).Assembly)
// Explicitly enable (same as default)
opt.SetUseLazyHandlerResolution(true)
// Disable lazy resolution (handlers kept in memory)
opt.SetUseLazyHandlerResolution(false)
// Convenience method for disabling
opt.DisableLazyHandlerResolution()
Adaptive Algorithm:
When enabled, EverTask automatically chooses the best resolution strategy:
- Immediate tasks: Lazy mode (v3.7+: the worker resolves a fresh handler in its per-task scope; an eager instance resolved at dispatch would stay pinned in the root container until shutdown)
- Recurring tasks with intervals ≥ 5 minutes: Lazy mode (memory efficient)
- Recurring tasks with intervals < 5 minutes: Eager mode (performance efficient)
- Delayed tasks with delay ≥ 30 minutes: Lazy mode
- Delayed tasks with delay < 30 minutes: Eager mode
Benefits:
- Memory Optimization: Handlers are disposed after dispatch, reducing memory footprint for long-running scheduled tasks
- Fresh Dependencies: Handlers get fresh scoped services at execution time (important for DbContext, etc.)
- Automatic Tuning: Adaptive algorithm balances memory and performance
When to Disable:
Only disable lazy resolution if:
- You have handlers with expensive initialization that should be cached
- Your environment has issues with lazy resolution (rare)
- You’re debugging handler lifecycle issues
Performance Impact:
- Memory: Up to 43,000 fewer handler allocations per day for high-frequency recurring tasks
- CPU: Negligible overhead (handler instantiation is fast with DI)
Notes:
- Handler dependencies are resolved at execution time, ensuring fresh scoped services
- At dispatch time, a short-lived metadata instance is resolved (and disposed with its scope) to extract handler options
SetRateLimiterOptions
Configures the global infrastructure knobs of the keyed rate limiter (v3.7+). See the dedicated Rate Limiting Configuration section below for the full reference (global knobs, per-handler RateLimitPolicy, key source).
Queue Configuration
You can set up multiple queues to isolate different types of work and give them different priorities or resource allocations.
The
EverTaskServiceBuilderreturned byAddEverTask(...)also exposes a public.Servicesproperty (the underlyingIServiceCollection), so you can register your own services mid-chain without breaking the fluent flow, e.g.builder.Services.AddSingleton<IKeyedRateLimiter, MyRedisLimiter>()or a customIGuidGenerator. There is alsoEnsureRecurringQueue()to create the recurring queue with defaults without a configure action (normally unnecessary: both thedefaultandrecurringqueues are auto-created during service registration, inRegisterQueueManager, if not configured). The well-known queue names are the public constantsQueueNames.Default("default") andQueueNames.Recurring("recurring").
ConfigureDefaultQueue
Customizes the default queue (used when you don’t specify a queue name for a task).
Signature:
ConfigureDefaultQueue(Action<QueueConfiguration> configure)
Example:
.ConfigureDefaultQueue(q => q
.SetMaxDegreeOfParallelism(10)
.SetChannelCapacity(1000)
.SetFullBehavior(QueueFullBehavior.Wait)
.SetDefaultTimeout(TimeSpan.FromMinutes(5))
.SetDefaultRetryPolicy(new LinearRetryPolicy(3, TimeSpan.FromSeconds(1))))
AddQueue
Creates a new named queue with its own configuration.
Signature:
AddQueue(string name, Action<QueueConfiguration>? configure = null)
Parameters:
name(string): Queue name (throwsArgumentExceptionwhen null/whitespace)configure(Action, optional): Queue configuration
Defaults for new queues (different from the auto-created default queue, which inherits the global settings with QueueFullBehavior.Wait):
MaxDegreeOfParallelism= 1 (sequential: set it explicitly for parallel consumption)- Channel capacity = 500 (
FullMode.Wait) QueueFullBehavior=FallbackToDefault(a full queue spills to the default queue)- Retry policy / timeout = unset (fall back to the global defaults)
Calling AddQueue again with the same name replaces the previous configuration (no error is raised).
Raw object defaults. The values above are what
AddQueueapplies. A barenew QueueConfiguration()(if you construct one directly rather than viaAddQueue) defaults to:Name = "default",MaxDegreeOfParallelism = 1,ChannelOptions = new BoundedChannelOptions(2000) { FullMode = Wait, SingleReader = false, SingleWriter = false, AllowSynchronousContinuations = false },QueueFullBehavior = FallbackToDefault, and null retry policy / timeout. Note the raw channel capacity is 2000, whereasAddQueuesets 500.
Example:
.AddQueue("high-priority", q => q
.SetMaxDegreeOfParallelism(20)
.SetChannelCapacity(500)
.SetFullBehavior(QueueFullBehavior.Wait))
.AddQueue("background", q => q
.SetMaxDegreeOfParallelism(2)
.SetChannelCapacity(100)
.SetFullBehavior(QueueFullBehavior.FallbackToDefault))
ConfigureRecurringQueue
Customizes the recurring queue (EverTask automatically creates this queue for recurring tasks).
Signature:
ConfigureRecurringQueue(Action<QueueConfiguration> configure)
Example:
.ConfigureRecurringQueue(q => q
.SetMaxDegreeOfParallelism(5)
.SetChannelCapacity(200)
.SetDefaultTimeout(TimeSpan.FromMinutes(10)))
EnsureRecurringQueue
Creates the recurring queue with default settings only if it doesn’t already exist. Normally unnecessary: both the default and recurring queues are auto-created during service registration (RegisterQueueManager). Use it only if you want to guarantee the recurring queue exists without supplying a configure action (it is idempotent: a no-op when the queue is already present).
Signature:
EnsureRecurringQueue() // returns EverTaskServiceBuilder for chaining
Behavior: when the recurring queue is absent, it clones the existing default queue configuration (or, if even that is absent, a fresh QueueConfiguration seeded from the global MaxDegreeOfParallelism / ChannelOptions / retry / timeout) and registers it under QueueNames.Recurring. When the queue already exists it does nothing.
QueueConfiguration Methods
Each queue supports these configuration methods:
// Parallelism
SetMaxDegreeOfParallelism(int parallelism)
// Capacity
SetChannelCapacity(int capacity)
// Full channel options replacement (FullMode, SingleReader/Writer, ...)
SetChannelOptions(BoundedChannelOptions options)
// Full behavior
SetFullBehavior(QueueFullBehavior behavior)
// Timeout (null reverts to the global default)
SetDefaultTimeout(TimeSpan? timeout)
// Retry policy (null reverts to the global default)
SetDefaultRetryPolicy(IRetryPolicy? policy)
Per-queue retry/timeout resolution chain (v3.7+): handler override → queue default → global default. The queue is the task’s declared queue: a task rerouted by FallbackToDefault keeps its declared queue’s retry/timeout.
QueueFullBehavior values (applies to immediate dispatches only; scheduler-triggered dispatches use a non-blocking write + backoff):
| Value | Behavior |
|---|---|
Wait | Block until space frees (cancellable via the dispatch CancellationToken). Default of the auto-created default queue. |
FallbackToDefault | First tries the target queue with a non-blocking write; if it’s full, logs a warning and re-routes the task to the default queue with blocking Wait backpressure (it does not throw unless the default queue itself is unavailable). Two consequences: (1) once on the default queue the task runs there, so it does not honor the target queue’s MaxDegreeOfParallelism/isolation; (2) if the target queue is the default queue, this degenerates to plain Wait (self-reference). Default for AddQueue-created queues. |
ThrowException | Throw QueueFullException; the task stays persisted as WaitingQueue and is re-enqueued by startup recovery. |
Rate Limiting Configuration
Keyed rate limiting (v3.7+) constrains how often tasks of a type execute per key (tenant, account, external resource). Behavior, semantics, and edge cases are documented in Keyed Rate Limiting; this section covers the configuration surface.
Configuration lives in three places:
- Per-handler policy: the
RateLimitPolicyproperty on the handler declares the limit. - Key source: the task implements
IRateLimitedTask, or the handler overridesGetRateLimitKey. If the key is null/empty (or the key selector throws), the exception is caught, a warning is logged, and the task runs ungated (fail-open), never failing the task over a key-resolution error. - Global knobs:
SetRateLimiterOptionsbounds the limiter infrastructure.
RateLimitPolicy (per handler)
Declares the per-key execution budget for a task type.
Declaration:
public class SyncTenantHandler : EverTaskHandler<SyncTenantTask>
{
// 15 executions per minute PER KEY
public override RateLimitPolicy? RateLimitPolicy =>
new RateLimitPolicy(permits: 15, period: TimeSpan.FromMinutes(1))
{
Burst = 15,
ThrottleRetries = true,
StartEmpty = false,
MaxReservationHorizon = TimeSpan.FromHours(1),
MaxInSlotWait = TimeSpan.FromSeconds(1),
OverflowBehavior = RateLimitOverflowBehavior.WaitForCapacity
};
public override async Task Handle(SyncTenantTask task, CancellationToken ct) { ... }
}
Constructor:
permits(int): executions allowed perperiod. Must be > 0.period(TimeSpan): the budget window. Must be > 0.
Properties:
| Property | Type | Default | Description |
|---|---|---|---|
Permits | int | n/a (constructor, required) | Public get-only property set from the constructor: executions allowed per Period. Must be > 0 |
Period | TimeSpan | n/a (constructor, required) | Public get-only property set from the constructor: the rolling window for Permits. Must be > 0 |
Burst | int | Permits | Burst tolerance (≥ 1). 1 = strict even spacing (Period / Permits between executions); Permits = the full budget can front-load |
ThrottleRetries | bool | true | Retry attempts re-acquire the key’s budget through the gate (the re-acquire happens before the per-attempt timeout starts, so a budget wait never erodes it). This is not an inline wait between attempts: if the next free slot is far, the retry path stops the in-process retry loop and re-parks the task to the scheduler; it fires again via redelivery, and the retry attempt numbering restarts from the redelivered execution. false lets retries run without re-acquiring budget. |
StartEmpty | bool | false | When false (default), a fresh bucket starts full: the entire burst is available immediately (a restart can front-load up to Burst executions). Set to true to start at the steady rate from the first execution, capping the post-restart burst |
MaxReservationHorizon | TimeSpan | 1 hour | Slots farther than this are never parked: terminal rejection (one-shot → Failed + OnError; recurring → occurrence skipped) |
MaxInSlotWait | TimeSpan | 1 second | No-op, retained for binary compatibility only. The gate no longer waits inline on the consumer: every over-budget task (near or far slot) is re-parked to the scheduler and fires at its reserved slot via redelivery. An inline wait would head-of-line-block the single consumer (including unthrottled tasks behind it). |
OverflowBehavior | RateLimitOverflowBehavior | WaitForCapacity | WaitForCapacity defers over-budget tasks to their reserved slot; Discard terminally rejects them (one-shot → Failed + OnError; recurring → occurrence skipped) |
Notes:
- The policy is read once per handler type (first-wins cache); changing it requires a restart.
- A policy without a key (see below) logs a warning once per task type and executes ungated (fail-safe).
- Limiter outage fails open. If the
IKeyedRateLimiteritself throws while acquiring budget (most relevant for a custom/distributed implementation, e.g. Redis unreachable), the gate logs a warning and lets the task execute unthrottled rather than failing it (the never-lose-a-task contract). The same fail-open applies whenMaxTrackedKeysoverflows. OnlyOperationCanceledException(service shutdown) propagates, leaving the task in a recoverable status for next-startup recovery.
Rate-Limit Key Source
The key is derived per dispatch from the task:
// Option A: the task declares its key
public record SyncTenantTask(Guid TenantId) : IEverTask, IRateLimitedTask
{
public string RateLimitKey => TenantId.ToString();
}
// Option B: the handler derives the key (overrides IRateLimitedTask if both present)
public class SyncTenantHandler : EverTaskHandler<SyncTenantTask>
{
public override string? GetRateLimitKey(SyncTenantTask task) => task.TenantId.ToString();
}
Keep keys low-cardinality and stable (tenant ids, account ids); see Best Practices.
SetRateLimiterOptions (global knobs)
Bounds the limiter infrastructure process-wide. These are safety valves, not per-task limits.
Signature:
SetRateLimiterOptions(Action<RateLimiterOptions> configure)
Example:
opt.SetRateLimiterOptions(o =>
{
o.MaxParkedTasks = 5000;
o.MaxTrackedKeys = 100_000;
o.MaxKeyLength = 256;
o.EmitDeferralEvents = true;
});
Options:
| Option | Type | Default | Description |
|---|---|---|---|
MaxParkedTasks | int | min(5000, 2 × default-queue channel capacity) | Cap of DISTINCT rate-limited tasks parked waiting for budget; at the cap, consumers pause (bounded) before dequeued rate-limited tasks of the affected queues (unthrottled traffic keeps flowing), so backpressure reaches producers |
MaxTrackedKeys | int | 100,000 | Maximum (task type, key) buckets tracked in memory; new keys beyond the cap fail OPEN (execute unthrottled) with a warning and a monitoring event |
MaxKeyLength | int | 256 | Keys longer than this are hashed (SHA-256) before use |
EmitDeferralEvents | bool | true | Publish deferral monitoring events (aggregated at the source: first deferral per key per window plus periodic summaries) |
Notes:
- The
MaxParkedTasksdefault is computed at first resolution, after builder methods likeConfigureDefaultQueue(q => q.SetChannelCapacity(...))have run. - The rate limiter is per-instance (in-memory): N app instances each enforce the limit independently; see Multi-Instance.
Storage Configuration
Choose where EverTask saves task data.
AddMemoryStorage
Uses in-memory storage (fine for development/testing, but tasks won’t survive a restart).
Signature:
AddMemoryStorage()
Example:
builder.Services.AddEverTask(opt => opt.RegisterTasksFromAssembly(typeof(Program).Assembly))
.AddMemoryStorage();
Characteristics:
- No external dependencies
- Fast performance
- Tasks lost on restart
AddSqlServerStorage
Uses SQL Server for persistent storage.
Signature:
AddSqlServerStorage(string connectionString, Action<SqlServerTaskStoreOptions>? configure = null)
Parameters:
connectionString(string): SQL Server connection stringconfigure(Action, optional): Storage configuration options
Examples:
// Basic
.AddSqlServerStorage("Server=localhost;Database=EverTaskDb;Trusted_Connection=True;")
// With options
.AddSqlServerStorage(
connectionString,
opt =>
{
opt.SchemaName = "EverTask";
opt.AutoApplyMigrations = true;
})
SqlServerTaskStoreOptions Properties:
SchemaName(string?): Database schema name (default: “EverTask”);nullor empty falls back todbo(used for stored-procedure execution)AutoApplyMigrations(bool): Auto-apply EF Core migrations (default: true)
AddPostgresStorage
Uses PostgreSQL (via Npgsql) for persistent storage.
Signature:
AddPostgresStorage(string connectionString, Action<PostgresTaskStoreOptions>? configure = null)
Parameters:
connectionString(string): Npgsql connection string (e.g.Host=localhost;Database=evertask;Username=...;Password=...)configure(Action, optional): Storage configuration options
Examples:
// Basic
.AddPostgresStorage("Host=localhost;Database=evertask;Username=evertask;Password=***")
// With options
.AddPostgresStorage(
connectionString,
opt =>
{
opt.SchemaName = "evertask";
opt.AutoApplyMigrations = true;
})
PostgresTaskStoreOptions Properties:
SchemaName(string?): Database schema name (default: “evertask”, must be lowercase; null =publicschema)AutoApplyMigrations(bool): Auto-apply EF Core migrations (default: true)
AddSqliteStorage
Uses SQLite for persistent storage.
Signature:
AddSqliteStorage(string connectionString = "Data Source=EverTask.db",
Action<SqliteTaskStoreOptions>? configure = null)
Parameters:
connectionString(string, optional): SQLite connection string; defaults to"Data Source=EverTask.db", so.AddSqliteStorage()with no arguments is validconfigure(Action, optional): Storage configuration options
Examples:
// Zero-config (Data Source=EverTask.db)
.AddSqliteStorage()
// Basic
.AddSqliteStorage("Data Source=evertask.db")
// With options
.AddSqliteStorage(
"Data Source=evertask.db;Cache=Shared;",
opt =>
{
opt.AutoApplyMigrations = true;
})
Notes:
SchemaNamemust remain an empty string (""): SQLite has no schema concept, do not change it
Logging Configuration
AddSerilog
Integrates Serilog for structured logging throughout EverTask.
Package: EverTask.Logging.Serilog
Signature:
AddSerilog(Action<LoggerConfiguration>? configure = null)
Parameters:
configure(Action, optional): Serilog logger configuration; calling.AddSerilog()with no arguments configures a Console-only sink
Example:
.AddSerilog(opt =>
opt.ReadFrom.Configuration(
configuration,
new ConfigurationReaderOptions { SectionName = "EverTaskSerilog" }))
appsettings.json Example:
{
"EverTaskSerilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
},
"WriteTo": [
{
"Name": "Console"
},
{
"Name": "File",
"Args": {
"path": "Logs/evertask-.txt",
"rollingInterval": "Day",
"retainedFileCountLimit": 10
}
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"],
"Properties": {
"Application": "MyApp"
}
}
}
WithPersistentLogger
Available since: v3.0
Configures persistent handler logging options. When enabled, logs written via Logger property in handlers are stored in the database for audit trails.
Important: Logs are ALWAYS forwarded to ILogger infrastructure (console, file, Serilog, etc.) regardless of this setting. This option only controls database persistence.
Signature:
WithPersistentLogger(Action<PersistentLoggerOptions> configure)
Parameters:
configure(Action): Configuration action for persistent logger options
Default: Disabled
Example:
.AddEverTask(opt => opt
.WithPersistentLogger(log => log
.SetMinimumLevel(LogLevel.Information)
.SetMaxLogsPerTask(1000)))
Note: Calling .WithPersistentLogger() automatically enables database persistence. You don’t need to call .Enable().
PersistentLoggerOptions Methods:
Enable() / Disable()
Enable() turns on database persistence; Disable() turns it off (logs still flow to ILogger in both cases). WithPersistentLogger(...) already calls Enable() for you, so Enable() is rarely needed explicitly. There is also a settable Enabled (bool) property backing both.
.WithPersistentLogger(log => log.Disable()) // configured but persistence off
SetMinimumLevel(LogLevel level)
Sets the minimum log level for database persistence. Logs below this level are not stored in the database but are still forwarded to ILogger.
Parameters:
level(LogLevel): Minimum level to persist (Trace,Debug,Information,Warning,Error,Critical)
Default: LogLevel.Information
Example:
.WithPersistentLogger(log => log
.SetMinimumLevel(LogLevel.Warning)) // Only persist Warning and above
Note: This only affects database persistence. ILogger receives all log levels regardless of this setting.
SetMaxLogsPerTask(int? maxLogs)
Sets the maximum number of logs to persist per task execution. Once this limit is reached, additional logs are not persisted (but still forwarded to ILogger), except for a single appended truncation marker record noting that logs were dropped.
Parameters:
maxLogs(int?): Maximum logs to persist.null= unlimited (not recommended for production)
Default: 1000
Example:
.WithPersistentLogger(log => log
.SetMaxLogsPerTask(500)) // Limit to 500 logs
Performance: ~100 bytes per log in memory during execution. Single bulk INSERT to database after task completion.
Complete Example:
.AddEverTask(opt => opt
.RegisterTasksFromAssembly(typeof(Program).Assembly)
.WithPersistentLogger(log => log
.SetMinimumLevel(LogLevel.Information)
.SetMaxLogsPerTask(1000)))
Monitoring Configuration
AddMonitoringApi
Adds the EverTask Monitoring API with an optional embedded React dashboard for monitoring and managing tasks.
Package: EverTask.Monitor.Api
Signature:
AddMonitoringApi() // on EverTaskServiceBuilder
AddMonitoringApi(Action<EverTaskApiOptions> configure)
For apps that don’t use the EverTask builder chain, there is an IServiceCollection variant: services.AddEverTaskMonitoringApiStandalone(Action<EverTaskApiOptions>? configure = null): it does not auto-register SignalR monitoring and requires you to register ITaskStorage yourself.
Parameters:
configure(Action): Configuration options for the monitoring API
Examples:
Basic Setup (Default Settings):
.AddMonitoringApi()
// Dashboard: http://localhost:5000/evertask-monitoring
// API: http://localhost:5000/evertask-monitoring/api
// Credentials: admin / admin
Custom Configuration:
.AddMonitoringApi(options =>
{
options.EnableUI = true;
options.Username = "monitor_user";
options.Password = "secure_password_123";
options.EnableAuthentication = true;
options.EnableCors = true;
options.CorsAllowedOrigins = new[] { "https://myapp.com" };
})
API-Only Mode (No Dashboard):
.AddMonitoringApi(options =>
{
options.EnableUI = false; // Disable embedded dashboard
options.EnableAuthentication = false; // Open API for custom frontend
})
Environment-Specific Configuration:
.AddMonitoringApi(options =>
{
options.EnableUI = true;
if (builder.Environment.IsDevelopment())
{
// Development: No authentication
options.EnableAuthentication = false;
}
else
{
// Production: Secure credentials from environment
options.EnableAuthentication = true;
options.Username = Environment.GetEnvironmentVariable("MONITOR_USERNAME")
?? throw new InvalidOperationException("MONITOR_USERNAME not set");
options.Password = Environment.GetEnvironmentVariable("MONITOR_PASSWORD")
?? throw new InvalidOperationException("MONITOR_PASSWORD not set");
options.EnableCors = true;
options.CorsAllowedOrigins = new[] { "https://app.example.com" };
}
})
EverTaskApiOptions Properties:
| Property | Type | Default | Description |
|---|---|---|---|
EnableUI | bool | true | Enable embedded React dashboard |
EnableSwagger | bool | false | Enable Swagger/OpenAPI documentation |
Username | string | "admin" | JWT Authentication username |
Password | string | "admin" | JWT Authentication password (CHANGE IN PRODUCTION!) |
EnableAuthentication | bool | true | Enable JWT Authentication |
JwtSecret | string? | null | JWT signing key; when unset, a random 256-bit secret is generated per instance. Set it explicitly (≥ 32 bytes) for multi-instance deployments |
JwtIssuer | string | "EverTask.Monitor.Api" | JWT issuer claim |
JwtAudience | string | "EverTask.Monitor.Api" | JWT audience claim |
JwtExpirationHours | int | 8 | JWT token TTL in hours |
EnableCors | bool | true | Registers a named CORS policy (EverTaskMonitoringApi); EverTask does NOT apply it: your app must (app.UseCors(...)). See note below |
CorsAllowedOrigins | string[] | [] | Origins for the registered policy (empty = allow-any). Only effective once the policy is actually applied |
AllowedIpAddresses | string[] | [] | IP address whitelist (empty = allow all IPs). Supports IPv4, IPv6, and CIDR notation |
MagicLinkToken | string? | null | Static token for magic link authentication. When set, enables instant access via /api/auth/magic?token=... |
EventDebounceMs | int | 1000 | Debounce time in milliseconds for SignalR event-driven cache invalidation in the dashboard. Higher values reduce API load during task bursts but introduce slight UI update delays. Recommended: 300ms (very responsive), 500ms (balanced), 1000ms (conservative for high-volume) |
BasePath | string | /evertask-monitoring | Read-only computed property (fixed; cannot be set) |
ApiBasePath | string | /evertask-monitoring/api | Read-only computed property ({BasePath}/api) |
UIBasePath | string | /evertask-monitoring | Read-only computed property (= BasePath) |
SignalRHubPath | string | /evertask-monitoring/hub | Read-only computed property (fixed when using AddMonitoringApi/MapEverTaskApi) |
EnableUI
Controls whether the embedded React dashboard is served.
Examples:
// Full mode (default): API + Dashboard
options.EnableUI = true;
// API-only mode: REST API without dashboard
options.EnableUI = false;
Use Cases for API-Only Mode:
- Building custom frontend applications
- Mobile app integration
- Third-party monitoring system integration
- Headless server environments
EnableSwagger
Controls whether Swagger/OpenAPI documentation is generated for the monitoring API.
When enabled, EverTask creates a separate Swagger document that includes only monitoring endpoints and automatically excludes them from your application’s Swagger document.
Examples:
// Enable Swagger for monitoring API
options.EnableSwagger = true;
// Configure SwaggerUI in your application
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "My Application API", Version = "v1" });
});
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My Application API");
c.SwaggerEndpoint("/swagger/evertask-monitoring/swagger.json", "EverTask Monitoring API");
});
How It Works:
- Swagger document name:
evertask-monitoring - Swagger JSON endpoint:
/swagger/evertask-monitoring/swagger.json - Includes only EverTask monitoring controllers (
/evertask-monitoring/api/*) - Your application’s Swagger document automatically excludes EverTask endpoints
- No manual filtering or namespace predicates required
Use Cases:
- API documentation and exploration
- Integration with API clients and code generators
- Testing monitoring endpoints with Swagger UI
- API versioning and contract validation
Username / Password
JWT Authentication credentials for accessing the monitoring dashboard and API.
Examples:
// Development (not recommended for production)
options.Username = "admin";
options.Password = "admin";
// Production: Environment variables
options.Username = Environment.GetEnvironmentVariable("MONITOR_USERNAME") ?? "admin";
options.Password = Environment.GetEnvironmentVariable("MONITOR_PASSWORD") ?? "changeme";
// Production: Configuration
options.Username = configuration["Monitoring:Username"];
options.Password = configuration["Monitoring:Password"];
Security Notes:
- Always change default credentials in production
- Use environment variables or secure configuration systems
- Always use HTTPS when authentication is enabled
- Consider using anonymous read access for internal networks
EnableAuthentication
Controls whether JWT Authentication is required for API endpoints and SignalR hub.
Examples:
// Require authentication (default, recommended for production)
options.EnableAuthentication = true;
// No authentication (development only)
options.EnableAuthentication = false;
// Environment-specific
options.EnableAuthentication = !builder.Environment.IsDevelopment();
Protection Scope:
- API endpoints: All
/api/*endpoints (except login and config) - SignalR hub: Real-time monitoring hub at
/evertask-monitoring/hub - UI: Not protected by JWT (only IP whitelist, see
AllowedIpAddresses)
Always Accessible (No JWT Required):
/api/config- Dashboard configuration endpoint/api/auth/login- Login endpoint for obtaining JWT/api/auth/validate- Token validation endpoint/api/auth/magic- Magic-link token exchange (returns 404 whenMagicLinkTokenis not configured)- UI static files (HTML, JS, CSS)
JWT Authentication Flow:
- Client authenticates via
/api/auth/loginwith username/password - Server returns JWT token
- Client includes token in subsequent requests:
- API:
Authorization: Bearer <token>header - SignalR:
accessTokenFactoryoption or?access_token=<token>query string
- API:
Notes:
- When disabled, all API and hub endpoints are publicly accessible (only IP whitelist applies)
- UI is always accessible (relies on IP whitelist for protection)
- JWT tokens expire after 8 hours by default (see
JwtExpirationHours)
SignalRHubPath
The SignalR hub path is now fixed to /evertask-monitoring/hub and cannot be changed.
Notes:
- The hub path is readonly and set to
/evertask-monitoring/hub - SignalR monitoring is automatically configured if not already registered
- Dashboard automatically uses this fixed path for real-time updates
EnableCors
When true, registers a named CORS policy (EverTaskMonitoringApi) via AddCors.
Examples:
// Register the CORS policy (default)
options.EnableCors = true;
// Don't register it
options.EnableCors = false;
⚠ Important: EverTask only registers this policy; it does not apply it (
MapEverTaskApi/the startup filter never callUseCorsorRequireCors). For cross-origin requests to actually be permitted, your application must apply the policy itself, e.g.app.UseCors("EverTaskMonitoringApi")in the pipeline. With API and dashboard on the same origin (the default embedded-UI setup) no CORS is needed.
Notes:
- Relevant when the dashboard/frontend is hosted on a different origin from the API
- Not needed when API and frontend are on the same origin (default embedded UI)
CorsAllowedOrigins
Specifies allowed origins for CORS requests.
Examples:
// Allow all origins (default, useful for development)
options.CorsAllowedOrigins = Array.Empty<string>();
// Restrict to specific origins (production)
options.CorsAllowedOrigins = new[]
{
"https://myapp.com",
"https://dashboard.myapp.com"
};
// Environment-specific origins
options.CorsAllowedOrigins = builder.Environment.IsDevelopment()
? Array.Empty<string>() // Allow all in development
: new[] { "https://app.example.com" }; // Restrict in production
Security Notes:
- Empty array = allow all origins (convenient for development)
- Always restrict origins in production
- Use HTTPS origins in production
AllowedIpAddresses
Restricts monitoring access to specific IP addresses or CIDR ranges. Applies to both API endpoints and SignalR hub.
Examples:
// Allow all IPs (default)
options.AllowedIpAddresses = Array.Empty<string>();
// Restrict to specific IPs (production)
options.AllowedIpAddresses = new[]
{
"192.168.1.100", // Specific admin workstation
"10.0.0.0/8", // Internal network (CIDR notation)
"172.16.0.0/12", // Another internal range
"::1" // IPv6 localhost
};
// Reverse proxy scenario (public IP ranges)
options.AllowedIpAddresses = new[]
{
"203.0.113.0/24" // Office public IP range
};
Features:
- Supports IPv4 and IPv6 addresses
- Supports CIDR notation (e.g.,
192.168.0.0/24) - Checks
X-Forwarded-Forheader first (reverse proxy support) - Returns 403 Forbidden if IP not in whitelist
- IP check runs before authentication (more efficient)
Security Notes:
- Empty array = allow all IPs (default, suitable for internal networks)
- Always configure in production when exposed to internet
- Works with reverse proxies (nginx, IIS, etc.)
- Protects both API and SignalR hub endpoints
- More efficient than firewall rules at application level
Reverse Proxy Configuration: When behind a reverse proxy, ensure X-Forwarded-For header is set:
# Nginx example
location /evertask-monitoring {
proxy_pass http://localhost:5000/evertask-monitoring;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
MagicLinkToken
Enables instant authentication via a static token URL. Useful for embedding the dashboard in other systems or providing quick access without credential management.
Examples:
// Enable magic link access
options.MagicLinkToken = "your-very-long-secret-token-here-min-32-chars";
// Combined with IP whitelist for extra security
options.MagicLinkToken = "your-secret-token";
options.AllowedIpAddresses = new[] { "10.0.0.0/8" };
Access URL:
https://your-server/evertask-monitoring/magic?token=your-very-long-secret-token-here-min-32-chars
How it works:
- User visits the magic link URL
- Backend validates the token against
MagicLinkToken - If valid, generates a standard JWT session token
- User is redirected to the dashboard, fully authenticated
Security Notes:
- Use a long, random token (32+ characters recommended)
- Token never expires - change it in configuration to revoke all magic link access
- Combine with
AllowedIpAddressesfor defense in depth - If
MagicLinkTokenis not set, the endpoint returns 404
Token Generation:
# PowerShell - generate secure random token
[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Maximum 256 }) -as [byte[]])
API Endpoints
Once configured, the monitoring API exposes REST endpoints for querying tasks and reading statistics. All endpoints are relative to {BasePath}/api (default: /evertask-monitoring/api).
Main endpoints:
GET /tasks- Paginated task list with filteringGET /tasks/{id}- Task detailsGET /tasks/{id}/status-audit- Status change historyGET /tasks/{id}/runs-audit- Execution historyGET /tasks/{id}/execution-logs- Persisted handler logs (when persistent logging is enabled)GET /dashboard/overview- Dashboard statisticsGET /queues- Queue metricsGET /statistics/success-rate-trend- Success rate trendsGET /rate-limits- Keyed rate-limit state (per-key parked count, next slot, tracked keys, fail-open count; in-memory, single-node)
See Monitoring Dashboard for complete API documentation.
Dashboard Features
When EnableUI is true, the embedded React dashboard provides:
- Overview Dashboard: Total tasks, success rate, active queues, execution times
- Task List: Filtering, sorting, pagination, status filters
- Task Details: Complete information, execution history, error details
- Queue Metrics: Per-queue statistics and health monitoring
- Analytics: Success rate trends, task type distribution, execution times
- Real-Time Updates: Live task updates via SignalR
Mapping Endpoints
After configuring the monitoring API, map the endpoints in your application:
var app = builder.Build();
// Map EverTask monitoring endpoints (includes SignalR hub automatically)
app.MapEverTaskApi();
app.Run();
MapEverTaskApi() maps endpoints only:
- Maps SignalR monitoring hub (at
/evertask-monitoring/hub) with automatic JWT authentication - Maps all API controllers
- Serves embedded dashboard (if
EnableUIis true)
MapEverTaskApi()does not wire JWT authentication middleware or apply a CORS policy. The JWT middleware is wired automatically byAddMonitoringApi()(via anIStartupFilter); the CORS policy is only registered byAddMonitoringApi()(AddCors) and is not applied: if you need it enforced, callapp.UseCors("EverTaskMonitoringApi")yourself. No manualUseEverTaskApiMiddleware()call is needed (that method is obsolete).
Important Notes:
- The monitoring API handles SignalR setup completely autonomously:
AddMonitoringApi()automatically registers SignalR monitoring services (if not already registered)MapEverTaskApi()automatically maps the SignalR hub endpoint with authentication- No additional SignalR configuration is required unless you want to customize hub options
- To customize hub options, pass an
Action<HttpConnectionDispatcherOptions>toMapEverTaskApi():app.MapEverTaskApi(hubOptions => { // Custom SignalR hub configuration hubOptions.TransportMaxBufferSize = 1024 * 1024; // 1MB buffer hubOptions.ApplicationMaxBufferSize = 1024 * 1024; });
Integration with SignalR
The monitoring API automatically configures SignalR monitoring if it hasn’t been added:
// This is sufficient - SignalR is auto-configured
.AddMonitoringApi()
// Manual SignalR configuration (if you need more control)
.AddSignalRMonitoring(opt =>
{
opt.IncludeExecutionLogs = true; // Include logs in SignalR events
})
.AddMonitoringApi()
// Note: SignalRHubPath is now fixed to "/evertask-monitoring/hub" and cannot be changed
AddSignalRMonitoring
Enables real-time task monitoring via SignalR.
Package: EverTask.Monitor.AspnetCore.SignalR
Signature:
AddSignalRMonitoring()
AddSignalRMonitoring(Action<SignalRMonitoringOptions> monitoringConfiguration)
AddSignalRMonitoring(Action<HubOptions> hubConfiguration)
AddSignalRMonitoring(Action<HubOptions> hubConfiguration, Action<SignalRMonitoringOptions> monitoringConfiguration)
Parameters:
configure(Action): Monitoring configuration options (SignalRMonitoringOptions)hubOptions(Action): SignalRHubOptionscustomization
Examples:
// Basic (default configuration)
.AddSignalRMonitoring()
// With execution log streaming enabled
.AddSignalRMonitoring(opt =>
{
opt.IncludeExecutionLogs = true; // Stream logs to SignalR clients (increases bandwidth)
})
SignalRMonitoringOptions Properties:
| Property | Type | Default | Description |
|---|---|---|---|
IncludeExecutionLogs | bool | false | Include execution logs in SignalR events (increases message size) |
Standalone usage (without Monitor.Api):
When using the SignalR package without AddMonitoringApi()/MapEverTaskApi(), you MUST map the hub yourself or no events are broadcast:
app.MapEverTaskMonitorHub(); // default route /evertask-monitoring/hub
app.MapEverTaskMonitorHub("/custom/hub"); // custom route
app.MapEverTaskMonitorHub("/custom/hub", hub => // custom route + SignalR hub dispatcher options
{
hub.TransportMaxBufferSize = 1024 * 1024;
});
MapEverTaskMonitorHub maps the hub and subscribes the monitor, so it is required in standalone mode. Overloads: () (default pattern), (string pattern), and (string pattern, Action<HttpConnectionDispatcherOptions>).
Important Notes:
- Hub Route: When mapped by
MapEverTaskApi()the route is fixed atEverTaskApiOptions.SignalRHubPath(/evertask-monitoring/hub, read-only). In standalone mode the route is configurable:MapEverTaskMonitorHub(pattern)accepts any pattern (defaulting to/evertask-monitoring/hub); if you choose a custom pattern, point your client at the same path. - Log Streaming: Execution logs are always available via ILogger and database persistence (if enabled)
- Performance Impact: Enabling
IncludeExecutionLogssignificantly increases SignalR message size and network bandwidth - Use Case: Enable only when you need real-time log streaming to monitoring dashboards
Client-Side Setup:
<!-- Add SignalR client library -->
<script src="https://cdn.jsdelivr.net/npm/@microsoft/signalr@latest/dist/browser/signalr.min.js"></script>
<script>
const connection = new signalR.HubConnectionBuilder()
.withUrl("/evertask-monitoring/hub") // must match the mapped hub route (fixed under MapEverTaskApi; configurable under standalone MapEverTaskMonitorHub)
.withAutomaticReconnect()
.build();
connection.on("EverTaskEvent", (eventData) => {
console.log("Task event:", eventData);
// eventData.TaskId, eventData.Severity, eventData.Message, eventData.Exception, etc.
});
connection.start()
.then(() => console.log("SignalR connected"))
.catch(err => console.error("SignalR connection error:", err));
</script>
Event Data Structure:
{
"TaskId": "dc49351d-476d-49f0-a1e8-3e2a39182d22",
"EventDateUtc": "2024-10-19T16:10:20Z",
"Severity": "Information", // "Information" | "Warning" | "Error"
"TaskType": "MyApp.Tasks.SendEmailTask",
"TaskHandlerType": "MyApp.Tasks.SendEmailHandler",
"TaskParameters": "{\"Email\":\"user@example.com\"}",
"Message": "Task completed successfully",
"Exception": null // Stack trace if task failed
}
Severity Levels:
Information: Task started, completed, or scheduledWarning: Task cancelled or timed outError: Task failed with exception
Storage Provider Details
SQL Server Storage Options
Package: EverTask.Storage.SqlServer
Advanced Configuration:
.AddSqlServerStorage(connectionString, opt =>
{
// Schema name (default: "EverTask"); null/empty falls back to dbo
opt.SchemaName = "EverTask";
// Auto-apply migrations (default: true)
opt.AutoApplyMigrations = true;
})
// Note: there are only two configurable options (SchemaName, AutoApplyMigrations).
// DbContext pooling (via AddPooledDbContextFactory) and the status-update stored
// procedures are always on: baked into the provider/migrations, not user-toggleable.
Manual Migrations:
For production environments, apply migrations manually:
# Generate migration script (run from src/Storage/EverTask.Storage.SqlServer/)
dotnet ef migrations script --context SqlServerTaskStoreContext --output migration.sql
# Apply via your deployment pipeline
sqlcmd -S localhost -d EverTaskDb -i migration.sql
Stored Procedures:
EverTask uses stored procedures for critical operations:
[EverTask].[usp_SetTaskStatus](v2.0+): status update + audit insert in one round-trip and one transaction[EverTask].[usp_UpdateCurrentRun](v3.6+): single-round-trip recurring-run update- The procs do the audit insert and status update in one round-trip instead of two statements, kept atomic. That saves a round-trip on the status-change path; it is not a task-throughput multiplier
Connection String Options:
// Basic
"Server=localhost;Database=EverTaskDb;Trusted_Connection=True;"
// With pooling (recommended)
"Server=localhost;Database=EverTaskDb;Trusted_Connection=True;Min Pool Size=5;Max Pool Size=100;"
// Azure SQL
"Server=tcp:yourserver.database.windows.net,1433;Database=EverTaskDb;User ID=user;Password=pass;Encrypt=True;"
Schema Customization:
-- Custom schema
CREATE SCHEMA [CustomSchema]
GO
-- Configure in code
opt.SchemaName = "CustomSchema";
SQLite Storage Options
Package: EverTask.Storage.Sqlite
Advanced Configuration:
.AddSqliteStorage(connectionString, opt =>
{
// Auto-apply migrations (default: true)
opt.AutoApplyMigrations = true;
// Note: SchemaName must remain "" (empty string): SQLite has no schema concept
})
Connection String Options:
// Basic
"Data Source=evertask.db"
// In-memory (for testing)
"Data Source=:memory:"
// Shared cache
"Data Source=evertask.db;Cache=Shared;"
// Full options
"Data Source=evertask.db;Mode=ReadWriteCreate;Cache=Shared;Foreign Keys=True;"
Performance Tuning:
-- WAL mode for better concurrency
PRAGMA journal_mode=WAL;
-- Optimize for performance
PRAGMA synchronous=NORMAL;
PRAGMA cache_size=10000;
PRAGMA temp_store=MEMORY;
Limitations:
- No schema support (unlike SQL Server)
- Single writer: tops out around a couple hundred tasks/sec on this hardware, and parallelism does not help
- Best for: Single-server deployments, development, small workloads
PostgreSQL Storage Options
Package: EverTask.Storage.Postgres
Advanced Configuration:
.AddPostgresStorage(connectionString, opt =>
{
// Schema name (default: "evertask"). MUST be lowercase (matches ^[a-z_][a-z0-9_]*$):
// Npgsql always double-quotes generated identifiers, so a mixed-case schema becomes
// permanently case-sensitive. null = the "public" schema.
opt.SchemaName = "evertask";
// Auto-apply migrations (default: true). Disable for DBA-controlled / staged deploys.
opt.AutoApplyMigrations = true;
})
// Note: there are only two configurable options (SchemaName, AutoApplyMigrations).
// DbContext pooling is always on. Status/run updates use single-statement data-modifying
// CTEs (the Postgres analog of SQL Server's stored procedures): versioned in C#, no DB objects.
Connection String Examples:
// Basic
"Host=localhost;Database=evertask;Username=evertask;Password=***"
// With port + SSL
"Host=db.example.com;Port=5432;Database=evertask;Username=app;Password=***;SSL Mode=Require;Trust Server Certificate=true"
Manual Migrations: same pattern as SQL Server, using --context PostgresTaskStoreContext.
Notes / limitations:
SchemaNamelowercase-only (see above).- All
DateTimeOffsetvalues map totimestamptz(UTC). - Full multi-server / high-write-concurrency support (unlike SQLite).
Handler Configuration
You can configure behavior at the handler level to override global defaults.
Handler Properties
The active handler-level settings (Timeout, RetryPolicy, QueueName, RateLimitPolicy) are virtual properties you override (expression-bodied / get-only: you don’t assign them in a constructor). The obsolete CpuBoundOperation is the exception: a plain settable, non-virtual, no-op property (don’t use it).
public class MyHandler : EverTaskHandler<MyTask>
{
// Timeout
public override TimeSpan? Timeout => TimeSpan.FromMinutes(10);
// Retry policy
public override IRetryPolicy? RetryPolicy => new LinearRetryPolicy(5, TimeSpan.FromSeconds(2));
// Queue routing
public override string? QueueName => "high-priority";
// Per-key rate limiting (v3.7+), see rate-limiting.md
public override RateLimitPolicy? RateLimitPolicy =>
new RateLimitPolicy(15, TimeSpan.FromMinutes(1));
public override async Task Handle(MyTask task, CancellationToken cancellationToken)
{
// Handler logic
}
}
Available Properties:
Timeout(TimeSpan?): Handler-specific timeout (falls back to queue, then global default)RetryPolicy(IRetryPolicy?): Handler-specific retry policy (falls back to queue, then global default)QueueName(string?): Target queue for this handler. If the name is not registered (typo, or a queue you never added viaAddQueue), routing logs a warning (Queue '{name}' not found, falling back to 'default' queue) and the task runs on thedefaultqueue; the per-queue retry/timeout resolution falls back to thedefaultqueue’s config the same way, so an unknown name never throws and never silently drops the task.RateLimitPolicy(RateLimitPolicy?): Per-key execution frequency constraint; the key comes fromIRateLimitedTaskon the task or aGetRateLimitKeyoverride on the handler (see Rate Limiting Configuration)CpuBoundOperation(bool): OBSOLETE, no effect. Deprecated; EverTask’s async execution is already non-blocking. For CPU-intensive synchronous work, useTask.RuninsideHandle.
Overridable methods:
GetRateLimitKey(TTask task): derive the rate-limit bucket key from task data (e.g.task.TenantId.ToString()) without implementingIRateLimitedTask. Default readsIRateLimitedTask.RateLimitKey.- Lifecycle callbacks:
OnStarted(Guid),OnCompleted(Guid),OnError(Guid, Exception?, string?),OnRetry(Guid, int attemptNumber, Exception, TimeSpan delay), andDisposeAsyncCore(). See Resilience › Error Observation and Retry Callbacks.
Dispatch Parameters
Every ITaskDispatcher.Dispatch(...) overload accepts these optional parameters (see Task Dispatching for full behavior):
| Parameter | Type | Default | Behavior |
|---|---|---|---|
auditLevel | AuditLevel? | null → the global SetDefaultAuditLevel (default Full) | Per-dispatch override of the audit level for this task |
taskKey | string? | null (no deduplication) | Idempotency key (≤ 200 chars, stored-column length-limited). Non-recurring: InProgress → no-op; an immediate one-shot whose delivery is already in flight → no-op (returns existing id, before any status update); Pending/Queued/WaitingQueue → update; terminal (Completed/Failed/Cancelled/ServiceStopped) → remove + recreate. Recurring: InProgress → no-op; every other status incl. Completed/Failed → update in place (a recurring row is never “terminated”/replaced), preserving NextRunUtc + CurrentRunCount only when NextRunUtc.HasValue: an exhausted series (no stored next run) is recalculated instead of preserved; a re-dispatch with no recurring config (recurring to one-shot) is discarded to avoid destroying the schedule. Essential for idempotent recurring registration across restarts |
cancellationToken | CancellationToken | default | Cancels the dispatch operation (e.g. a blocking enqueue on a full Wait queue), not the task’s execution |
The scheduling discriminator (TimeSpan delay, DateTimeOffset time, or Action<IRecurringTaskBuilder>) is a positional argument that selects the overload.
Recurring Task Builder
The Action<IRecurringTaskBuilder> overload of Dispatch configures a recurring schedule via a fluent builder (src/EverTask.Abstractions/IRecurringTaskBuilder.cs). All times are UTC. Full feature docs: Recurring Tasks.
Entry / first run:
Schedule(): pure recurring, no initial one-off run.RunNow()/RunDelayed(TimeSpan)/RunAt(DateTimeOffset)→.Then(): run once first (now / after a delay / at a time), then follow the recurring schedule.
Interval:
Every(int n)followed by.Seconds()/.Minutes()/.Hours()/.Days()/.Weeks()/.Months().EverySecond()/EveryMinute()/EveryHour()/EveryDay()/EveryWeek()/EveryMonth().OnHours(): every hour (1-hour interval; refine with.AtMinute(...)).OnDays(params DayOfWeek[]): specific weekdays;OnMonths(params int[]): specific months.
Refinement:
- Hour →
.AtMinute(0–59); minute →.AtSecond(0–59). - Day →
.AtTime(TimeOnly)or.AtTimes(params TimeOnly[]). - Week →
.OnDay(DayOfWeek)/.OnDays(params DayOfWeek[])→ then.AtTime(...). - Month →
.OnDay(1–31)/.OnDays(params int[])/.OnFirst(DayOfWeek)→ then.AtTime(...).
Cron: UseCron("expr"): 5-field (min hour dom month dow) or 6-field (with seconds), via Cronos. Overrides every other interval call; invalid expressions throw ArgumentException on the first schedule calculation.
Limits: .RunUntil(DateTimeOffset) (must be future) and .MaxRuns(int) (counts real executions only; occurrences skipped to realign after downtime do not consume the budget). Stops at whichever is reached first.
OnLast(DayOfWeek)is not implemented (onlyOnFirst). For idempotent registration across restarts, pass a stabletaskKey(see Dispatch Parameters).
Complete Examples
Basic Configuration
The simplest setup for getting started:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEverTask(opt =>
{
opt.RegisterTasksFromAssembly(typeof(Program).Assembly);
})
.AddMemoryStorage();
var app = builder.Build();
app.Run();
Production Configuration
A fuller setup with SQL Server storage, retry policies, and logging:
builder.Services.AddEverTask(opt =>
{
opt.SetChannelOptions(5000)
.SetMaxDegreeOfParallelism(Environment.ProcessorCount * 4)
.SetDefaultTimeout(TimeSpan.FromMinutes(5))
.SetDefaultRetryPolicy(new LinearRetryPolicy(3, TimeSpan.FromSeconds(1)))
.SetThrowIfUnableToPersist(true)
.RegisterTasksFromAssembly(typeof(Program).Assembly);
})
.AddSqlServerStorage(
builder.Configuration.GetConnectionString("EverTaskDb")!,
opt =>
{
opt.SchemaName = "EverTask";
opt.AutoApplyMigrations = false; // Manual migrations in production
})
.AddSerilog(opt =>
opt.ReadFrom.Configuration(
builder.Configuration,
new ConfigurationReaderOptions { SectionName = "EverTaskSerilog" }));
Multi-Queue Configuration
This setup isolates different workloads into separate queues:
builder.Services.AddEverTask(opt =>
{
opt.RegisterTasksFromAssembly(typeof(Program).Assembly);
})
.ConfigureDefaultQueue(q => q
.SetMaxDegreeOfParallelism(10)
.SetChannelCapacity(1000))
.AddQueue("critical", q => q
.SetMaxDegreeOfParallelism(20)
.SetChannelCapacity(500)
.SetFullBehavior(QueueFullBehavior.Wait)
.SetDefaultTimeout(TimeSpan.FromMinutes(2))
.SetDefaultRetryPolicy(new LinearRetryPolicy(5, TimeSpan.FromSeconds(1))))
.AddQueue("email", q => q
.SetMaxDegreeOfParallelism(10)
.SetChannelCapacity(10000)
.SetFullBehavior(QueueFullBehavior.FallbackToDefault))
.AddQueue("reports", q => q
.SetMaxDegreeOfParallelism(2)
.SetChannelCapacity(50)
.SetDefaultTimeout(TimeSpan.FromMinutes(30)))
.ConfigureRecurringQueue(q => q
.SetMaxDegreeOfParallelism(5)
.SetChannelCapacity(200))
.AddSqlServerStorage(connectionString);
High-Performance Configuration
Tuned for very large workloads:
builder.Services.AddEverTask(opt => opt
.RegisterTasksFromAssembly(typeof(Program).Assembly)
.UseShardedScheduler(shardCount: Environment.ProcessorCount)
.SetMaxDegreeOfParallelism(Environment.ProcessorCount * 4)
.SetChannelOptions(10000)
.SetDefaultTimeout(TimeSpan.FromMinutes(10))
)
.AddSqlServerStorage(connectionString, opt =>
{
opt.SchemaName = "EverTask";
opt.AutoApplyMigrations = false;
});
Multi-Assembly Configuration
When your task handlers are spread across multiple assemblies:
builder.Services.AddEverTask(opt =>
{
opt.RegisterTasksFromAssemblies(
typeof(CoreTasks.MyTask).Assembly,
typeof(ApiTasks.MyTask).Assembly,
typeof(BackgroundTasks.MyTask).Assembly)
.SetMaxDegreeOfParallelism(20);
})
.AddSqlServerStorage(connectionString);
Environment-Specific Configuration
Here the configuration changes based on your environment:
var builder = WebApplication.CreateBuilder(args);
// Storage methods extend the EverTask builder returned by AddEverTask,
// NOT IServiceCollection: keep a reference when branching by environment
var everTask = builder.Services.AddEverTask(opt =>
{
opt.RegisterTasksFromAssembly(typeof(Program).Assembly);
if (builder.Environment.IsProduction())
{
opt.SetMaxDegreeOfParallelism(Environment.ProcessorCount * 4)
.SetChannelOptions(10000)
.SetDefaultTimeout(TimeSpan.FromMinutes(10));
}
else
{
opt.SetMaxDegreeOfParallelism(2)
.SetChannelOptions(100);
}
});
if (builder.Environment.IsProduction())
{
everTask.AddSqlServerStorage(
builder.Configuration.GetConnectionString("EverTaskDb")!,
opt => opt.AutoApplyMigrations = false);
}
else
{
everTask.AddMemoryStorage();
}
Configuration Validation
What EverTask actually checks at startup:
Errors:
- No assemblies registered for handler scanning:
AddEverTaskthrowsArgumentException - Channel capacity < 1:
ArgumentOutOfRangeException(raised by the BCLBoundedChannelOptionsconstructor)
Warnings:
- Global
MaxDegreeOfParallelism == 1: a startup warning is logged (a single consumer is usually a bad idea in production); the value is honored as-is - Per-queue
MaxDegreeOfParallelism < 1: clamped to 1 consumer at startup with a warning (prevents a zero-consumer deadlock), never treated as “unlimited”. (The per-queue path clamps< 1; the global-level warning fires specifically at== 1.)
Behaviors to be aware of (no error raised):
- Re-adding a queue with an existing name silently replaces the previous configuration
SetMaxDegreeOfParallelismperforms no validation at configuration time; the worker clamps any value< 1to 1 at startup (see above)
Performance Tuning Guidelines
CPU-Bound Tasks
If your tasks do heavy computation, match your parallelism to your CPU cores:
opt.SetMaxDegreeOfParallelism(Environment.ProcessorCount) // Match CPU cores
.SetChannelOptions(100); // Small queue
I/O-Bound Tasks
If your tasks spend most of their time waiting on I/O (database, APIs, files), you can run many more in parallel:
opt.SetMaxDegreeOfParallelism(Environment.ProcessorCount * 4) // Higher parallelism
.SetChannelOptions(5000); // Larger queue
Mixed Workloads
When you have different types of tasks, use separate queues:
.ConfigureDefaultQueue(q => q
.SetMaxDegreeOfParallelism(Environment.ProcessorCount * 2))
.AddQueue("cpu-intensive", q => q
.SetMaxDegreeOfParallelism(Environment.ProcessorCount))
.AddQueue("io-intensive", q => q
.SetMaxDegreeOfParallelism(Environment.ProcessorCount * 4))
Extreme High Load
For very large workloads, enable the sharded scheduler:
opt.UseShardedScheduler(Environment.ProcessorCount)
.SetMaxDegreeOfParallelism(Environment.ProcessorCount * 4)
.SetChannelOptions(10000);
Next Steps
- Getting Started - Setup guide
- Scalability - Multi-queue and sharded scheduler
- Resilience - Retry policies and timeouts
- Storage - Storage options and configuration