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/awaitwould 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.Runrisks (starvation if not throttled). - Requires developer discipline to distinguish critical vs. non-critical tasks.
- Inherits
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
- Non-Critical Only: Use
Task.Runonly for tasks where a failure to complete (due to an AppPool reset) does not lead to data corruption or financial loss. - Error Handling: Every
Task.Runblock MUST have a try-catch to prevent unhandled exceptions from affecting the process. - Throttling: Avoid using
Task.Runin high-frequency loops (the "Loop of Death") to prevent thread pool exhaustion. - Visibility: Log the start and end of background tasks for traceability.
Consequences
- Responsiveness Improvement: UI actions will return faster.
- Technical Debt: Adds implicit dependency on the application's process lifetime.
- Future Migration: Any task moved to
Task.Runshould be tagged for eventual migration to a formal queue if it becomes business-critical.