Task.Delay fails if you wait longer than 49.7 days

12/28/2025
2 minute read

Task.Delay fails if you wait longer than 49.7 days. So something like: await Task.Delay(TimeSpan.FromDays(50)); will fail. But why and should you care?

The Why

If you try to execute this code:

using System;
using System.Threading.Tasks;

await Task.Delay(TimeSpan.FromDays(50));

If you want to try: sharplab.io

You will be greeted by:

System.ArgumentOutOfRangeException: The value needs to translate in milliseconds to -1 (signifying an infinite timeout), 0, or a positive integer less than or equal to the maximum allowed timer duration. (Parameter 'delay')

Using TimeSpan.FromDays(49) works.

Hmm, but don't we have a positive amount? Well yes, but internally Task.Delay will use uint as its base unit. Therefore the max value is uint.MaxValue - 1 which is "4294967294U". That is in milliseconds, so: 4294967294 ms / 1000 (to seconds) / 60 (to minutes) / 60 (to hours) / 24 (to days) = 49.7103 Days.

Task.Delay internally also checks that:

internal static uint ValidateTimeout(TimeSpan timeout, ExceptionArgument argument)
{
	long totalMilliseconds = (long) timeout.TotalMilliseconds;
	if (totalMilliseconds < -1L || totalMilliseconds > 4294967294L)
		ThrowHelper.ThrowArgumentOutOfRangeException(argument, ExceptionResource.Task_InvalidTimerTimeSpan);
	return (uint) totalMilliseconds;
}

This method is called when you call Task.Delay.

Should I care?

Probably: Not. Only if you write some background logic that has to delay work far far in the future. I did come across that, when I wrote my own Job-Scheduler: NCronJob. Basically, the user can enter any CRON notation and it should work. So if you want to have a job executed only once a year, of course that has to work. So what can we do? Well, I wrote a helper for that:

/// <summary>
/// Represents extensions for the <see cref="Task"/> object.
/// </summary>
internal static class TaskExtensions
{
    /// <summary>
    /// Has the same semantics as <see cref="Task.Delay(TimeSpan, TimeProvider, CancellationToken)"/> but is safe to use for long delays.
    /// </summary>
    /// <param name="targetTimeSpan">The <see cref="TimeSpan" /> to wait before completing the returned task, or <see cref="Timeout.InfiniteTimeSpan" /> to wait indefinitely.</param>
    /// <param name="timeProvider">The <see cref="TimeProvider" /> with which to interpret <paramref name="targetTimeSpan" />.</param>
    /// <param name="token">A cancellation token to observe while waiting for the task to complete.</param>
    public static async Task LongDelaySafe(TimeSpan targetTimeSpan, TimeProvider timeProvider, CancellationToken token)
    {
        ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(targetTimeSpan, TimeSpan.Zero);

        var remainingTime = targetTimeSpan;
        while (remainingTime > TimeSpan.Zero)
        {
            var delay = TimeSpan.FromMilliseconds(Math.Min(remainingTime.TotalMilliseconds, uint.MaxValue - 1));
            await Task.Delay(delay, timeProvider, token).ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
            remainingTime -= delay;
        }
    }
}

So basically, we divide and conquer at the uint.MaxValue boundary and await those multiple times until we are at the specific time given by the user.

An error has occurred. This application may no longer respond until reloaded.Reload x