CPU-bound vs IO-bound
BuddingMost performance advice is really about one prior question: is your program CPU-bound or IO-bound? The answer decides which resource you are fighting, which concurrency model fits, and which languages will feel effortless versus which will fight you the whole way.
Telling them apart
- CPU-bound work spends its time computing: number crunching, transformations, encryption, image and video encoding. It is limited by how fast the processor can execute instructions - clock speed, core count, cache locality, vectorization. You make it faster with more or faster cores and data-parallel code.
- IO-bound work spends its time waiting: disk reads and writes, network calls, database queries. The CPU sits mostly idle while the latency of the external operation dominates. You make it faster by overlapping the waiting - non-blocking IO and many lightweight concurrent tasks.
A quick test: if pinning the CPU at 100% is what makes the job finish sooner, you are CPU-bound; if the CPU idles while you wait on something else, you are IO-bound.
Why it dictates the concurrency model
The bottleneck picks the tool.
- CPU-bound work wants parallelism - real threads on real cores, or SIMD and GPU offload - to do more computation at once. An event loop buys nothing when the CPU is already the limit.
- IO-bound work wants cheap concurrency - a way to keep thousands of waits in flight without burning a thread on each. Thread-per-connection collapses under context-switch cost; event loops and lightweight tasks (goroutines, coroutines, async/await) shine.
Confusing the two is the classic mistake: throwing async at a CPU-bound loop, or spawning a thread per socket for tens of thousands of idle connections.
Where the languages fall
The workload maps straight onto language strengths:
- C, C++, Rust own CPU-bound work - compiled to the metal, manual or zero-cost memory control, aggressive optimization. Rust pairs that with a strong async story (Tokio) for IO too.
- Java and C# are capable all-rounders - JIT-optimized hot loops for compute, mature async frameworks for IO - at the cost of GC tuning and warm-up time.
- Go is built for IO concurrency: goroutines and a non-blocking net stack make massive concurrency trivial with almost no ceremony. It is competent but not a leader at raw CPU-bound number crunching, where the garbage collector and scheduler start to show.
- Python fits IO-bound web work well (
asyncio, FastAPI), but the GIL and interpreter make it a poor choice for raw compute unless you drop into C-extensions (NumPy) or multiprocessing. - Node.js is superb at IO - its whole API is non-blocking by design - and a poor choice for heavy computation, which blocks its single event-loop thread until you offload to worker threads.
The bridge
“CPU-bound or IO-bound” is the question sitting underneath the entire concurrency conversation - threads versus coroutines, why Go’s goroutines feel free, why an event loop can stall on one bad function. That machinery is its own topic: see threads_vs_coroutines.
Related
- Latency vs Throughput vs IOPS: why your fast API still fails at scale - the metrics your workload optimizes
- Queues Are Everywhere - where IO waits pile up
- Performance & Latency MOC - the map these notes hang from