Skip to content

Background jobs

Work that does not belong in a request runs on scheduled jobs. Job state is persisted in the database, so a restart does not lose scheduled work.

The dashboard is at Hangfire:DashboardPath (/hangfire by default) and is restricted to administrators who have completed two-factor sign-in.

All configured under the BackgroundJobs section. Each block has at least Enabled, Interval, and MaxItems.

Job Does Interval Batch
PermissionRebuild Materialises effective permissions from the rebuild queue. 30 s 200
ReminderDispatch Fires due document reminders. 1 min 200
EmailDelivery Drains the outbound email outbox. Holds mail while email is not configured. 1 min 100
SignatureExpiry Expires stale signature requests. 5 min 200
AntivirusBackfill Scans files still waiting for a verdict. Must stay on. 5 min 5000
FolderEmailIntake Polls a mailbox and files attachments. Disabled by default. 5 min -
UploadSessionSweep Cleans up abandoned chunked uploads. 15 min 200
WorkflowEscalation Escalates overdue workflow tasks. 15 min 200
PreviewGeneration Renders missing previews and thumbnails, and regenerates unreadable ones. 15 min 200
OcrBackfill Extracts text from documents that have none. 15 min 100
StorageEncryptionBackfill Encrypts content stored before encryption at rest was enabled. 15 min 100
StorageSidecarBackfill Protects legacy storage metadata files. 15 min 100
RetentionCleanup Purges recycle-bin items past retention. 1 h 200
OrphanByteReclamation Deletes stored files nothing references any more. 6 h 200
StorageIntegrity Verifies stored files against their recorded hashes. 12 h 500
PrivacyRetention Clears aged IP addresses and user agents, and resumes interrupted erasures. 1 day 500
StorageReadRemediation One-time upgrade repairs; becomes a no-op once complete. 2 min 500

Three more jobs run on fixed schedules with no configuration block:

Job Does
audit-seal Seals new audit rows into signed, chained ranges every 5 minutes.
audit-integrity Verifies the audit chain: recent seals hourly, the full chain daily.
workflow-assignment-reconciliation Reassigns workflow tasks and signature slots belonging to users who can no longer act on them.
Job Extra settings
UploadSessionSweep SessionLifetime (1 day) - how long an abandoned session survives.
OrphanByteReclamation MinimumAge (1 hour) - how long a blob must be unreferenced before deletion.
WorkflowEscalation EscalationWindow (2 days) - how long a task may sit before escalation.
PreviewGeneration MaxAttempts (3) - retries before giving up on a version.
EmailDelivery MaxAttempts (3), BaseBackoff (1 min), LeaseTimeout (5 min), HoldingWindow (3 days) - mail queued longer than this while email was unavailable is suppressed rather than sent late.
AntivirusBackfill TimeBudget (4 min) - must stay below the interval. VirusTotal lookups are paced by Antivirus:VirusTotalRequestsPerMinute.
PrivacyRetention ResumeErasuresOlderThan (10 min) - when an interrupted erasure is picked up again.
StorageReadRemediation TimeBudget (90 s), BatchSize (500).

The intervals are not arbitrary, and the pattern is worth understanding before changing any of them.

Fast (30 s - 1 min) for things a user is waiting on. Permission rebuild, reminders, email.

Medium (5 - 15 min) for things that should happen soon but that nobody is watching. Previews, text extraction, escalation, session cleanup. A new document without a thumbnail for a few minutes is acceptable; the alternative is doing that work inside the upload request.

Slow (1 - 12 h) for housekeeping. Retention, byte reclamation, integrity verification. These scan and are deliberately infrequent.

The configuration file itself records why PermissionRebuild runs every 30 seconds rather than every 5: access-control writes drain the rebuild queue inline, in the same transaction, so this job is a safety net for rows that outlive their request, not the primary path. At a 5-second cadence the job runner’s own bookkeeping became the dominant cost of an otherwise idle system.

That is the general lesson. A job that ticks frequently and usually finds nothing to do is not free - it costs a scheduler wake-up, a connection, and a query, on every tick, forever.

Deleting a document does not free disk space, and the reason is two jobs with different jobs to do:

  1. RetentionCleanup purges the document record once it is past retention.
  2. OrphanByteReclamation later deletes the underlying stored file, if nothing else references it and it is older than MinimumAge.

Because identical content is stored once, a stored file may still be referenced by other documents. Space is therefore freed some hours after a purge, not at it. See Governance and retention.

Every job can be turned off with its Enabled flag. A job checks the flag when each run starts, so runs already queued, retrying or interrupted do nothing once it is disabled. Some are worth knowing about:

  • FolderEmailIntake is disabled by default and needs both its own flag and EmailIntake:Enabled to be turned on.
  • Disabling PermissionRebuild is usually safe in the short term, because access-control writes drain the queue inline. It is a safety net, not the primary path - but leaving it off indefinitely means nothing catches the rows that outlive their request.

Leave these on:

  • StorageIntegrity - your only automatic detection of corrupted or missing stored files.
  • AntivirusBackfill - the only recovery path for a file whose scan never ran. Unscanned files are never served.
  • PrivacyRetention - clears network identifiers on schedule and finishes interrupted erasures.

The dashboard shows scheduled, running, succeeded, and failed jobs, with retry history. Hangfire:RetryAttempts (3 by default) controls automatic retries.

A job failing consistently is worth investigating rather than waiting out - a preview job failing every pass usually means the converter path is wrong, not that the documents are unusual.