Audit Configuration
EverTask provides configurable audit trail levels to control database bloat from high-frequency tasks. By default, every task execution creates audit records in StatusAudit and RunsAudit tables. For tasks running every few minutes, this can generate thousands of records per day.
Audit Levels
Control audit trail verbosity with the AuditLevel enum:
| Level | StatusAudit | RunsAudit | Use Case |
|---|---|---|---|
| Full (default) | All status transitions | All executions | Critical tasks requiring complete history |
| Minimal | Errors only | All executions | High-frequency recurring tasks (tracks last run + errors) |
| ErrorsOnly | Errors only | Errors only | Tasks where only failures matter |
| None | Never | Never | Extremely high-frequency tasks, no audit needed |
Database Impact
Audit rows are synchronous database writes on the task execution path, so the audit level directly changes how much work the storage layer does per task. What each level writes (from AuditPolicy):
| Level | StatusAudit rows | RunsAudit rows (recurring) | On a clean success run |
|---|---|---|---|
| Full | one per status transition | one per run | the most rows |
| Minimal | only on real errors | one per run | one run-history row |
| ErrorsOnly | only on real errors | only on errors | no audit rows |
| None | never | never | no audit rows |
Total audit rows ≈ executions × records-per-execution, so the cost scales with how often your tasks run. Example: 100 recurring tasks every 5 minutes = 100 × (1440 / 5) = 28,800 runs/day. At Full, each run writes one RunsAudit row plus a StatusAudit row per transition; at ErrorsOnly, a clean day writes essentially nothing (only failures record). The exact ratio between levels depends on your success/failure mix and how many status transitions each run goes through.
Higher audit means more writes, which means lower task throughput. That is expected, not a bug: every audit row is an extra synchronous DB write, and task execution is storage-bound. The same goes for the persistent proxy logger (
WithPersistentLogger): each captured log line is one moreTaskExecutionLogwrite per execution. Turn audit down (or the persistent logger off) on high-frequency tasks where you don’t need the trail.
Global Default Configuration
Set the default audit level for all tasks:
builder.Services.AddEverTask(opt => opt
.RegisterTasksFromAssembly(typeof(Program).Assembly)
.SetDefaultAuditLevel(AuditLevel.Minimal)) // Default: AuditLevel.Full
.AddSqlServerStorage(connectionString);
Per-Task Override
Override audit level when dispatching individual tasks:
// High-frequency health check - minimal audit
await dispatcher.Dispatch(
new HealthCheckTask(),
recurring => recurring.Every(5).Minutes(),
auditLevel: AuditLevel.Minimal);
// Critical payment processing - full audit
await dispatcher.Dispatch(
new ProcessPaymentTask(orderId),
auditLevel: AuditLevel.Full);
// Background cleanup - no audit needed
await dispatcher.Dispatch(
new CleanupTempFilesTask(),
recurring => recurring.EveryDay().AtTime(new TimeOnly(2, 0)),
auditLevel: AuditLevel.None);
All Dispatch() overloads support the optional auditLevel parameter:
// Immediate execution
Task<Guid> Dispatch(IEverTask task, AuditLevel? auditLevel = null, ...);
// Delayed execution
Task<Guid> Dispatch(IEverTask task, TimeSpan delay, AuditLevel? auditLevel = null, ...);
// Scheduled execution
Task<Guid> Dispatch(IEverTask task, DateTimeOffset scheduleTime, AuditLevel? auditLevel = null, ...);
// Recurring execution
Task<Guid> Dispatch(IEverTask task, Action<IRecurringTaskBuilder> recurring,
AuditLevel? auditLevel = null, string? taskKey = null, ...);
Audit Level Behavior
Full (Default)
Complete audit trail for debugging and compliance:
- StatusAudit: Records all status transitions (Queued → InProgress → Completed/Failed)
- RunsAudit: Records every execution with timestamp, duration, and result
- Use When: Critical business tasks, compliance requirements, production debugging
// Critical payment processing - keep full history
await dispatcher.Dispatch(
new ProcessPaymentTask(orderId),
auditLevel: AuditLevel.Full);
Minimal
Optimized for high-frequency recurring tasks:
- StatusAudit: Only errors (failed executions, service stopped)
- RunsAudit: All executions (tracks last run timestamp)
- QueuedTask.LastExecutionUtc: Updated on every execution
- Use When: Recurring health checks, periodic data sync, monitoring tasks
// Health check every 5 minutes - track last run, only audit errors
await dispatcher.Dispatch(
new HealthCheckTask(),
recurring => recurring.Every(5).Minutes(),
auditLevel: AuditLevel.Minimal,
taskKey: "health-check");
Write cost: drops the per-transition StatusAudit rows on success (keeps one RunsAudit row per run), so a clean run writes far fewer rows than Full.
ErrorsOnly
Only track failures:
- StatusAudit: Only errors (failed executions, service stopped)
- RunsAudit: Only errors (no success records)
- QueuedTask Status: Updated to Completed on success (no audit)
- Use When: Fire-and-forget tasks, background cleanup, non-critical operations
// Cleanup task - only care about failures
await dispatcher.Dispatch(
new CleanupOldFilesTask(),
recurring => recurring.EveryDay().AtTime(new TimeOnly(3, 0)),
auditLevel: AuditLevel.ErrorsOnly,
taskKey: "cleanup-old-files");
Write cost: on a clean success run, no audit rows at all; only failures write a StatusAudit + RunsAudit row.
None
No audit trail (use with caution):
- StatusAudit: Never created
- RunsAudit: Never created
- QueuedTask: Only the task status and exception fields updated
- Use When: Extremely high-frequency tasks (every few seconds), temporary testing tasks
// Cache refresh every 10 seconds - no audit needed
await dispatcher.Dispatch(
new RefreshCacheTask(),
recurring => recurring.Every(10).Seconds(),
auditLevel: AuditLevel.None,
taskKey: "cache-refresh");
Warning: No historical data available for debugging. Use only when audit data provides no value.
Real-World Configuration Example
builder.Services.AddEverTask(opt => opt
.RegisterTasksFromAssembly(typeof(Program).Assembly)
// Set conservative global default
.SetDefaultAuditLevel(AuditLevel.Full))
.AddSqlServerStorage(connectionString);
// Critical business tasks use global default (Full)
await dispatcher.Dispatch(new ProcessPaymentTask(orderId));
// High-frequency health checks - minimal audit
await dispatcher.Dispatch(
new HealthCheckTask(),
recurring => recurring.Every(5).Minutes(),
auditLevel: AuditLevel.Minimal,
taskKey: "health-check");
// Background email queue processing - errors only
await dispatcher.Dispatch(
new ProcessEmailQueueTask(),
recurring => recurring.Every(1).Minutes(),
auditLevel: AuditLevel.ErrorsOnly,
taskKey: "email-queue");
// Temporary cache warming task - no audit
await dispatcher.Dispatch(
new WarmCacheTask(),
recurring => recurring.Every(30).Seconds(),
auditLevel: AuditLevel.None,
taskKey: "cache-warmer");
Performance Optimization Details
EverTask eliminates unnecessary database queries by passing AuditLevel through the execution pipeline:
- No SELECT Queries: Audit level passed as parameter to storage methods (not queried from database)
- SQL Server Stored Procedure:
usp_SetTaskStatusconditionally creates audit records in T-SQL - Single Roundtrip: Status update + conditional audit insert in one database call
- 50% Fewer Queries: Reduced from 2 queries (SELECT + UPDATE/INSERT) to 1 (UPDATE/INSERT)
SQL Server Example (simplified):
CREATE PROCEDURE [EverTask].[usp_SetTaskStatus]
@TaskId uniqueidentifier,
@Status nvarchar(15), -- status stored by name, e.g. 'Completed', 'Failed'
@Exception nvarchar(max) = NULL,
@AuditLevel int = 0 -- 0 Full, 1 Minimal, 2 ErrorsOnly, 3 None
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Now datetimeoffset = SYSDATETIMEOFFSET();
-- Update task status
UPDATE [EverTask].[QueuedTasks]
SET Status = @Status, Exception = @Exception
WHERE Id = @TaskId;
-- Conditionally insert an audit record based on AuditLevel.
-- Minimal and ErrorsOnly only audit real errors: a Failed/ServiceStopped status or any exception.
IF (@AuditLevel = 0 -- Full
OR (@AuditLevel IN (1, 2) AND (@Status IN ('Failed', 'ServiceStopped') OR @Exception IS NOT NULL)))
BEGIN
INSERT INTO [EverTask].[StatusAudit] (QueuedTaskId, UpdatedAtUtc, NewStatus, Exception)
VALUES (@TaskId, @Now, @Status, @Exception);
END
END
Migration Notes
- Backward Compatible: Null
AuditLevelin database treated asFull(default) - Existing Tasks: Tasks created before v1.7 continue with Full audit level
- No Data Loss: Changing audit level only affects future executions
- Custom Storage: Implementations must accept
AuditLevelparameter inSetStatus()andUpdateCurrentRun()
Recommendations by Task Type
| Task Type | Recommended Audit Level | Reason |
|---|---|---|
| Payment processing | Full | Compliance, dispute resolution |
| Order fulfillment | Full | Business-critical, customer service |
| Email sending | ErrorsOnly | Only care about delivery failures |
| Health checks (5-10 min) | Minimal | Track last run, audit errors |
| Cache refresh (< 1 min) | None or ErrorsOnly | High-frequency, low value |
| Data sync (hourly) | Minimal | Track sync status, audit errors |
| Cleanup tasks | ErrorsOnly | Only need failure alerts |
| Report generation | Full | Audit trail for generated reports |
| Background indexing | Minimal | Track progress, audit errors |
Monitoring Audit Growth
Query audit table sizes to determine if audit levels need adjustment:
-- Check audit table row counts
SELECT
'StatusAudit' AS TableName,
COUNT(*) AS TotalRows,
COUNT(*) / NULLIF(DATEDIFF(DAY, MIN(UpdatedAtUtc), MAX(UpdatedAtUtc)), 0) AS AvgRowsPerDay
FROM [EverTask].[StatusAudit]
UNION ALL
SELECT
'RunsAudit' AS TableName,
COUNT(*) AS TotalRows,
COUNT(*) / NULLIF(DATEDIFF(DAY, MIN(ExecutedAt), MAX(ExecutedAt)), 0) AS AvgRowsPerDay
FROM [EverTask].[RunsAudit];
If audit tables grow too quickly (> 10,000 rows/day), consider:
- Reducing audit level for high-frequency tasks
- Implementing audit retention policies (see Best Practices)
- Archiving historical audit data to separate tables/database
Next Steps
- Best Practices - Storage optimization and cleanup strategies
- SQL Server Storage - Stored procedure optimization
- Monitoring - Track task execution and audit growth