Thread.Sleep vs Task.Delay in C#: Which to Use and Why
Thread.Sleep and Task.Delay both make code wait, but they wait in completely different ways. Thread.Sleep holds on to the thread and does nothing with it. await Task.Delay gives the thread back and resumes later. In a web API or any server that handles many requests at once, that difference decides whether you run out of threads under load.
What each one does
- Thread.Sleep(1000) blocks the current thread for one second. Nothing else can run on it.
- await Task.Delay(1000) schedules a timer and returns the thread to the pool. When the timer fires, the method continues, often on a different thread.
- Task.Delay without await does nothing useful: it creates a task that nobody waits for.
Why Thread.Sleep hurts servers
ASP.NET Core serves requests from the thread pool. Each request that calls Thread.Sleep pins a pool thread for the whole wait. With enough concurrent requests the pool runs dry, new requests queue up, and latency climbs even though the CPU is idle. This is called thread-pool starvation.
await Task.Delay uses no thread while it waits, so the same server can handle thousands of waiting requests.
Before: blocking retry loop
public async Task<Invoice> GetInvoiceAsync(string id)
{
for (var attempt = 1; attempt <= 3; attempt++)
{
try { return await _client.GetInvoiceAsync(id); }
catch (HttpRequestException) when (attempt < 3)
{
Thread.Sleep(500 * attempt);
}
}
throw new InvalidOperationException("unreachable");
}
After: non-blocking, cancellable delay
public async Task<Invoice> GetInvoiceAsync(string id, CancellationToken ct)
{
for (var attempt = 1; ; attempt++)
{
try { return await _client.GetInvoiceAsync(id, ct); }
catch (HttpRequestException) when (attempt < 3)
{
await Task.Delay(TimeSpan.FromMilliseconds(500 * attempt), ct);
}
}
}
Better tools for common cases
- Retries: use a resilience library such as Polly or Microsoft.Extensions.Http.Resilience instead of hand-written loops
- Polling every N seconds: use PeriodicTimer in a BackgroundService (await timer.WaitForNextTickAsync(ct))
- Tests: avoid fixed sleeps; wait for the condition you expect, or inject a TimeProvider and advance time
When Thread.Sleep is fine
In a console tool or a dedicated thread that you own, Thread.Sleep is harmless because nobody else needs that thread. The problem is sleeping on shared threads: request handlers, async methods, UI threads, and thread-pool work items.
Find Thread.Sleep in your codebase
CodeInspector flags Thread.Sleep as rule CI0005 ("Avoid Thread.Sleep") with the file and line, so you can review each one. Scan a GitHub repository at https://codeinspector.app with the 100 free credits new accounts get.