async and await: Sending 10 Fee Reminders at Once – C# | Lesson 53
Introduction
Most of what an application does is wait: for the database to answer, for a file to be read, for a message provider to accept a text. While a program waits, it is doing nothing useful, yet it can be holding on to resources other work needs.
Asynchronous code lets a program start a slow operation, step away while it runs, and
continue when the result arrives. In C#, two keywords make this read almost like normal code:
async and await.
By the end of this lesson, you will be able to:
- Explain what asynchronous code is for, and what it is not for.
- Write methods that return
TaskandTask<T>, usingasyncandawait. - Run independent operations together with
Task.WhenAll. - Cancel slow work with a
CancellationToken. - Handle exceptions from tasks, and avoid
async voidand blocking calls.
Why this matters
The Web API in Modules 10 to 12 is asynchronous from top to bottom. Each request reads the
database with await, and while it waits, the server uses the same thread to start other
requests. That is how a modest server handles hundreds of parents at 8:45 in the morning.
Asynchronous code also changes how long tasks feel. Sending 10 fee reminders one after another took about 2.6 seconds in this lesson; sending them together took about 0.26 seconds. For 10,000 students, that is the difference between minutes and seconds.
The concept
Example: waiting without blocking
Think of the school office sending ten fee letters by courier. The slow way: hand over one letter, wait at the door until the courier returns, then hand over the next.
The sensible way: hand all ten to the courier, go back to other work, and check the delivery receipts later. The letters travel at the same speed either way; the difference is whether the clerk stands idle while they travel. Asynchronous code is the sensible clerk.
Example: Task and Task<T>
A task represents work that will finish later. It is a promise of a result:
| Type | Means | Example |
|---|---|---|
Task | Work that finishes with no result | Writing a log file |
Task<string> | Work that finishes with a string | Sending a message, returning a status |
Task<string[]> | Work that finishes with an array | Task.WhenAll of many Task<string> |
A method marked async can use await inside, and returns a Task or Task<T>. By
convention, its name ends with Async, as in SendAsync and File.ReadAllLinesAsync.
Example: what await does
await means "if this task is not finished yet, pause this method here, give the thread back,
and continue from this exact line when the task completes".
The method looks sequential, line after line, but no thread sits blocked during the wait. The compiler turns the method into a small state machine that remembers where to continue. You write simple code; the compiler writes the complicated part.
Example: I/O-bound versus CPU-bound
| Kind of work | Example | Right tool |
|---|---|---|
| I/O-bound: waiting for something outside | Database query, web request, file read | async and await |
| CPU-bound: the processor is busy calculating | Attendance percentages for 600,000 rows | Parallel work on several cores (Lesson 54) |
async does not make calculations faster. It makes waiting cheaper. The two ideas are often
confused in interviews, so keep this table in mind.
Apply it to the SMS
Step 1: a gateway that takes time
Real text messages go through a provider's web service over the network. Here, a stand-in waits
250 milliseconds per message with Task.Delay, an asynchronous pause that holds no thread:
// Lesson 53: a stand-in for a text-message provider. A real provider is called over the network,
// so each message takes time; Task.Delay pretends to be that wait without blocking a thread.
class TextMessageGateway
{
public int Sent { get; private set; }
public async Task<string> SendAsync(string phone, string text, CancellationToken cancellationToken = default)
{
if (phone.Length != 10)
{
throw new ArgumentException($"Invalid phone number: {phone}", nameof(phone));
}
await Task.Delay(250, cancellationToken); // the "network" wait
Sent++;
return $"sent to {phone}";
}
}
Step 2: the program
The reminders go to the ten families who have a balance due in SchoolDb: Diya's family owes 25,500, four families 45,500, and five families 39,500.
Show src/SchoolApp.Console/Program.cs
// Lesson 53: async and await. Fee reminders for the 10 families with a balance due in SchoolDb.
using System.Diagnostics;
// 1. Real asynchronous I/O: the thread is free while the disk works.
string path = Path.Combine(AppContext.BaseDirectory, "data", "class10-marks.csv");
string[] lines = await File.ReadAllLinesAsync(path);
Console.WriteLine($"Read {lines.Length - 1} marks asynchronously");
Reminder[] reminders =
[
new("Suresh Patel", "9876540103", "Diya", 25500m),
new("Harpreet Singh", "9876540105", "Kabir", 45500m),
new("Venkat Iyer", "9876540107", "Meera", 45500m),
new("Amit Das", "9876540109", "Rohan", 45500m),
new("Imtiaz Sheikh", "9876540111", "Sana", 45500m),
new("Ravi Reddy", "9876540113", "Ananya", 39500m),
new("Karan Kapoor", "9876540115", "Vihaan", 39500m),
new("Faisal Ali", "9876540117", "Zara", 39500m),
new("Sanjay Mishra", "9876540119", "Arnav", 39500m),
new("Joseph Fernandes", "9876540121", "Kiara", 39500m),
];
// 2. One after another: each await waits for the previous message.
TextMessageGateway gateway = new TextMessageGateway();
Stopwatch timer = Stopwatch.StartNew();
foreach (Reminder r in reminders)
{
await gateway.SendAsync(r.Phone, r.Text);
}
Console.WriteLine($"One by one: {gateway.Sent} messages in {timer.ElapsedMilliseconds} ms");
// 3. All at once: start every send, then await them together.
gateway = new TextMessageGateway();
timer.Restart();
Task<string>[] sends = reminders.Select(r => gateway.SendAsync(r.Phone, r.Text)).ToArray();
string[] results = await Task.WhenAll(sends);
Console.WriteLine($"Together: {results.Length} messages in {timer.ElapsedMilliseconds} ms; first: {results[0]}");
// 4. Cancellation: give up after 600 ms.
gateway = new TextMessageGateway();
using CancellationTokenSource timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(600));
try
{
foreach (Reminder r in reminders)
{
await gateway.SendAsync(r.Phone, r.Text, timeout.Token);
}
}
catch (OperationCanceledException)
{
Console.WriteLine($"Cancelled after {gateway.Sent} messages");
}
// 5. One failing task: await Task.WhenAll throws, and the other tasks still finish.
gateway = new TextMessageGateway();
Task<string>[] mixed =
[
gateway.SendAsync("9876540103", "Test 1"),
gateway.SendAsync("98765", "Test 2"), // a mistyped number
gateway.SendAsync("9876540107", "Test 3"),
];
try
{
await Task.WhenAll(mixed);
}
catch (ArgumentException ex)
{
Console.WriteLine($"Failed: {ex.Message}; completed anyway: {mixed.Count(t => t.IsCompletedSuccessfully)}");
}
record Reminder(string Guardian, string Phone, string Student, decimal Balance)
{
public string Text => $"{Student}: fee balance {Balance}. NexCoding Public School";
}
Read 12012 marks asynchronously
One by one: 10 messages in 2605 ms
Together: 10 messages in 262 ms; first: sent to 9876540103
Cancelled after 2 messages
Failed: Invalid phone number: 98765 (Parameter 'phone'); completed anyway: 2
Three runs gave 2,605, 2,587, and 2,595 ms one by one, and 262, 263, and 256 ms together.
Example: await in a loop runs one at a time
In the foreach, each await waits for one message before the loop moves on. Ten messages
of 250 ms each take about 2.6 seconds: the sum of all the waits.
That is correct when each step needs the previous one, such as "save the payment, then send the receipt". For independent messages, it wastes time.
Example: Task.WhenAll runs them together
reminders.Select(r => gateway.SendAsync(...)).ToArray() starts all ten sends without
awaiting them, giving ten running tasks. await Task.WhenAll(sends) then waits once for all
of them, and returns their ten results in the same order.
The total time is about the time of the slowest single message: 262 ms instead of 2,605. Nothing ran faster; the waits overlapped.
Example: cancellation
A CancellationTokenSource with a 600 ms time limit produced a token, passed down to
SendAsync and from there to Task.Delay. When the time ran out, the waiting task stopped with
an OperationCanceledException, and the loop ended after two messages.
Every asynchronous method should accept a CancellationToken and pass it on. In the Web API,
ASP.NET Core cancels the token when a parent closes the browser, so the server stops working on
a page nobody will read.
Example: when one task fails
The second of three sends had a mistyped number, 98765. await Task.WhenAll(mixed) threw that
ArgumentException, but the other two tasks still completed: completed anyway: 2.
WhenAll waits for every task, then rethrows the first failure. To see every failure, inspect
each task's status or Exception afterwards. Design batch sends so one bad number never stops the
whole batch.
Example: a counter that is not safe
TextMessageGateway.Sent adds 1 after every message. In the "together" run, ten continuations can
finish at nearly the same moment, on different threads.
Sent++ is not safe in that situation, which is why that demo prints results.Length instead.
Lesson 54 shows exactly what goes wrong, and how to fix it.
Example: rules for async code
| Rule | Why |
|---|---|
Async all the way: if you call an async method, await it | Blocking with .Result or .Wait() wastes a thread, and can deadlock in older frameworks |
Never async void, except event handlers | Nobody can await it, and its exceptions crash the process |
Pass a CancellationToken through every async method | Lets callers stop work nobody needs |
End async method names with Async | Callers can see they must await it |
Forgetting await compiles, but with a warning: CS4014: Because this call is not awaited, execution of the current method continues before the call is completed. Using await in a
method without async fails with CS4033.
Common mistakes
- Blocking on async code with
.Resultor.Wait(). Threads stand idle, and in UI or older ASP.NET apps it can deadlock. Fix:awaitall the way up. async voidmethods. Exceptions cannot be caught by the caller. Fix: returnTask.- Awaiting independent work one at a time. Waits add up. Fix: start all, then
Task.WhenAll. - Ignoring cancellation. Work continues after nobody needs it. Fix: accept and pass a
CancellationToken. - Using
asyncfor calculations. It does not add speed. Fix: parallelism for CPU-bound work.
In real projects
On a busy results day, a school portal stopped responding although the server's processor was
almost idle. Every request called the database with .Result, blocking a thread for each wait.
The thread pool ran out of threads, a problem called thread pool starvation. New requests queued up behind threads that were doing nothing but waiting.
Making the data access async all the way, with no other change, let the same server handle the
load. Since then, any .Result or .Wait() in web code is a review comment from me.
Best practices
- Use
asyncandawaitfor I/O: databases, files, and network calls. - Await all the way up; never block with
.Resultor.Wait(). - Run independent operations together with
Task.WhenAll. - Accept a
CancellationTokenin every async method, and pass it on. - Return
TaskorTask<T>; useasync voidonly for event handlers.
Recap
- Asynchronous code frees the thread while waiting for I/O.
- An
asyncmethod returnsTaskorTask<T>;awaitpauses it without blocking. - Awaiting in a loop runs one at a time (2.6 s);
Task.WhenAlloverlaps the waits (0.26 s). - A
CancellationTokenstops work that is no longer needed. WhenAllrethrows the first failure after all tasks finish.asynchelps waiting, not calculating; CPU-bound work is Lesson 54.
Homework
Library module task: When The Jungle Book comes back, notify Kabir, Meera, and Rohan at the
same time with Task.WhenAll, each notice taking 300 ms. Write the notices to a log file with
File.WriteAllLinesAsync and read them back.
Then send a notice with a 100 ms time limit, and show that it is cancelled.
A correct result looks like:
3 notices in about the time of one
Logged 3 lines; first: The Jungle Book is ready for Kabir Singh
Notice to Sana Sheikh cancelled: the 100 ms time limit passed
Show the solution
// Library homework, Lesson 53: notify waiting students at the same time, then log asynchronously.
// Run with: dotnet run library-assignment/053-library-async.cs
using System.Diagnostics;
string[] waiting = ["Kabir Singh", "Meera Iyer", "Rohan Das"];
Stopwatch timer = Stopwatch.StartNew();
string[] results = await Task.WhenAll(waiting.Select(student => NotifyAsync(student, "The Jungle Book")));
Console.WriteLine($"{results.Length} notices in about {(timer.ElapsedMilliseconds < 500 ? "the time of one" : "the time of all")}");
string logPath = Path.Combine(Path.GetTempPath(), "library-notices.log");
await File.WriteAllLinesAsync(logPath, results);
string[] logged = await File.ReadAllLinesAsync(logPath);
Console.WriteLine($"Logged {logged.Length} lines; first: {logged[0]}");
using CancellationTokenSource timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
try
{
await NotifyAsync("Sana Sheikh", "Malgudi Days", timeout.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Notice to Sana Sheikh cancelled: the 100 ms time limit passed");
}
static async Task<string> NotifyAsync(string student, string title, CancellationToken cancellationToken = default)
{
await Task.Delay(300, cancellationToken); // pretend to send a message
return $"{title} is ready for {student}";
}
The log goes to the system's temporary folder, Path.GetTempPath(), so the homework never leaves
files inside the repository.
Quiz
What does await do when the task is not finished?
Show answer
Answer: B. Pauses the method and frees the thread until the task completes The method continues from the same line when the task completes.
Ten independent 250 ms sends: which is fastest?
Show answer
Answer: B. Start all, then await Task.WhenAll The waits overlap: about 260 ms instead of 2.6 seconds.
Which kind of work does async speed up?
Show answer
Answer: B. Waiting for I/O such as databases and networks Async makes waiting cheap; it does not make the processor faster.
Why avoid async void?
Show answer
Answer: B. Callers cannot await it, and its exceptions can crash the process Return Task instead; async void is only for event handlers.
One of three tasks fails. What does await Task.WhenAll do?
Show answer
Answer: B. Waits for all, then throws the failure The other two completed anyway in the demo.
Interview questions
Questions are grouped by experience level. Click a question to see a model answer. All questions: Advanced C# interview questions.
Fresher (0-2 yrs)
F What do async and await do? concept
async marks a method that can use await and returns Task or Task<T>. await pauses the
method until a task completes, giving the thread back in the meantime, and continues from the same
line afterwards.
F What is the difference between Task and Task<T>? concept
Both represent work that finishes later. Task has no result, like writing a log file;
Task<T> produces a value of type T, like Task<string> for a send status.
F Does async make code run faster? concept
Not by itself. It makes waiting for I/O cheaper by freeing threads, which improves scalability and lets independent waits overlap. CPU-bound calculations need parallelism instead.
Mid (3-5 yrs)
M How do you run several independent async operations together? coding
Start them without awaiting, collect the tasks, and await Task.WhenAll(tasks). Ten 250 ms sends
took about 260 ms together instead of 2.6 seconds one by one. Results come back in the same order.
M What is a CancellationToken, and why pass it through async methods? concept
A token that signals that work should stop, created by a CancellationTokenSource, for example with
a time limit. Passing it down to every async call lets the whole operation stop early; ASP.NET Core
cancels it when the client disconnects.
M Why should you avoid async void? concept
An async void method cannot be awaited, so callers cannot know when it ends, and exceptions it
throws cannot be caught by the caller and can crash the process. Return Task instead; use
async void only for event handlers.
Senior (6-9 yrs)
S A web portal stops responding under load, but the CPU is nearly idle. The code uses .Result on database calls. What is happening? scenario
Thread pool starvation: each request blocks a thread while waiting for the database, so the pool
runs out and new requests queue. Make the call chain async all the way with await, so threads are
released during waits.
How to answer: Explain the symptom (low CPU, high latency), the cause (blocked threads), the fix, and how to confirm it with thread pool counters or dotnet-counters.
S How are exceptions handled with Task.WhenAll? concept
WhenAll waits for all tasks to finish, then await rethrows the first exception; the other
tasks still complete. Inspect each task's status or the aggregate Exception to see all failures,
and design batches so one failure does not stop the rest.
How to answer: Show awareness that only the first exception surfaces through await, and how to collect all errors for reporting in batch jobs.
Architect / Lead (10+ yrs)
A How would you send fee reminders to 10,000 families reliably and quickly? design
Send asynchronously with limited concurrency (for example batches or a semaphore) so the provider is not overwhelmed, with timeouts and cancellation, retries for temporary failures, and a record of what was sent so a restart does not send twice. Run it as a background job, not inside a web request.
How to answer: Cover throughput (bounded concurrency), reliability (retries, idempotency, persistence), provider limits, and observability. Mention background workers in Module 12.
Resources
- Code for this lesson: git tag
lesson-53-async-await(code repository coming soon) - Files:
src/SchoolApp.Console/TextMessageGateway.cs,src/SchoolApp.Console/Program.cs,library-assignment/053-library-async.cs - Video: coming soon
What's next
Async made waiting cheap. Next: making heavy calculations faster by using every processor core, and the race conditions that appear when several threads share one counter. Lesson 54: Threading
Want live sessions, code reviews, and interview preparation? Join the next batch at Sahasra Technologies.