Skip to main content

ADR-008: Selective Use of Task.Run for Background Operations

Status: Proposed
Date: 2026-01-27
Context: WepNG Performance & Responsiveness


Decision

We permit the selective use of Task.Run for offloading non-critical, side-effect-heavy operations from the main request thread in the legacy .NET Framework 4.8 monolith.

However, for critical transactional workflows (e.g., payment processing, final order confirmation), Task.Run is considered an intermediate "quick win" and must be replaced by a robust Persistent Task Queue (e.g., Hangfire, RabbitMQ, or Azure Service Bus) in future phases.

Context

The WepNG_MVC platform faces several performance bottlenecks:

  • Synchronous Service Layer: Many business services are purely synchronous and perform heavy I/O or external API calls.
  • Legacy Framework Constraints: Complete transition to async/await would require deep refactoring across hundreds of files ("Async all the way down").
  • User Experience: Users experience long wait times for operations that could be processed in the background (e.g., data feeds, log generation).

Considered Options

Option A: Strictly Synchronous (Traditional)

  • Pros: Thread-safe, predictable error handling, zero risk of lost work during AppPool restarts.
  • Cons: Blocks IIS threads, leads to timeouts, poor user perception of speed.

Option B: Universal Task.Run (Extreme)

  • Pros: Maximum UI responsiveness.
  • Cons: High risk of thread pool starvation, unhandled exceptions crashing processes, "fire-and-forget" tasks lost if the application resets.

Option C: Selective Task.Run with Path to Queues (Balanced)

  • Pros:
    • Immediate latency reduction for users.
    • No infrastructure change required today.
    • Acknowledges technical debt while providing a migration path.
  • Cons:
    • Inherits Task.Run risks (starvation if not throttled).
    • Requires developer discipline to distinguish critical vs. non-critical tasks.

Rationale

The "Pros" of immediate responsiveness in the current legacy context outweigh the "Cons" of using a non-persistent background thread, provided the task is not critical for data integrity.

Using Task.Run allows us to "simulate" a decoupled architecture while we prepare the infrastructure for a formal queue-based system. It serves as a tactical bridge towards Pillar 2 (Performance) of the Evolution Plan.

Constraints & Rules of Engagement

  1. Non-Critical Only: Use Task.Run only for tasks where a failure to complete (due to an AppPool reset) does not lead to data corruption or financial loss.
  2. Error Handling: Every Task.Run block MUST have a try-catch to prevent unhandled exceptions from affecting the process.
  3. Throttling: Avoid using Task.Run in high-frequency loops (the "Loop of Death") to prevent thread pool exhaustion.
  4. Visibility: Log the start and end of background tasks for traceability.

Consequences

  1. Responsiveness Improvement: UI actions will return faster.
  2. Technical Debt: Adds implicit dependency on the application's process lifetime.
  3. Future Migration: Any task moved to Task.Run should be tagged for eventual migration to a formal queue if it becomes business-critical.

References