Why async programming exists
· 5 min readengineering software-development #csharp #dotnet #async
Applications often wait for work they cannot do themselves.
A file read waits for disk access. A database query waits for the database. A checkout waits for a payment provider. A message sent to another system waits for a reply.
Those waits are not computation. The application has started work elsewhere, and the result is not ready yet.
Async programming exists so the rest of the application does not have to stop while that happens.
Waiting is not computing
Some work happens inside the application. Adding numbers, validating input, filtering an in-memory list, and formatting text can run immediately on the CPU.
I/O is different. It crosses a boundary to a database, network, file system, timer, or remote service. The application can start the operation, but it cannot decide when the external system finishes. These operations are called I/O-bound because their duration mostly depends on something outside the current computation.[2]
That matters because waiting for I/O is different from doing a short calculation. While a database query travels across the network, the application can either wait in place or return to work that is ready now.
One request, two designs
Imagine a weather screen. A user selects a city, and the application requests a forecast from a remote API.
With blocking code, the execution path waits for the response. In a user interface, that path cannot handle another action in simpler words it looks stuck. In a server application, it cannot handle another incoming request either.
With async code, the application starts the request and yields while the response is pending. When the response arrives, it resumes the original operation. The network request still takes the same amount of time, but the application can use the wait for other ready work.[1]
| Time | Blocking thread | Async operation |
|---|---|---|
| 0 ms | Start the weather request. | Start the weather request. |
| 1–500 ms | Stay blocked while the remote API responds. | Yield while the response is pending; other ready work can continue. |
| 500 ms | Receive the response and continue. | Resume the original operation and continue. |
The remote API decides when the response arrives. Async does not make the forecast available sooner. It changes what the application can do while it waits.
Keeping work moving while I/O waits
A user interface needs to process input, redraw itself, and report progress. If it blocks on a remote request, a click can appear to do nothing. The screen may feel frozen even though the application is only waiting for a response.
Async code lets the application return to events that are ready to run during that wait. The user can keep using the interface, and the application can update the screen when the response arrives. MDN describes the same general model: a request can wait for a response without blocking other work.[1]
Servers have a similar problem. A server may handle many requests at once, and some of them need a database query or another service before they can respond.
If every request stays blocked during external waits, its worker remains occupied and less capacity is available for newly arrived work. The server may still accept other requests, but a growing number of blocked workers can increase response times.
Asynchronous I/O avoids holding execution capacity for a request that is only waiting on another system. ASP.NET Core guidance recommends avoiding blocking calls for this reason: requests use available capacity better when they do not wait synchronously for I/O.[3]
Async does not remove real limits. The database, remote API, network connection, or memory budget can still become the bottleneck. It only avoids using application capacity for an external wait.
When async helps
Async is useful when an operation spends meaningful time waiting for I/O:
- Calling an HTTP API
- Querying a database
- Reading or writing a file
- Receiving a queue message
- Waiting for a timer or delay
Making a method asynchronous does not automatically make it useful. A short calculation that already has its data in memory has no external wait to give back to the application.