Best Practice LongRunning Task creation
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
In .NET, a "long-running task" usually means work that should not compete with short request-handling tasks on the normal thread-pool path. The right design is less about forcing work into a task and more about deciding whether the work is CPU-bound, I/O-bound, recurring, or service-like.
The best practice is to choose a lifetime model first, then pick the task API. In many cases, async methods or hosted services are better than TaskCreationOptions.LongRunning.
Know When LongRunning Is Appropriate
TaskCreationOptions.LongRunning is a hint to the scheduler that the work may deserve its own dedicated thread. That can be useful for a worker loop that runs for a long time and does real synchronous work.
It is not a general performance switch. If the task mostly waits on I/O by using await, asking for LongRunning usually adds complexity without benefit.
This console example shows a dedicated worker that runs until cancellation:
That pattern is reasonable when the loop is intentionally long-lived and does blocking work. It is a poor fit for ordinary request work or short jobs.
Prefer async for I/O-Bound Operations
If the task spends most of its time waiting on a database, HTTP call, file operation, or timer, write it as an async method instead of forcing a dedicated thread.
This version scales better because the thread is returned to the runtime while the operation is awaiting external work. That is the key distinction: asynchronous waiting is not the same thing as a long-running CPU worker.
For Application Services, Prefer a Hosted Worker
Inside ASP.NET Core or a worker service, long-lived background logic usually belongs in BackgroundService instead of an ad hoc fire-and-forget task. A hosted service has explicit startup, shutdown, logging, and dependency injection boundaries.
This gives the runtime a clean way to stop the worker during application shutdown. It also avoids a common anti-pattern where code starts a task and then loses track of it.
Design for Cancellation and Observation
Whatever creation model you choose, treat cancellation and error observation as first-class concerns. Long-running work should have a CancellationToken, and exceptions should be awaited, logged, or otherwise observed.
If the task produces items for later processing, prefer a queue such as Channel rather than spinning up a new long-running task for each unit of work. One controlled worker is easier to reason about than many unmanaged ones.
Common Pitfalls
The most common mistake is using TaskCreationOptions.LongRunning for work that is actually asynchronous and mostly idle. That can waste threads and reduce scalability.
Another problem is creating fire-and-forget tasks inside web requests. If the request ends, the task may outlive the request scope, lose dependencies, or be terminated during app shutdown.
It is also easy to forget cancellation. A worker without a stop path becomes hard to shut down cleanly in tests, containers, and production.
Finally, developers sometimes assume "long-running" means "faster." It does not. It is only a scheduler hint, and the wrong hint can make the system worse.
Summary
- Use
LongRunningonly for truly long-lived synchronous work. - Prefer
asyncmethods for I/O-bound operations. - In application code, hosted services are usually better than raw background tasks.
- Always wire in cancellation and observe exceptions.
- Do not create unmanaged fire-and-forget work unless you also own its lifetime.

