How to Raise the Minimum Threads of the .NET Thread Pool
Up to its minimum, the .NET thread pool creates threads as soon as they're needed. Past that, it adds a couple per second. So raising the minimum helps a burst of blocking work. It stopped a real crash on my API, and I measured what it costs.
Three Ways to Set It
All three do the same thing. Microsoft's threading configuration page lists them.
In the project file, which is the one I'd use, because it travels with the code:
<PropertyGroup>
<ThreadPoolMinThreads>500</ThreadPoolMinThreads>
</PropertyGroup>
The build writes it into runtimeconfig.json, so you can check it there:
"System.Threading.ThreadPool.MinThreads": 500
As an environment variable, for trying it without a rebuild. The value is hexadecimal, so 500 is 1F4:
DOTNET_ThreadPool_ForceMinWorkerThreads=1F4 dotnet run --project src/Dewiride.Inventory.Api
In PowerShell, use $env:DOTNET_ThreadPool_ForceMinWorkerThreads = "1F4" on the line before.
In code, at the start of Program.cs:
ThreadPool.GetMinThreads(out var before, out var ioThreads);
Console.WriteLine($"Before: {before} worker threads");
ThreadPool.SetMinThreads(500, ioThreads);
ThreadPool.GetMinThreads(out var after, out _);
Console.WriteLine($"After: {after} worker threads");
Before: 10 worker threads
After: 500 worker threads
By default, the minimum is the number of processors, which is 10 on this computer.
What It Did for Me
A .NET API using MySql.Data blocks a thread while it waits for a pooled connection. With 150 requests at once and a pool of 100, the thread pool starved, and the process often ended after about 15 seconds. The same burst of 600 requests, repeated with each setting:
| Setting | Runs | Result |
|---|---|---|
| The default | 26 | The process ended in 17, and 4 more gave errors. Only 5 were clean |
DOTNET_ThreadPool_ForceMinWorkerThreads=1F4 | 8 | All 600 answered 200 every time, in about 2.4 s |
ThreadPoolMinThreads of 500 | 3 | All 600 answered 200 every time, in about 2.5 s |

It held at 800 at once as well: all 3,200 answered.
What It Costs
Each blocked thread is a real thread. I compared it with a concurrency limit in front of the pool, which queues extra requests without holding any thread, at 500 requests at once and a pool of 25:
| Time for 2,000 requests | Threads in the process afterwards | |
|---|---|---|
| A minimum of 500 threads | 6.2 s | 386 to 425 |
| A concurrency limit | 6.0 s | 38 to 39 |
Same speed, with ten times the threads. It also doesn't limit the wait: past the pool, a request can still block for up to Connect Timeout, and then fail. So it's a real fix for a crash, and it's the quickest one to try. But where you can keep requests from blocking in the first place, do that. A higher minimum is a good second layer if some blocking is out of your hands.
Choosing a Number
Set it to about the most requests you expect to block at the same moment, and no higher. Microsoft's documentation warns: "Using the SetMinThreads method to increase the minimum number of threads can cause performance problems as described in the preceding text. In most cases, the thread pool will perform better with its own algorithm for allocating threads."
Common Mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
Writing 500 in the environment variable | It's read as hexadecimal: 1,280 threads | Write 1F4 |
| Raising it instead of finding what blocks | Memory goes up, and the blocking stays | Find it first |
| Setting it huge "to be safe" | Threads compete for CPU time | Size it to your load |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No change | The setting isn't in runtimeconfig.json | Rebuild, and check the file |
| Memory went up | One thread's stack per thread | Lower it |
| Still slow | The blocking is longer than the burst | Limit requests |
Conclusion
Set ThreadPoolMinThreads in the project file, and threads are there when a burst arrives. It stopped a crash here, at the cost of hundreds of threads, so prefer not blocking if you can.
Additional Resources
- Runtime configuration options for threading — the settings.
- ThreadPool.SetMinThreads — the method, and its caution.
- Debug ThreadPool starvation — finding the cause.
