A backend worker calls read() on a client socket, but the client has not sent its next request yet.
What should happen to the worker?
With blocking I/O, the kernel keeps the call pending and parks the calling thread until the read can make progress. With non-blocking I/O, the kernel reports immediately that no data is currently available.
The difference is not whether the network is fast or slow. It is the contract between the application and the kernel when an operation cannot make progress now:
Blocking I/O waits for progress. Non-blocking I/O reports that progress is currently impossible.
This choice changes how an application uses threads, preserves partially processed data, handles errors, and manages many I/O endpoints.
Consider:
Assume client_fd refers to a connected stream socket. Four broad outcomes are possible:
| Socket state | Blocking read() | Non-blocking read() |
|---|---|---|
| At least one byte is available | Returns available bytes | Returns available bytes |
| Peer has closed its sending side | Returns 0 | Returns 0 |
| No byte is available, but some may arrive | Waits | Returns -1 with EAGAIN or EWOULDBLOCK |
| An error prevents the read | Returns -1 | Returns -1 |
The mode matters only when the operation would otherwise need to wait. If data is already queued, both forms can return immediately. If the stream has ended, both report end-of-stream immediately.
Non-blocking does not change what counts as data, end-of-stream, or failure. It changes the response to not ready yet.
A blocking call does not normally make the CPU spin inside read(). The kernel records what the thread is waiting for, changes the thread from runnable to a sleeping state, and lets the scheduler choose other work.
For an empty socket, the path is conceptually:
read().read() returns data.The wait consumes a thread, but it does not consume a CPU continuously. A sleeping thread retains its stack, execution state, and kernel bookkeeping while other runnable threads execute.
The kernel rechecks the condition after wakeup because a wakeup does not reserve data exclusively for that thread. Another execution path might consume the data first, or the state may have changed for another reason.
Different operations wait for different conditions:
read() waits for data, end-of-stream, or an error.write() may wait for enough local send-buffer capacity to accept some bytes.accept() waits for a connection that can be accepted.A blocking call does not promise that the entire requested byte count will be transferred. It promises that the call may wait until it can return a valid result.
Suppose an application asks for 4096 bytes:
If only 600 bytes are available on a stream, a blocking read may return 600. It does not have to wait for all 4096 bytes.
Similarly, a write can accept fewer bytes than requested:
A positive return value means progress occurred. The application must preserve any unconsumed portion:
Blocking and non-blocking describe what happens when no immediate progress is possible. They do not make stream operations all-or-nothing.
Message-oriented endpoints can have additional rules about preserving message boundaries. The application must follow the contract of the specific endpoint rather than assuming that all descriptors behave like byte streams.
On Linux and other Unix-like systems, the O_NONBLOCK file status flag requests non-blocking behavior.
An application can ask for the flag when creating some descriptors. For example, Linux can create a non-blocking socket with:
An existing descriptor can be changed with fcntl():
Reading the existing flags first is important. Calling F_SETFL with only O_NONBLOCK can unintentionally clear other changeable status flags.
O_NONBLOCK is associated with the underlying open-file state, not merely with one integer variable in the process. Descriptors created through operations such as dup() can share that state. Changing the flag through one such descriptor can therefore affect I/O through another.
Sockets also support flags such as MSG_DONTWAIT on individual send or receive calls. That requests non-blocking behavior for one operation without changing the shared O_NONBLOCK status.
EAGAIN MeansOn a non-blocking descriptor, an operation that would need to wait normally returns:
The result means:
The operation made no progress because its required condition is not true now. Try again only when the condition may have changed.
It is not an end-of-stream indication and not necessarily a permanent failure.
On Linux, EAGAIN and EWOULDBLOCK have the same numeric value. Portable socket code should still accept either because standards do not require them to be identical everywhere:
0 is different from EAGAINFor a stream socket:
0 means the peer has ended its byte stream and no more bytes remain.-1 with EAGAIN means the stream is still open but has no bytes available now.Treating EAGAIN as disconnect closes healthy, temporarily idle connections. Treating 0 as “try later” leaves dead connections open.
EINTR is also differentA blocking system call can be interrupted by a handled signal before it transfers data. It may then return -1 with errno set to EINTR.
EINTR means the call was interrupted. EAGAIN means the operation would have to wait. Code often retries after EINTR, while it should stop its current drain attempt after EAGAIN.
Some system calls are automatically restarted under some signal-handling configurations. Correct code must still understand the documented behavior of the operation it uses.
EAGAINNon-blocking behavior is operation-specific. For example, a non-blocking connect() commonly returns -1 with errno set to EINPROGRESS when the connection attempt has started but the network handshake is not finished. This is not the same as a failed connection; the application must later check the connection result.
Calls such as read(), write(), and accept() commonly use EAGAIN when their immediate readiness condition is false. Correct code must follow the contract of the particular operation rather than treating every non-blocking result identically.
A non-blocking consumer commonly reads until one of three boundaries:
The core pattern is:
The loop continues after a positive count because more data may already be queued. It stops at EAGAIN because immediately calling read() again would normally produce the same result.
The EAGAIN branch must return control to code that can perform other work or wait efficiently for the descriptor's state to change. Retrying in a tight loop turns non-blocking I/O into busy polling:
Non-blocking mode prevents the kernel from parking this thread on the I/O condition. It does not make unavailable data arrive sooner.
Non-blocking writes require the same attention to partial progress. Suppose data_length bytes must be sent and offset records how many have already been accepted:
If 400 of 1000 bytes are accepted before EAGAIN, the application must retain the remaining 600 bytes and the offset 400. Re-sending from offset zero duplicates data. Discarding the buffer loses data.
A successful socket write means the local kernel accepted bytes into its sending path. It does not mean the remote application has consumed them. Non-blocking mode changes local waiting behavior; it does not change that success boundary.
O_NONBLOCK does not promise that a system call consumes zero time or can never wait for any internal reason. It changes the behavior of operations that would wait for an endpoint-specific readiness condition.
For sockets and pipes, that distinction is central. The kernel can report that no data or buffer capacity is available now.
Regular files behave differently. On Linux, O_NONBLOCK generally has no effect on ordinary regular-file I/O because regular files are treated as ready. A file operation can still take time due to page faults, filesystem work, memory pressure, or storage behavior.
Therefore:
A non-blocking descriptor avoids selected readiness waits; it does not make every layer of I/O non-blocking in the everyday sense of the word.
This is why setting O_NONBLOCK on every descriptor is not a universal latency solution.
A non-blocking read() checks whether it can make progress during that call. If not, it returns EAGAIN. The kernel does not keep that read pending and later fill the same application buffer on behalf of the completed call.
The application must issue another operation when progress may be possible.
This makes non-blocking I/O different from a model in which an application submits work and receives a later completion. Non-blocking mode changes the immediate syscall contract; it does not by itself create background completion.
The following program creates a connected pair of local stream sockets. It makes one endpoint non-blocking and demonstrates the three important read outcomes:
Compile and run it:
Expected output:
The first call finds an open stream with no queued bytes, so it reports EAGAIN. The second finds five bytes and returns them. After those bytes are consumed and the peer is closed, the third returns 0.
On Linux, strace can expose the corresponding system-call results:
A representative trace includes:
Library wrappers and syscall names can differ by architecture, but the return semantics remain the important observation.
Blocking I/O fits naturally when a thread has one linear job:
The code can follow that order directly. While I/O is unavailable, the kernel parks the thread efficiently. The cost is that each outstanding wait needs a thread and its associated state.
Non-blocking I/O fits when one thread must remain able to work on other endpoints instead of sleeping on any single one. It makes waiting explicit in the application:
Non-blocking code has to handle all four outcomes on every call. The would-block branch is the one that has no equivalent in blocking code, and forgetting it is what turns a non-blocking socket into a busy loop.
This can support large numbers of mostly idle connections with fewer threads, but it requires more state management. The application must remember pending input, output offsets, protocol progress, and which operations should be retried.
Neither mode is inherently superior. Blocking I/O favors straightforward control flow. Non-blocking I/O favors explicit control over when a thread may wait. The workload, concurrency level, and complexity budget determine which trade-off is appropriate.
Loading simulation...
Blocking and non-blocking I/O differ in what the kernel does when an operation cannot make immediate progress. A blocking call parks the thread until the condition changes. A non-blocking call returns -1 with EAGAIN or EWOULDBLOCK.
Both modes can return partial results. A stream read returning 0 means end-of-stream, while EAGAIN means the stream remains open but has no data now. Correct code also distinguishes interrupted calls, permanent errors, and successful partial transfers.
Non-blocking reads should consume available data until EAGAIN, then stop rather than busy-loop. Non-blocking writes must preserve the unaccepted suffix and resume from the correct offset.
O_NONBLOCK controls endpoint-specific readiness waits; it does not make all I/O instantaneous and does not create background completion. Blocking mode provides simpler sequential control flow, while non-blocking mode gives an application explicit control over when a thread may wait.
5 quizzes