0Pricing
C Academy · Lesson

Blocking vs Non-Blocking I/O

Why event loops matter.

Blocking vs Non-Blocking I/O is a free C Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

What Blocking Means

A blocking system call suspends the calling thread until the operation can proceed. When you call recv() on a socket with no data, the kernel parks your thread until bytes arrive.

This is simple to reason about: one connection, one thread, straight-line code. The cost shows up when you need to serve thousands of clients at once.

ssize_t n = recv(fd, buf, sizeof buf, 0);
/* thread sleeps here until data or error */
if (n > 0) handle(buf, n);

The Scaling Problem

With blocking I/O, one stuck client blocks the whole thread. The classic fix is one thread (or process) per connection.

That works to a point, but 10,000 threads means 10,000 stacks, heavy context switching, and scheduler overhead. This is the famous C10k problem that pushed servers toward event-driven designs.

Non-Blocking Mode

A non-blocking socket never sleeps. If a call cannot complete immediately, it returns -1 right away and sets errno to EAGAIN or EWOULDBLOCK.

Your code is now responsible for retrying later. This lets a single thread juggle many sockets without ever getting stuck on one of them.

ssize_t n = recv(fd, buf, sizeof buf, 0);
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
    /* no data right now, try again later */
}

Setting O_NONBLOCK with fcntl

You switch a descriptor to non-blocking by adding the O_NONBLOCK flag with fcntl(). Always read the current flags first, then OR in the bit, so you do not clobber other settings.

This same helper is used on listening sockets, accepted client sockets, and pipes alike.

int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) return -1;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

Handling Partial Reads

Non-blocking I/O makes partial operations the norm. A recv() may return fewer bytes than requested, and send() may accept only part of your buffer.

You must track how much you have sent or received and resume from there. Never assume one call moves all the bytes.

size_t sent = 0;
while (sent < len) {
    ssize_t w = send(fd, buf + sent, len - sent, 0);
    if (w < 0) { if (errno == EAGAIN) break; else return -1; }
    sent += w;
}

Busy-Waiting Is Wrong

The naive way to use non-blocking sockets is to loop over all of them, retrying constantly. This busy-wait burns 100% CPU even when nothing is happening.

What we really want is to ask the kernel: "tell me which descriptors are ready, and let me sleep until then." That is exactly what readiness notification provides.

Readiness Notification

I/O multiplexing lets one thread wait on many descriptors at once and wake only when at least one is ready. The kernel does the watching for you.

The classic interfaces are select() and poll(). They work, but they rescan every descriptor on each call, which gets expensive at scale.

fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
select(fd + 1, &rfds, NULL, NULL, NULL);

Why select and poll Don't Scale

Both select() and poll() are O(n): each call passes the full descriptor set into the kernel, which scans all of them, then you scan all of them again to find the ready ones.

select() also caps out around FD_SETSIZE (often 1024). For thousands of connections this overhead dominates.

Enter epoll

epoll is Linux's scalable answer. You register interest in a descriptor once, and the kernel keeps an internal data structure tracking readiness.

Each wait returns only the descriptors that are actually ready, so cost scales with active connections, not total connections. This makes it roughly O(1) per ready event.

int epfd = epoll_create1(0);
/* register fds once, then wait for ready events */

Non-Blocking Plus epoll

epoll and non-blocking sockets are a team. epoll tells you a descriptor is ready; non-blocking calls let you drain it without ever sleeping.

You should always set O_NONBLOCK on sockets you hand to epoll. Otherwise a spurious wakeup or a partial read could block your single event-loop thread.

set_nonblocking(conn_fd);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);

The Mental Model

Picture the server as a loop: block in epoll_wait(), get back a small list of ready descriptors, do non-blocking work on each, repeat.

The thread sleeps when idle and wakes only for real work. One thread can now serve tens of thousands of connections efficiently.

Quick Check

Test your understanding of non-blocking sockets.

Recap

Blocking I/O is simple but ties up a thread per connection, which fails at scale. Non-blocking I/O returns immediately with EAGAIN instead of sleeping.

Polling sockets in a tight loop wastes CPU, so we use readiness notification. select/poll are O(n); epoll scales to many thousands of connections. Next we set epoll up.

Frequently asked questions

Is the “Blocking vs Non-Blocking I/O” lesson free?

Yes — the full text of “Blocking vs Non-Blocking I/O” is free to read here on the web, and the C Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C Academy course, upgrade to CoddyKit PRO.

What will I learn in “Blocking vs Non-Blocking I/O”?

Why event loops matter. You practise C Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start C Academy?

No prior experience is required. C Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Blocking vs Non-Blocking I/O” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this C Academy lesson?

Yes. Every C Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Blocking vs Non-Blocking I/O
  2. Setting Up epoll
  3. The Event Loop
  4. A Simple Echo Server
← Back to C Academy