Async & Non-Blocking I/O
Processing I/O without blocking the thread, keeping CPU utilisation high.
Overview
Asynchronous I/O allows a process to initiate an I/O operation (disk read, network request) and continue executing other work while waiting for the result, rather than blocking the thread. Node.js is built on non-blocking I/O via libuv's event loop; Ruby has async-io via the Async gem and Fibers; Go uses goroutines with channel-based I/O. The key benefit is handling thousands of concurrent I/O operations on a single thread without thread-per-connection overhead.
Origin
Non-blocking I/O has roots in Unix select() and poll() system calls (1970s-1980s). epoll (Linux 2.5.44, 2002) and kqueue (FreeBSD, 2000) provided efficient scalable I/O multiplexing. Node.js (Ryan Dahl, 2009) built a JavaScript runtime on non-blocking I/O as its core design. The C10K problem (Dan Kegel, 1999) articulated the need to handle 10,000 concurrent connections, motivating non-blocking I/O research.
Examples
Concurrent I/O with Promise.all vs sequential in TypeScript
interface ProductDetails {
product: { id: string; name: string };
inventory: { qty: number };
reviews: { avgRating: number; count: number };
}
// SEQUENTIAL: total time = t_product + t_inventory + t_reviews
async function getProductDetailsSequential(id: string): Promise<ProductDetails> {
const product = await fetchProduct(id); // ~50ms
const inventory = await fetchInventory(id); // ~30ms
const reviews = await fetchReviews(id); // ~40ms
return { product, inventory, reviews }; // total ~120ms
}
// CONCURRENT: total time = max(t_product, t_inventory, t_reviews)
async function getProductDetailsConcurrent(id: string): Promise<ProductDetails> {
const [product, inventory, reviews] = await Promise.all([
fetchProduct(id), // ~50ms
fetchInventory(id), // ~30ms (runs concurrently)
fetchReviews(id), // ~40ms (runs concurrently)
]);
return { product, inventory, reviews }; // total ~50ms
}
// CONCURRENT with independent error handling
async function getProductDetailsResilient(id: string) {
const [product, inventory, reviews] = await Promise.allSettled([
fetchProduct(id),
fetchInventory(id),
fetchReviews(id),
]);
return {
product: product.status === 'fulfilled' ? product.value : null,
inventory: inventory.status === 'fulfilled' ? inventory.value : null,
reviews: reviews.status === 'fulfilled' ? reviews.value : null,
};
}The concurrent version runs all three fetches simultaneously. On a 120ms total with sequential execution, concurrent reduces wall-clock time to 50ms, a 2.4x speedup for this pattern. Promise.allSettled allows partial results when individual sources can fail independently.
Async streams with the Async gem in Ruby
require 'async'
require 'async/http/internet'
# Sequential: each request waits for the previous
def fetch_prices_sequential(product_ids)
internet = Async::HTTP::Internet.new
product_ids.map do |id|
response = internet.get("https://pricing.api/products/#{id}")
JSON.parse(response.read)
end
end
# Concurrent: all requests in-flight simultaneously using Async tasks
def fetch_prices_concurrent(product_ids)
Async do
internet = Async::HTTP::Internet.new
tasks = product_ids.map do |id|
Async do
response = internet.get("https://pricing.api/products/#{id}")
JSON.parse(response.read)
end
end
tasks.map(&:wait)
end.wait
end
# With timeout per task
def fetch_prices_with_timeout(product_ids, timeout: 5)
Async do
internet = Async::HTTP::Internet.new
product_ids.map do |id|
Async do
Async::Task.current.with_timeout(timeout) do
response = internet.get("https://pricing.api/products/#{id}")
JSON.parse(response.read)
end
rescue Async::TimeoutError
nil
end
end.map(&:wait)
end.wait
endThe Async gem (Samuel Williams, 2018) uses Ruby Fibers to implement cooperative concurrency on top of the event loop. Unlike threads, Fibers yield control explicitly at I/O boundaries; the Async::HTTP client automatically yields when waiting for network responses.
Use Cases
- 01API aggregation layers that call multiple downstream services and merge results; concurrent I/O reduces latency from sum of calls to max of calls
- 02Web scrapers and bulk data fetchers that make many HTTP requests; async I/O allows hundreds of in-flight requests on one thread
- 03Node.js web servers handling thousands of simultaneous connections where thread-per-connection model would exhaust memory
- 04Real-time messaging servers (WebSocket, SSE) where thousands of clients maintain idle connections waiting for push notifications
When Not to Use
- //Do not use async I/O for CPU-bound work; async I/O is effective when the bottleneck is network/disk wait time, not computation. CPU work blocks the event loop regardless of async syntax
- //Do not nest too many awaits in series where parallelisation is possible; sequential async code does not improve on synchronous code in terms of throughput
- //Do not assume async is always faster; for a single sequential database query, async adds overhead (Promise allocation, microtask scheduling) without parallelism benefit
Technical Notes
- libuv (the C library underlying Node.js) uses epoll on Linux, kqueue on macOS/BSD, and IOCP on Windows for efficient I/O multiplexing. File system operations in Node.js are handled by a thread pool (default 4 threads, UV_THREADPOOL_SIZE env var controls size) because most OS file APIs are synchronous
- The Node.js event loop has six phases: timers (setTimeout, setInterval), pending callbacks (I/O errors), idle/prepare, poll (I/O events, the main phase), check (setImmediate), close callbacks. process.nextTick callbacks run between every phase, before the next loop iteration
- Ruby 3 introduced Fiber Scheduler interface (Bruno Sutic, 2021) allowing Gems to hook into the event loop; Async gem implements this interface. This means standard library IO (Net::HTTP, File) can be made non-blocking without modifying call sites
- Go's goroutines are scheduled cooperatively by the Go runtime at function call boundaries and preemptively since Go 1.14. The runtime's scheduler (GOMAXPROCS threads) multiplexes thousands of goroutines onto a small number of OS threads, achieving M:N threading
More in Performance