Photo by Alexandre Debiève on Unsplash
Polling vs Interrupts: When to Check vs When to Wait
Every system that responds to external events faces a fundamental choice: should it continuously check whether something has happened, or should it wait to be notified? This polling-versus-interrupt decision shapes everything from CPU architecture to network drivers to application-level code, and the tradeoffs involved explain many performance characteristics of modern systems.
The Basic Tradeoff
Polling means repeatedly checking a status flag or reading from a source to see if data has arrived or an event has occurred. A network card driver might read a register in a tight loop, checking if a packet has arrived. The advantage is immediate response: the instant data appears, the next iteration of the loop finds it. The disadvantage is waste: if nothing is happening, the CPU burns cycles checking and rechecking.
Interrupts flip the model. The hardware or subsystem signals the CPU when something requires attention, and the processor stops what it’s doing to handle it. This saves power and CPU time when events are infrequent, but introduces overhead: saving context, jumping to the interrupt handler, and restoring state all cost time. If events arrive rapidly, this per-event overhead becomes expensive.
Where Latency Demands Polling
High-frequency trading systems, real-time data acquisition, and low-latency networking often choose polling despite its CPU cost. When a network interface receives thousands of packets per second, the time spent entering and exiting interrupt handlers adds up quickly. Polling the network card in a tight loop eliminates this overhead, delivering microseconds of latency improvement.
Linux’s NAPI (New API) networking subsystem uses a hybrid approach: interrupts trigger a switch to polling mode. When packet rates are low, interrupts provide efficiency. When a burst arrives, the driver disables interrupts and polls until the queue drains, then re-enables interrupts. This adapts to load, getting low latency under pressure without wasting cycles when idle.
Modern NVMe drivers employ similar techniques. At very high IOPS, polling the completion queue beats waiting for interrupts. The io_poll flag in io_uring lets applications explicitly choose polling for specific operations, trading CPU time for nanoseconds of latency.
When Interrupts Win
Mobile devices, embedded systems, and energy-constrained environments lean heavily on interrupts. A smartphone waiting for a touch event can’t afford to spin a CPU core in a polling loop, draining the battery. Interrupts let the processor enter deep sleep states, waking only when something genuinely needs attention.
Even in data centers, interrupts often make sense. Most web servers don’t need single-digit-microsecond latency, and burning entire CPU cores to poll sockets would reduce the capacity available for application logic. The overhead of interrupt handling is real but manageable, and the CPU time saved can be used elsewhere.
Interrupt coalescing adds another dimension: instead of firing an interrupt per event, hardware can batch them, signaling once after N events or after a timeout. Network cards and storage controllers tune these parameters to balance latency and throughput, smoothing out the interrupt storm that would otherwise accompany high event rates.
The Hybrid Middle Ground
Busy-wait loops with backoff represent a middle path. An application might poll for a short window, then yield or sleep if nothing happens. This captures the low latency of polling for hot events while degrading gracefully when idle. The challenge lies in tuning: poll too briefly and you lose the latency benefit; poll too long and you’re back to wasting CPU.
Adaptive polling adjusts based on observed behavior. If events consistently arrive quickly, extend the polling window. If the system sits idle, shrink it or switch to blocking mode. This requires runtime measurement and heuristics, adding complexity but handling diverse workloads more gracefully.
Architectural Echoes
The polling-interrupt tradeoff appears at every layer. Databases must decide whether to check for query cancellations on every loop iteration or rely on signal handlers. Message queues choose between blocking reads and tight receive loops. Even user-facing code confronts this: should a UI framework poll for events at 60 FPS or wait for the OS to signal input?
The correct choice depends on event frequency, latency requirements, and resource constraints. Systems that handle one event per second have no business polling. Systems that process millions of events per second and need every microsecond often have no choice but to poll. In between lies a spectrum where hybrid approaches, adaptive strategies, and careful measurement determine the right balance.
Understanding this tradeoff clarifies why performance tuning so often involves flags like SO_BUSY_POLL, kernel parameters controlling interrupt coalescing, and application-level options to switch between blocking and non-blocking modes. The decision between checking and waiting isn’t an implementation detail—it’s a fundamental architectural choice with cascading implications for latency, throughput, and efficiency.