Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

485
Views
Memory leak when doing fire-and-forget of Task.Delay with CancellationToken

In my application I need to schedule executing of some Actions with delay. It's like setTimeout in JavaScript. Also, when app execution ends I need to cancel all scheduled executions that have not been executed yet. So I have to call Task.Delay without await and pass CancellationToken into it. But if I do so, I face with memory leak: none of CancellationTokenSource+CallbackNode will be disposed until I call Cancel and Dispose of CancellationTokenSource from which I take CancellationTokens to pass to Task.Delay.

Minimal reproducible example:

CancellationTokenSource cts = new CancellationTokenSource();
for (int i = 0; i < 1000; i++)
{
    Task.Delay(500, cts.Token).ContinueWith(_ => Console.WriteLine("Scheduled action"));
}
await Task.Delay(1000);
Console.ReadLine();

After executing this example, it leave 1000 of CancellationTokenSource+CallbackNode. with leak

If I write cts.Cancel() after await Task.Delay(1000); leak does not appear without leak

Why this leak happens? All Tasks was completed, so there should not be references to cts.Token. Disposing passed Task to continuation action does not help.
Also, if I await Task that schedule execution of action, leak does not appear.

over 4 years ago · Santiago Trujillo
1 answers
Answer question

0

I was a bit surprised, but it looks like this is on purpose. When a registration is cancelled, CancellationTokenSource still keeps the instance of CancellationTokenSource+CallbackNode in a free-list to reuse it for the next callback. This is an opiniated optimization, that can backfire in your scenario.

To illustrate this, try running your test twice in a row:

var tasks = new Task[1000];

for (int i = 0; i < 1000; i++)
{
    tasks[i] = Task.Delay(500, cts.Token).ContinueWith(_ => Console.WriteLine("Scheduled action"));
}
await Task.WhenAll(tasks);

for (int i = 0; i < 1000; i++)
{
    tasks[i] = Task.Delay(500, cts.Token).ContinueWith(_ => Console.WriteLine("Scheduled action"));
}
await Task.WhenAll(tasks);

Console.ReadLine()

You will see that there are still 1000 instances of CancellationTokenSource+CallbackNode, despite registering 2000 callbacks in total. That's because the second iteration reused the nodes created during the first one.

Not much you can do about this, I believe this is by design. In any case, the amount of memory should be mostly negligible (at most x instances, where x is the number of simultaneously registered callbacks).

over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!