Skip to main content
Version: Next

17. Performance

17.1 Execution model

Every job execution rides entirely on Hangfire's own worker thread pool and async/await — the framework introduces no additional threading, no custom scheduler loop, and no execution concurrency model of its own. Overhead added per execution is limited to: constructing one BackgroundJobExecutionContext, one job-specific adapter, one BackgroundJobExecutor, and invoking the pipeline's thin RunAsync (a cancellation check, an await of the caller-supplied delegate, and exception classification only on the failure path). See §4 — Runtime Pipeline for exactly what that call chain does.

17.2 Allocation strategy

Nothing in the current design pools or caches objects across executions — this is a deliberate simplicity choice, not an oversight requiring one:

  • BackgroundJobExecutionContext — a new instance per execution, immutable, no unmanaged resources, garbage collected normally once the call chain returns.
  • The job-specific IBackgroundJob adapter and BackgroundJobExecutor — both constructed with new, per execution, by the bridge; neither is DI-registered, so there is no container overhead for either.
  • BackgroundJobExecutionLifetime — one lightweight timing struct-like object per pipeline call; its Elapsed value is currently computed but has no consumer (see AP-01).
  • The one DI-resolved, Scoped service (IBackgroundJobTenantContext) is resolved once per job's DI scope, not re-created per call within that scope.

For the recurring, high-frequency jobs already in production (campaign-workflow-executor-pipeline and notification-retry-processor-pipeline both run every 2 minutes; campaign-scheduler-pipeline runs every minute), this means each tick allocates a small, fixed number of short-lived objects — no unbounded growth, no per-tick accumulation of state.

17.3 Dependency graph efficiency

Every service is resolved via standard constructor-injection DI — there is no runtime reflection in the hot path of an execution (reflection is used only in the framework's own test suite, for DI-graph verification tests like ConstructorCoverageTests, never at runtime). The dependency graph itself is shallow and acyclic by design and by test (DependencyGraphTests.DependencyGraph_HasNoCircularDependencies), so container resolution cost per execution is proportional to the small, fixed number of services each job's bridge actually constructor-injects — not to the size of the framework's overall public surface.

17.4 Runtime efficiency — what's cheap and what to be deliberate about

Cheap, by design:

  • Jobs that are naturally idempotent at the set level (dead-letter cleanup, retry expire, campaign cleanup) add no locking or claiming overhead at all.
  • The distributed lock, when used, is a single round-trip to Hangfire's own storage-backed AcquireDistributedLock call — no polling loop, no custom retry-on-contention logic inside the framework itself.

Where the real cost lives — deliberately not in this framework's layer: for the two jobs with real per-row concurrency concerns (notification-retry-processor, campaign-workflow-executor), the actual contention-handling cost is an atomic, indexed SQL UPDATE ... WHERE Status = Pending AND WorkerToken IS NULL inside the Notification Framework — not a framework-level lock. This is intentional: per-row database claiming scales far better under concurrent execution than a coarse job-level lock would, since only rows actually being contested pay any cost at all. See §9.4.

17.5 What is not yet measured

Unlike the Notification Framework (which has a dedicated Shumoul.Notification.Tests.Performance project using BenchmarkDotNet), no formal benchmark suite exists for the Background Jobs Framework today. No throughput, latency, or allocation numbers are published for this framework's runtime — anything beyond the structural facts in this chapter would be invented rather than measured, and is deliberately left out per this guide's source-of-truth requirement. If performance benchmarking is added in the future, following the Notification Framework's Tests.Performance project as the template would keep the two Golden Reference modules consistent in this respect too.