Loading learning content...
What if your program could ask "is there data available?" without getting stuck waiting? What if every I/O call returned instantly, telling you either "here's your data" or "nothing available right now, try again later"? This is the essence of non-blocking I/O.
Non-blocking I/O inverts the blocking model's fundamental assumption. Instead of suspending your process until data is ready, non-blocking calls return immediately with whatever status is currently available. This shifts responsibility: the kernel no longer manages your waiting; you must explicitly decide when and how to retry.
By the end of this page, you will understand the precise semantics of non-blocking I/O, how to enable it on file descriptors, the critical role of EAGAIN/EWOULDBLOCK, and the polling patterns required to use non-blocking I/O effectively. You'll see why non-blocking I/O alone is rarely sufficient and how it sets the stage for I/O multiplexing.
Non-blocking I/O is an I/O model where system calls return immediately, regardless of whether the requested operation can complete. If the operation would block (e.g., no data available for read), the call returns an error indication rather than waiting.
The formal definition:
A non-blocking I/O operation behaves as follows:
This is in contrast to blocking I/O, where the call would suspend the process until the operation could complete.
Historically, EAGAIN and EWOULDBLOCK were different error codes on some Unix systems. In modern Linux, they're the same value (11). POSIX allows them to be either the same or different. Robust code checks for both: if (errno == EAGAIN || errno == EWOULDBLOCK)
The mental model:
Think of non-blocking I/O like a fast-food counter with multiple pickup windows. You place your order and immediately check window #1: "Is my order ready?" "Not yet." You check window #2: "Is my order ready?" "Yes, here it is!" You're never stuck waiting at one window—you can always move on and check again later.
This model is sometimes called synchronous non-blocking. It's synchronous because each call gives you an immediate answer about its status; it's non-blocking because you're never suspended waiting.
| Operation | Returns Immediately When | Returns EAGAIN When | Error Behavior |
|---|---|---|---|
| read() | Data available in buffer | Buffer empty, no data yet | Returns -1, errno set |
| write() | Buffer has space | Buffer full, backpressure | Returns -1, errno set |
| accept() | Connection pending | No pending connections | Returns -1, errno set |
| connect() | Connection initiated (returns EINPROGRESS) | N/A - EINPROGRESS indicates 'in progress' | Returns -1, errno set |
| recv() | Data in socket buffer | Socket buffer empty | Returns -1, errno set |
| send() | Send buffer has space | Send buffer full | Returns -1, errno set |
By default, file descriptors operate in blocking mode. Non-blocking behavior must be explicitly enabled. There are several ways to do this:
When opening a file or device, include the O_NONBLOCK flag:
1234567891011121314151617181920212223242526272829303132
#include <fcntl.h>#include <stdio.h>#include <errno.h> int main() { // Open a named pipe (FIFO) in non-blocking mode // Without O_NONBLOCK, this would block until a writer opens the FIFO int fd = open("/tmp/myfifo", O_RDONLY | O_NONBLOCK); if (fd < 0) { perror("open"); return 1; } // All reads on this fd will be non-blocking char buffer[1024]; ssize_t n = read(fd, buffer, sizeof(buffer)); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { printf("No data available yet (not an error)\n"); } else { perror("read error"); } } else if (n == 0) { printf("EOF reached\n"); } else { printf("Read %zd bytes\n", n); } return 0;}For already-open file descriptors (including those from socket() or inherited from parent processes), use fcntl() to add or remove the O_NONBLOCK flag:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
#include <fcntl.h>#include <unistd.h>#include <stdio.h> /** * Set a file descriptor to non-blocking mode. * Returns 0 on success, -1 on error. */int set_nonblocking(int fd) { // Get current flags int flags = fcntl(fd, F_GETFL, 0); if (flags < 0) { perror("fcntl F_GETFL"); return -1; } // Add O_NONBLOCK flag if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { perror("fcntl F_SETFL"); return -1; } return 0;} /** * Set a file descriptor back to blocking mode. */int set_blocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags < 0) return -1; // Remove O_NONBLOCK flag return fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);} int main() { // Create a socket (blocking by default) int sockfd = socket(AF_INET, SOCK_STREAM, 0); // Make it non-blocking if (set_nonblocking(sockfd) < 0) { return 1; } // Now all I/O on sockfd will be non-blocking // connect(), read(), write() all return immediately return 0;}Linux provides a convenience flag when creating sockets:
12345678910
#include <sys/socket.h>#include <netinet/in.h> // Create a non-blocking TCP socket in one call// Avoids race condition between socket() and fcntl()int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); // Similarly for accept4():int client_fd = accept4(server_fd, NULL, NULL, SOCK_NONBLOCK);// The new client socket is created in non-blocking modeUsing SOCK_NONBLOCK in socket() or accept4() is preferred over calling fcntl() afterward. The atomic approach avoids a race condition where another thread could perform I/O on the socket between creation and the fcntl() call. It also requires one fewer system call.
Non-blocking read() behaves differently from blocking read() in crucial ways. Understanding these semantics is essential for correct programming.
Return values for non-blocking read():
| Return Value | Meaning |
|---|---|
| > 0 | Success: returned number of bytes read |
| 0 | End-of-file (EOF) on file, or connection closed for sockets |
| -1, errno=EAGAIN | No data available; would have blocked |
| -1, errno=EINTR | Interrupted by signal; retry |
| -1, other errno | Actual error (EBADF, EIO, etc.) |
The critical difference from blocking read:
With blocking read, a return of -1 always indicates an error (except for EINTR). With non-blocking read, EAGAIN is not an error—it's the normal indication that no data is currently available.
This fundamentally changes how you write I/O code:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
#include <unistd.h>#include <errno.h>#include <stdio.h> /** * Attempt to read available data from a non-blocking fd. * Returns: * > 0: bytes read * 0: EOF * -1: would block (not an error!) * -2: actual error */ssize_t try_read(int fd, void *buf, size_t count) { ssize_t n = read(fd, buf, count); if (n > 0) { // Success! Got some data return n; } if (n == 0) { // EOF / connection closed return 0; } // n == -1, check errno if (errno == EAGAIN || errno == EWOULDBLOCK) { // NOT an error: just no data available return -1; // Signal "would block" } if (errno == EINTR) { // Interrupted by signal - typically retry immediately return try_read(fd, buf, count); // Recursive retry } // Actual error perror("read"); return -2;} /** * Non-blocking read pattern: read all available data * Useful for draining a socket of all pending data */ssize_t read_all_available(int fd, void *buf, size_t bufsize) { size_t total = 0; char *ptr = (char *)buf; while (total < bufsize) { ssize_t n = read(fd, ptr + total, bufsize - total); if (n > 0) { total += n; // Continue reading - there might be more data continue; } if (n == 0) { // EOF reached break; } // n == -1 if (errno == EAGAIN || errno == EWOULDBLOCK) { // No more data available right now // This is NOT an error - we've read all available data break; } if (errno == EINTR) { continue; // Retry } // Actual error return -1; } return total;}When read() returns EAGAIN, it means 'no data now.' Immediately retrying in a tight loop wastes CPU (busy waiting). Instead, use I/O multiplexing (select/poll/epoll) to wait efficiently until data arrives, then retry.
Non-blocking writes present their own challenges, particularly around partial writes and buffer management.
Return values for non-blocking write():
| Return Value | Meaning |
|---|---|
| > 0 | Success: returned number of bytes written (may be < count!) |
| 0 | No bytes written; rare but possible |
| -1, errno=EAGAIN | Buffer full; would have blocked |
| -1, errno=EINTR | Interrupted by signal; retry |
| -1, errno=EPIPE | Connection closed by peer |
| -1, other errno | Actual error |
The partial write challenge:
Non-blocking write() may write fewer bytes than requested. Unlike blocking write (which would wait until all bytes can be accepted), non-blocking write returns as soon as it's written whatever it can. You must track what's been written and retry the remainder later.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
#include <unistd.h>#include <errno.h>#include <string.h>#include <stdlib.h> /** * Write buffer for handling partial writes in non-blocking I/O. * Tracks pending data that couldn't be written immediately. */typedef struct { char *data; // Buffer of pending data size_t capacity; // Total buffer capacity size_t size; // Amount of pending data size_t offset; // Start of pending data (bytes already sent)} WriteBuffer; /** * Initialize a write buffer. */WriteBuffer *write_buffer_create(size_t capacity) { WriteBuffer *wb = malloc(sizeof(WriteBuffer)); wb->data = malloc(capacity); wb->capacity = capacity; wb->size = 0; wb->offset = 0; return wb;} /** * Add data to the write buffer. * Returns 0 on success, -1 if buffer is full. */int write_buffer_append(WriteBuffer *wb, const void *data, size_t len) { // Compact buffer if needed if (wb->offset > 0 && wb->offset + wb->size + len > wb->capacity) { memmove(wb->data, wb->data + wb->offset, wb->size); wb->offset = 0; } if (wb->size + len > wb->capacity) { return -1; // Buffer full } memcpy(wb->data + wb->offset + wb->size, data, len); wb->size += len; return 0;} /** * Attempt to flush write buffer to non-blocking fd. * Returns: * 1: all data flushed * 0: partial write, data remains * -1: error (connection closed or other) */int write_buffer_flush(WriteBuffer *wb, int fd) { while (wb->size > 0) { ssize_t n = write(fd, wb->data + wb->offset, wb->size); if (n > 0) { wb->offset += n; wb->size -= n; continue; // Try to write more } if (n == 0) { // Unusual but handle defensively return 0; // Partial progress } // n == -1 if (errno == EAGAIN || errno == EWOULDBLOCK) { // Kernel buffer full, can't write more now return 0; // Partial: data remains } if (errno == EINTR) { continue; // Retry immediately } // EPIPE, ECONNRESET, or other error return -1; } // All data flushed! wb->offset = 0; return 1;} /** * Check if buffer has pending data. */int write_buffer_has_data(WriteBuffer *wb) { return wb->size > 0;} // Usage examplevoid example_usage(int sockfd) { WriteBuffer *wb = write_buffer_create(65536); // Application wants to send a message const char *msg = "Hello, World!"; write_buffer_append(wb, msg, strlen(msg)); // Try to flush int result = write_buffer_flush(wb, sockfd); if (result == 1) { // All sent! } else if (result == 0) { // Partial write - need to monitor fd for writability // When epoll says fd is writable, call flush again } else { // Error - connection problems }}With non-blocking I/O, you almost always need application-level write buffering. When write() returns EAGAIN, you must save the unsent data and retry later when the socket becomes writable. This is why event-driven frameworks all have built-in write buffering.
Establishing TCP connections with non-blocking sockets requires special handling because connect() cannot complete instantly—the TCP three-way handshake takes time.
How non-blocking connect() works:
connect() is called on a non-blocking socketconnect() returns immediately with -1, errno=EINPROGRESS123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <fcntl.h>#include <errno.h>#include <poll.h>#include <stdio.h>#include <string.h>#include <unistd.h> /** * Initiate a non-blocking connect. * Returns: * 1: connected immediately (local connection, rare) * 0: connection in progress * -1: error */int start_connect(int sockfd, const char *host, int port) { struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); inet_pton(AF_INET, host, &addr.sin_addr); int result = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)); if (result == 0) { // Connected immediately (rare - usually loopback only) return 1; } if (errno == EINPROGRESS) { // Connection initiated, check back later return 0; } // Actual error (ECONNREFUSED, ENETUNREACH, etc.) return -1;} /** * Check if a non-blocking connect has completed. * Call this after poll/epoll indicates the socket is writable. * Returns: * 0: success, connected * -1: connection failed (errno set) */int check_connect_result(int sockfd) { int error = 0; socklen_t len = sizeof(error); // Get pending error on socket if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) { return -1; // getsockopt failed } if (error != 0) { // Connection failed - error contains the reason errno = error; return -1; } // Success! return 0;} /** * Complete example: non-blocking connect with timeout */int connect_with_timeout(const char *host, int port, int timeout_ms) { // Create non-blocking socket int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); if (sockfd < 0) return -1; // Start connection int result = start_connect(sockfd, host, port); if (result == 1) { // Already connected return sockfd; } if (result < 0) { close(sockfd); return -1; } // Wait for socket to become writable (or timeout) struct pollfd pfd = { .fd = sockfd, .events = POLLOUT, // Wait for writability .revents = 0 }; result = poll(&pfd, 1, timeout_ms); if (result == 0) { // Timeout close(sockfd); errno = ETIMEDOUT; return -1; } if (result < 0) { close(sockfd); return -1; } // Socket is writable - check if connect succeeded if (check_connect_result(sockfd) < 0) { close(sockfd); return -1; } // Success! Optionally switch back to blocking mode // (or keep non-blocking for the data phase) return sockfd;} int main() { int sock = connect_with_timeout("192.168.1.1", 80, 5000); if (sock < 0) { perror("connect failed"); return 1; } printf("Connected!\n"); close(sock); return 0;}When poll/epoll signals that a connecting socket is writable, it does NOT mean the connection succeeded. A failed connection also makes the socket 'writable' (you can try to write, it will just fail). Always use getsockopt(SO_ERROR) to get the actual connection result.
Non-blocking I/O solves the "thread gets stuck" problem but introduces a new challenge: how do you know when to retry?
The naive approach: Busy polling
123456789101112131415161718
// BAD: Busy polling wastes CPU!while (1) { ssize_t n = read(fd, buffer, sizeof(buffer)); if (n > 0) { // Got data, process it process(buffer, n); } else if (n == 0) { break; // EOF } else if (errno == EAGAIN) { // No data yet - but we immediately retry! // This loop runs millions of times per second // consuming 100% CPU doing nothing useful continue; // BAD! } else { break; // Error }}This approach is terrible for performance. The CPU spins in a tight loop, continuously making system calls that return EAGAIN. This wastes CPU, generates heat, increases power consumption, and steals cycles from other processes.
The naive fix: Sleep between polls
12345678910111213141516171819
// MEDIOCRE: Sleep reduces CPU but adds latencywhile (1) { ssize_t n = read(fd, buffer, sizeof(buffer)); if (n > 0) { process(buffer, n); } else if (n == 0) { break; } else if (errno == EAGAIN) { // Sleep before retrying usleep(10000); // 10 milliseconds // Problem: We ALWAYS wait 10ms, even if data arrives in 1ms // Average added latency: 5ms // This is too slow for many applications continue; } else { break; }}Sleep-based polling is a band-aid that trades CPU usage for latency. If you sleep 10ms, you might miss data that arrives 1ms after you started sleeping, adding up to 10ms of unnecessary delay.
The real solution: I/O multiplexing
What we really want is to tell the kernel: "Wake me up when any of these file descriptors has data." This is exactly what I/O multiplexing (select/poll/epoll) provides, which we'll cover in detail.
Non-blocking I/O is rarely used in isolation. It's almost always paired with an I/O multiplexing mechanism that efficiently waits for I/O readiness.
| Approach | CPU Usage | Latency | Practicality |
|---|---|---|---|
| Busy polling | 100% (terrible) | Minimal | Never acceptable in production |
| Sleep polling (1ms) | Low | +0.5ms average | Okay for low-volume, latency-tolerant |
| Sleep polling (10ms) | Very low | +5ms average | Only for background tasks |
| I/O multiplexing | Minimal | Near-zero | Standard approach for all serious applications |
Despite the polling complexity, non-blocking I/O enables important patterns that blocking I/O cannot achieve.
123456789101112131415161718192021222324252627282930313233343536373839404142
#include <time.h> /** * Example: Game loop with non-blocking network I/O * The game continues running smoothly regardless of network conditions */void game_loop(int network_socket) { // Socket is non-blocking set_nonblocking(network_socket); struct timespec last_frame, current; clock_gettime(CLOCK_MONOTONIC, &last_frame); while (game_running) { // 1. Handle network I/O (non-blocking, never stalls) char net_buffer[1024]; ssize_t n = read(network_socket, net_buffer, sizeof(net_buffer)); if (n > 0) { process_network_message(net_buffer, n); } // EAGAIN is fine - no network data this frame, carry on // 2. Process input (from queue, non-blocking) process_input_queue(); // 3. Update game state clock_gettime(CLOCK_MONOTONIC, ¤t); double delta = (current.tv_sec - last_frame.tv_sec) + (current.tv_nsec - last_frame.tv_nsec) / 1e9; update_game_state(delta); last_frame = current; // 4. Render frame render_frame(); // 5. Send pending network messages (non-blocking) flush_network_queue(network_socket); // Target 60 FPS sleep_until_next_frame(); }}While servers are the classic use case for non-blocking I/O, it's equally important for clients—especially clients with real-time requirements or graphical interfaces. Any application that must remain responsive while doing I/O benefits from non-blocking techniques.
Non-blocking I/O provides the foundation for scalable, responsive applications. Let's consolidate the key concepts:
What's next:
Non-blocking I/O alone requires polling, which is inefficient. The next step is asynchronous I/O—a model where the kernel notifies you when operations complete, without any polling at all. We'll explore the semantics, APIs, and use cases of truly asynchronous I/O operations.
You now understand non-blocking I/O—the immediate-return model that enables responsive applications. You know how to enable it, interpret its return values, handle partial reads/writes, and understand why it's paired with I/O multiplexing. Next, we'll explore asynchronous I/O, which goes further by eliminating polling entirely.