AlgoMaster Logo

Synchronous and Asynchronous I/O

14 min readUpdated August 7, 2026
Listen to this chapter
Unlock Audio

A storage service needs to read four independent index pages before it can answer a request.

With ordinary synchronous reads, one thread can request the first page and receive its result, then request the second, and so on. If each page requires device access, the thread repeatedly waits while the storage hardware works.

An asynchronous interface separates submitting each read from receiving its result. The service can place all four operations in flight, perform other useful work, and process each completion when it arrives.

The central distinction is:

A synchronous operation reports its result as part of the call. An asynchronous operation is submitted now and reports its result through a separate completion path.

This distinction is independent of whether an ordinary call blocks or returns EAGAIN.

Two Different Questions

I/O terminology becomes confusing when two questions are treated as if they were one:

  1. What happens if an operation cannot make progress now?
  2. When and how does the application receive the result of that operation?

Blocking versus non-blocking answers the first question.

Synchronous versus asynchronous answers the second.

The three most common behaviors are:

ModelThe callWhat comes back
Synchronous, blockingread(), waiting if necessaryBytes, end-of-stream, or an error
Synchronous, non-blockingread(), making whatever progress is availableBytes, end-of-stream, an error, or EAGAIN
AsynchronousSubmit a read request, which returns before the I/O result existsA later completion, while the operation stays in flight

A normal non-blocking read() is still synchronous. It returns the result of that attempt directly: bytes, 0, an error, or EAGAIN. If it returns EAGAIN, that invocation is finished. The kernel does not retain the call and later fill the same buffer.

An asynchronous read has a different lifetime. Successful submission creates an in-flight operation that exists after the submission call returns.

Synchronous I/O at the API Boundary

Consider a blocking file read:

If the requested data is not available in memory, the kernel may submit work to a storage controller, park the calling thread, handle a later device interrupt, and eventually wake the thread.

The hardware path is highly asynchronous internally:

The kernel submits the device work, the device operates independently, an interrupt reports completion, and the kernel wakes the thread.

The application-facing operation is still synchronous because read() does not return a successful byte count until its result is ready. Internal device concurrency does not automatically make the API asynchronous.

Completion means the documented operation completed

The word completion must be interpreted at the interface's documented boundary.

An ordinary buffered write() can synchronously return the number of bytes accepted by the kernel even though storage hardware has not made those bytes durable. The completed operation, from that call's perspective, was acceptance into the kernel's file-writing path.

Synchronous therefore does not mean “every physical consequence is finished.” It means the call has produced its documented result before returning.

Submission and Completion in Asynchronous I/O

An asynchronous operation has at least two visible phases.

During submission, the application describes the work:

  • Which operation to perform
  • Which file, socket, or device-backed object to use
  • Where the data should come from or go
  • How many bytes are involved
  • How to identify the request later

If submission succeeds, the operation becomes in flight. Its eventual outcome is not known merely because the submission call succeeded.

During completion, the application learns the result:

  • A positive number of bytes transferred
  • End-of-stream
  • A partial result
  • A cancellation result
  • An I/O error

The lifecycle is:

A completed operation is not the same as a consumed one. The result sits somewhere until the application collects it, and requests that are never collected are a leak.

The distinction between completed and consumed is useful. The kernel or runtime may have placed a result in a completion queue, but the application has not acted on it until it retrieves and processes that result.

Loading simulation...

Submission Success vs. I/O Success

An asynchronous submission call answers a narrow question:

Did the I/O system accept responsibility for tracking this request?

It does not usually answer whether the requested read or write will succeed.

For example:

  1. The application submits an asynchronous read.
  2. Submission succeeds, with request ID 42.
  3. The device later reports an unrecoverable read error.
  4. The completion for request 42 carries that failure.

A successful submission says nothing about whether the I/O worked.

Correct code handles two error channels:

  1. Submission errors, such as an invalid request or no capacity to accept more work
  2. Completion errors, such as device failure, connection reset, or cancellation

Logging “read succeeded” immediately after successful submission is therefore incorrect. At that moment, only submission has succeeded.

Request Identity Requirements

Synchronous code has an obvious relationship between call and result:

The result belongs to the call currently returning on this thread.

Asynchronous code may have hundreds of operations in flight and may receive their completions in a different order from submission. Each operation therefore needs an identity or application-supplied context.

Suppose reads A, B, and C are submitted in that order. Their completions might arrive as B, C, and then A.

This order can occur because requests target different devices or file regions, encounter different queue delays, or have different sizes.

A completion record commonly includes:

  • A request identifier or application context
  • The operation's result
  • The number of bytes transferred
  • Error or status information

The application uses the identity to find the correct buffer, request state, client, or higher-level operation. It must not assume that “the next completion belongs to the oldest request.”

Buffer-Lifetime Correctness

With a synchronous read, the application buffer only needs to remain valid until read() returns.

With an asynchronous read, the kernel or runtime may write into the buffer after the submission function has returned. The buffer must remain allocated, mapped, and otherwise valid until the completion contract says the operation is finished.

This function is unsafe for an API that retains the supplied buffer:

The same rule applies in the opposite direction. A buffer used for an asynchronous write must not be modified or freed while the I/O system may still read from it.

A practical request object often owns both the operation metadata and its buffer:

The application releases the request and buffer only after consuming a terminal completion.

Buffer ownership rules vary by API. Some interfaces copy small submission data immediately; others retain pointers. Correct code follows the exact contract rather than assuming that “submission returned” means “all arguments can be discarded.”

Partial Results in Asynchronous I/O

Asynchronous does not mean all-or-nothing.

An asynchronous stream read may complete with fewer bytes than requested. An asynchronous write may report that only a prefix was accepted. The application still needs protocol state and byte offsets.

For a 1000-byte write, a completion might report that 400 bytes were written. The remaining operation then starts at byte 400 and covers bytes 400 through 999.

The completion belongs to the submitted operation, but its byte count has the same importance as a synchronous return value. Re-submitting the entire buffer would duplicate the accepted prefix.

End-of-stream and errors also arrive as completion results rather than as the return value of the original submission call.

How Completions Reach the Application

Asynchronous APIs expose completion in several forms:

  • A callback invoked with the result
  • A signal or other notification
  • A future or promise that becomes complete
  • A queue containing completion records
  • A function that waits for or queries submitted requests

These mechanisms differ in ergonomics and performance, but they express the same transition from an in-flight request to an observable terminal result.

The code that processes a completion may run on the submitting thread, another runtime thread, or a thread that retrieves entries from a shared completion queue. An API must document that execution context because it affects synchronization and which operations are safe inside a callback.

A future or callback alone does not prove that the operating system performs kernel-native asynchronous I/O. It describes how the application receives a result, not how the implementation waits underneath.

Native Asynchrony and Helper Threads

There are two common ways to provide an asynchronous interface.

Kernel-native asynchronous I/O

The application submits a request to the kernel. The request remains in flight without requiring one application worker to block for its whole duration. A thread runs when submitting work and when processing completions, but no thread must represent each waiting operation.

This model can match hardware that already supports command and completion queues.

Asynchrony built with worker threads

A library can place a task on a worker queue. A helper thread calls a normal blocking I/O function, sleeps if necessary, and completes a future or invokes a callback when the call returns:

The blocking call never disappeared. It moved to a worker thread, which is why this arrangement still costs one thread per operation in flight.

The application-facing API is asynchronous because the original caller does not wait. The underlying kernel operation is synchronous and occupies a worker thread while blocked.

Both designs can be correct. Their resource costs differ. A helper-thread design needs enough workers and stacks to represent concurrent blocking operations. Kernel-native asynchronous I/O can keep more operations in flight without one sleeping worker per operation, although requests and buffers still consume memory and kernel resources.

Asynchronous syntax in a programming language does not reveal which implementation is being used. The runtime and the specific type of I/O determine whether a hidden worker is involved.

Overlapping Independent Work

The main advantage of asynchronous I/O is that a thread does not have to serialize its execution behind each I/O wait.

Three synchronous blocking reads on one thread look like:

With asynchronous submission, the requests can overlap:

The three reads now overlap instead of queueing behind each other, and the thread spends the waiting time on other work.

This can improve throughput when operations are independent and the device or remote service supports useful concurrency.

It does not make one device operation intrinsically faster. Asynchronous execution can add submission, bookkeeping, and completion-handling overhead. Too many in-flight requests can also consume memory, fill queues, and increase latency. A production system therefore keeps in-flight work bounded instead of treating asynchronous capacity as unlimited.

Cancellation Races

An application may lose interest in an in-flight operation because a timeout expires, a client disconnects, or a larger request is abandoned.

Requesting cancellation does not mean the operation instantly disappears. Several outcomes are possible:

  • The request is canceled before it starts.
  • The request completes normally before cancellation takes effect.
  • Part of the operation completes.
  • The underlying operation cannot be canceled.

The application must follow the API's terminal completion rules. It cannot free the buffer merely because it requested cancellation; the I/O system may still access that buffer until cancellation or normal completion is confirmed.

Closing a descriptor while operations are in flight is similarly API-specific. Robust code does not assume that closing the integer handle automatically resolves every request and buffer-lifetime obligation.

A POSIX AIO Example

POSIX AIO provides a compact C example of the submission/completion split for file I/O. The following program submits one read, performs application work after submission, and then waits for the request's terminal result:

On Linux, compile and run it with:

Representative output:

aio_read() returning 0 means the request was submitted, not that the file read succeeded. The program obtains the final status through aio_error() and calls aio_return() exactly once to collect the byte count.

The buffer and aiocb remain alive until completion is collected. If either were local to a helper function that returned immediately after aio_read(), the operation could access expired storage.

Calling aio_suspend() makes this example wait after its two lines of application work. A larger program can submit several requests, continue useful computation, and wait only when it has nothing else to do.

On Linux, a C library's POSIX AIO implementation may use helper threads internally. The example demonstrates the asynchronous API contract; it does not prove that a particular run used kernel-native asynchronous I/O.

When Asynchronous I/O Helps

Asynchronous I/O is most useful when:

  • Many independent operations can be in flight.
  • Waiting time is large relative to submission and completion overhead.
  • The application has useful work to perform while requests are pending.
  • Avoiding one blocked kernel thread per operation materially reduces resource use.

Synchronous I/O remains a strong choice when control flow is naturally sequential, concurrency is modest, or the operation is usually satisfied immediately. It is simpler because the call stack itself carries the operation's state and buffer lifetime.

The choice is not merely a syntax preference. In synchronous code, the call stack naturally holds state until the operation returns. In asynchronous code, an explicit request object must hold state across submission and completion. The asynchronous version moves waiting out of the thread, but it moves responsibility into request bookkeeping.

Summary

Synchronous I/O reports an operation's result as part of the call. It may block until progress occurs or return immediately with a result such as EAGAIN. Asynchronous I/O separates submission from completion and leaves a request in flight after submission returns.

Successful submission does not imply successful I/O. Each completion must identify its request and report a byte count, end-of-stream, cancellation, or error. Completions can arrive out of order and may report partial progress.

Asynchronous buffers and request metadata must remain valid until terminal completion. Cancellation does not release them immediately because cancellation can race with normal completion.

Kernel-native asynchronous I/O avoids one blocked application worker per request. Libraries can also provide an asynchronous API with helper threads that perform synchronous calls. Both models move waiting away from the caller, but they have different resource costs.

The central mental model is:

A synchronous call delivers the attempted operation's result before it returns; an asynchronous request preserves explicit state from submission until a later completion.

Quiz

Synchronous and Asynchronous I/O Quiz

5 quizzes