Input/Output: Talking to the World

Table of Contents

  1. What Is I/O?
  2. I/O Is Slow
  3. How Devices Get the CPU's Attention
  4. Blocking I/O
  5. Non-Blocking I/O and Async Code
  6. The Event Loop, Briefly
  7. Standard Input and Output
  8. The Network Is Just More I/O
  9. Try It Yourself

1. What Is I/O?

I/O stands for Input/Output: any time data moves between the CPU and RAM on one side, and anything else on the other.

  • Input: keyboard, mouse, touchscreen, microphone, camera, reading a file, receiving data from the network
  • Output: drawing to the screen, playing sound, writing a file, sending data over the network

A program that did no I/O at all would be useless — it could calculate things, but it could never receive a question or show you the answer. Every useful program is, at heart, a loop of: get input → process → produce output, just like the model from chapter 1.

2. I/O Is Slow

Go back to the table from chapter 4. If reading a register took 1 second, reading from an SSD took days, and a network request across the world took years.

Now add the slowest device of all: the human. You might take a second to click a button. For a CPU running billions of instructions per second, that's an eternity.

So most of a computer's time isn't spent calculating. It's spent waiting — for the disk, for the network, for the user. That's why you saw most processes sitting at 0% CPU in chapter 7.

This leads to the central question of this chapter: what should a program do while it waits?

3. How Devices Get the CPU's Attention

One way to wait would be for the CPU to keep checking: "Has a key been pressed? No. Has a key been pressed? No…" — millions of times a second. This is called polling, and it wastes nearly all of the CPU's time.

Instead, devices use interrupts. When something happens — a key is pressed, a network packet arrives, the disk finishes reading — the device sends an electrical signal to the CPU. The CPU briefly pauses what it's doing, runs a small piece of kernel code (usually from the device's driver) to handle the event, and then goes back to its work.

It's the difference between staring at your phone waiting for a message and simply letting it buzz when one arrives.

Interrupts are also how the scheduler (chapter 7) works: a hardware timer interrupts the CPU every few milliseconds, giving the kernel the chance to switch to another thread.

4. Blocking I/O

The simplest way to write I/O code is blocking: when your program asks to read a file, it simply stops and waits until the data is ready.

// Blocking: nothing else happens until the whole file is read
const data = fs.readFileSync("big-video.mp4");
console.log("Done!");

This is easy to understand — the code runs top to bottom, in order. While the program waits, the OS puts its thread in the waiting state and gives the CPU to someone else, so the computer as a whole isn't wasted.

The problem is your program's time. That thread can't do anything else while it waits. If it's the UI thread, the app freezes (chapter 8). If it's a web server handling one request per thread, every waiting request ties up a whole thread. A server with 100 threads, each waiting half a second for the database, can handle only about 200 requests per second — even though its CPU is mostly idle.

5. Non-Blocking I/O and Async Code

The alternative is non-blocking (or asynchronous) I/O. Instead of waiting, your program says to the OS: "Start reading this file, and let me know when it's done." Then it immediately carries on with other work.

// Non-blocking: start the read, then keep going
fs.readFile("big-video.mp4", (err, data) => {
  console.log("Done!"); // runs later, when the data is ready
});
console.log("Started reading…"); // runs immediately

Here, "Started reading…" is printed first, even though it's written second. The callback function runs later, when the OS reports that the read has finished.

In modern JavaScript you'll usually write the same idea with async/await, which looks like blocking code but doesn't block:

const data = await fs.promises.readFile("big-video.mp4");
console.log("Done!");

await means "pause this function until the data is ready — but let everything else keep running in the meantime."

The benefit is huge: a single thread can have thousands of I/O operations in flight at once, because it never sits still waiting for any one of them.

6. The Event Loop, Briefly

How does one thread keep track of all those unfinished operations? JavaScript (in browsers and in Node.js) uses an event loop. In simplified form, it's a loop that runs forever:

  1. Run the code that's ready to run.
  2. When code starts an I/O operation, hand it to the OS and move on.
  3. When the OS reports an operation is finished, add its callback to a queue.
  4. When the current code is done, take the next callback from the queue and run it.
  5. Repeat.

That's how a single-threaded Node.js server can serve many users at once: while it waits for one user's database query, it handles another user's request. It's also why, in JavaScript, one slow calculation blocks everything — the loop can't move on until the current code finishes.

You'll learn the event loop in depth in JavaScript & TypeScript Fundamentals. For now, the big idea is enough: async code exists because I/O is slow, and waiting is wasteful.

7. Standard Input and Output

Every process starts with three ready-made I/O channels, called the standard streams:

StreamNumberDefault
stdin (standard input)0The keyboard
stdout (standard output)1The terminal screen
stderr (standard error)2The terminal screen, for error messages

When your program calls console.log() (JavaScript) or print() (Python), it's writing to stdout. Error messages go to stderr, so they can be kept separate from normal output.

The powerful part is that these streams can be redirected. Instead of the screen, stdout can go into a file, or into another program. This is the basis of much of the command line — and it's how Docker collects your container's logs: docker logs simply shows what the process wrote to stdout and stderr. You'll use this constantly in Command Line Basics.

8. The Network Is Just More I/O

To a program, talking over the network looks a lot like reading and writing a file: open a connection, write some bytes, read some bytes, close it. The operating system provides a socket — an endpoint for a network connection — and the network card and its driver handle the actual sending.

The difference is speed and reliability. The network is the slowest level of the memory hierarchy, and unlike a local disk, the other side might be slow, busy, or not there at all. That's why network code needs timeouts and error handling, and why making fewer network requests is one of the most effective ways to make an app faster.

How requests actually travel across the internet — IP addresses, DNS, ports, and HTTP — is covered in How the Web Works and Networking for Developers.

9. Try It Yourself

  1. Watch I/O happen.

    • Windows: Task Manager → Performance → Disk and Ethernet/Wi-Fi.
    • macOS: Activity Monitor → Disk and Network tabs.
    • Linux: run iotop (may need installing) for disk, or check your system monitor's network graph.

    Now copy a large file, or download something big. Watch disk or network activity spike while CPU usage stays low — the computer is mostly waiting.

  2. Feel the difference in speed. Open an app you use often, close it, then open it again. The second launch is usually faster — part of it was still cached in RAM, so less had to be read from storage.

  3. Spot async in the wild. Notice how a web page shows its layout first and then images pop in one by one. The page didn't block while waiting for each image — it kept going and filled them in as they arrived.

You now have every piece: bytes, the CPU, memory, files, the OS, processes, threads, and I/O. In the final chapter, we'll put them all together.

Buy Me a Coffee

Found this tutorial helpful? Consider buying me a coffee to support more free content!