I/Ointerruptsirqhandlercontextlatency

Interrupts: How Hardware Gets the CPU's Attention

A network card cannot call a function. It raises a line, and the CPU abandons what it was doing at the next instruction boundary. The handler's instruction count is the smallest part of what that costs.

Follow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
A device runs on its own clock and has no way to call your code — so how does it get the CPU to notice that something happened?
What you wrote
You call `read()` and it returns bytes. Or you register a callback and it "fires when data arrives". Either way something asynchronous became something sequential, and the mechanism is invisible.
What the hardware does
The device signals an interrupt controller, which prioritises it and raises it to a core. The core stops issuing new instructions from the current stream at an instruction boundary, saves a minimal architectural context, switches to a privileged level and jumps to a handler address from a vector table. When the handler returns, the original stream resumes — into a drained pipeline, a colder cache and a colder TLB.
Interrupts are the only mechanism that lets the outside world be timely without the CPU burning cycles asking "anything yet?". But engineers consistently price them by the handler's instruction count, which is the cheapest part. The expensive part is the state that gets thrown away and rebuilt around it — and that cost lands on whatever thread happened to be running, which is usually not the thread that cares about the I/O.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

The only way in

A CPU core executes one instruction stream. Nothing in that stream knows a packet arrived, a disk finished writing, or a timer expired. Those events happen on other clocks entirely, and the core has no way to observe them except by being *told*.

The telling is a physical signal. The device asserts an interrupt request; an interrupt controller decides whether this core should take it now, given what else is pending and what priority the core is currently masking. If it should, the core takes the interrupt at the next instruction boundary — not immediately, and not mid-instruction. In-flight work either retires or is discarded, which is the first hidden cost: an out-of-order core may have dozens of instructions in flight, and taking an interrupt means giving up on the speculative ones.

Then the hardware does the minimum handoff it can: save enough architectural state to resume, raise the privilege level, and jump to the address the vector table holds for that interrupt number. Everything after that is software. The OS decides how much work to do now and how much to defer, and that decision is the main lever anyone actually has (see Why Kernel Mode Is Actually Privileged for the mode transition itself).

raise requestprioritise, deliversave state, raise privilegeacknowledgerestore stateDevice (NIC, timer, SSD)Interrupt controllerCore: boundary reachedVector table → handlerHandler (privileged)Resume interrupted stream
UserLLMAgentToolDataDecisionHumanGuardrail

What an interrupt actually costs

Ask an engineer what an interrupt costs and you will usually get the handler's instruction count. That is the part you can see in source, and it is routinely the smallest term. The costs that dominate are the ones no code expresses.

The pipeline drains: speculative work in flight is discarded, and the front end restarts cold on the handler. The privilege transition has its own fixed cost. Then the handler executes — touching driver structures, device registers and queues that the interrupted thread had no reason to have cached. When control returns, the original thread resumes with its working set partially evicted and its TLB entries displaced. It runs slower for a while, and nothing about that shows up as "interrupt time" in any accounting.

The scale below is deliberately unitless, because the ratios are what transfer between machines. On one particular server the absolute numbers might be tens of nanoseconds; on an embedded core they will differ by an order of magnitude, and the balance between the terms shifts with pipeline depth and cache size.

Where the time in one interrupt actually goes. Relative only — the balance shifts with pipeline depth, cache size and how much the handler touches. — 1 unit ≈ the cost of one ordinary function call in warm cacheSIMPLIFIED
The handler's own instructions×1
Pipeline drain and restart×3
Privilege transition×3
Cache and TLB displaced by the handler×8
Scheduler work if the handler wakes a thread×12
Ratios, not times. Absolute latencies depend on the processor, its clock, the memory it is attached to and what else is running — publishing them would be wrong everywhere except one machine. The bars are log-scaled, so each step is larger than it looks.
The handler's own instructionsthe part you can read in the driver source
Pipeline drain and restartin-flight speculative work discarded; front end restarts cold
Privilege transitionfixed cost, and larger where mitigations flush predictors
Cache and TLB displaced by the handlerpaid *after* the handler returns, by whatever thread was running
Scheduler work if the handler wakes a threadonly when the interrupt has to make something runnable

Where the cost lands, and who pays it

The thread that suffers an interrupt is not the thread that wanted the I/O. It is whichever thread happened to be scheduled on that core. This is why interrupt-heavy machines show latency spikes on workloads that do no I/O at all, and why the spikes are so hard to attribute — the victim's profile shows time in its own code, just more of it, because it is now running against a colder cache.

Operating systems respond by splitting the work: do the absolute minimum in the interrupt context — acknowledge the device, note what happened — and defer the rest to a context that can be scheduled and accounted for. The taxonomy differs by OS, but the shape is universal: a small, urgent, uninterruptible piece and a larger, deferrable piece.

Which gives you the two real levers. Coalescing: tell the device to raise one interrupt for many events instead of one per event, trading latency for throughput. Affinity: steer a device's interrupts at specific cores so that latency-critical threads run somewhere else. Both are configuration, not code, which is why they are so often overlooked by the people writing the code that suffers.

The same event, handled in three contexts
ContextWhat runs thereCan it be preempted?Who pays
Interrupt (hard IRQ)Acknowledge the device, record the event, schedule the restNo — keep it tinyWhatever thread was on that core
Deferred (soft IRQ, tasklet, DPC)Protocol processing, buffer handlingPartly, and it is accounted separatelyStill that core, but interruptibly
Thread (kernel or user)Everything that can waitYes, fully scheduledCharged to a real thread you can see

Key points

  • An interrupt is taken at an instruction boundary, not immediately — in-flight speculative work is discarded first.
  • The handler's instruction count is the smallest component of what an interrupt costs.
  • The largest component is cache and TLB displacement, and it is paid *after* the handler returns.
  • The cost lands on whatever thread was running on that core, which is usually not the one doing I/O.
  • The two practical levers are coalescing (latency for throughput) and interrupt affinity (isolate latency-critical cores).

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Device → interrupt controller: the device asserts a request; the controller applies priority and masking to decide whether any core should take it now.
  2. 2
    Controller → core: the core accepts at the next instruction boundary; in-flight speculative instructions are squashed rather than completed.
  3. 3
    Core → vector table: hardware saves a minimal architectural context, raises the privilege level and jumps to the handler address registered for that vector.
  4. 4
    Handler → device: the driver acknowledges the device so it stops asserting, does the minimum, and defers the remainder to a schedulable context.
  5. 5
    Core → interrupted thread: state is restored and execution resumes — into a drained pipeline, a partially evicted cache and a displaced TLB.
What people conclude from this — wrongly
  • "The handler is 200 instructions, so an interrupt costs 200 instructions." It costs that plus a pipeline drain, a privilege transition and the cache the victim thread has to rebuild.
  • "Interrupt overhead shows up as interrupt time." Most of it shows up as the *interrupted* thread running slower afterwards, attributed to that thread's own code.
  • "The core is only 30% busy, so interrupts are not a problem." Utilisation counts the handler; it does not count the slowdown the handler leaves behind.
  • "Interrupts are a kernel concern." They set the tail latency of user-space threads that never issue a syscall.

Consequences, controls and cost

What it causes
  • • Threads doing no I/O show latency spikes on interrupt-heavy cores, with profiles that just show "more time in my own code".
  • • A core saturated by interrupts converts a large fraction of its cycles into overhead, so throughput falls while utilisation looks high.
  • • High packet or completion rates can make interrupt delivery itself the bottleneck, which is the situation polling exists to solve.
  • • Interrupt-driven wakeups add scheduling latency that is invisible to the code doing the waiting.
What you can do
  • • Enable interrupt coalescing on high-rate devices — one interrupt per batch instead of per event. This is the single biggest lever and it is a device setting, not a code change.
  • • Pin device interrupts away from latency-critical cores with IRQ affinity, so the threads that must not stutter never take them.
  • • Keep handler work minimal and push everything deferrable into a schedulable context, so the cost is at least attributable.
  • • At very high event rates, stop taking interrupts at all and poll instead — see [[polling-vs-interrupts]].
How to see it
  • • Per-core interrupt counts over time, split by source — an unbalanced distribution is usually accidental rather than designed.
  • • Time attributed to hard and deferred interrupt contexts per core.
  • • Run the same latency-sensitive thread on an interrupt-heavy core and a quiet one and compare the tail; the difference is the cost nothing else attributes.
  • • Cache miss counters on the victim thread before and after moving device interrupts elsewhere.
What it costs
  • • Coalescing raises latency for every event in the batch in exchange for far fewer interrupts — wrong for a latency-critical control path, right for bulk throughput.
  • • Interrupt affinity constrains the scheduler and can leave cores idle that could have helped, in exchange for predictability.
  • • Deferring work makes accounting honest but adds a scheduling hop, which is latency the interrupt context would not have paid.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • GENERALThe mechanism — asynchronous signal, instruction-boundary delivery, vectored handler, deferred work — is common to essentially all general-purpose CPUs.
  • PLATFORM-SPECIFICController design, vector table format, priority and masking rules differ substantially between x86-64 (APIC), AArch64 (GIC) and embedded designs. The deferred-work taxonomy is an OS choice, not a hardware one.
  • SIMPLIFIEDThe cost scale omits interrupt-remapping, virtualisation-induced exits, and the extra flushes some speculative-execution mitigations impose on privilege transitions.

Misconceptions

Claim
“An interrupt pauses the CPU mid-instruction.”
Reality
Delivery happens at an instruction boundary. On an out-of-order core, in-flight speculative instructions are discarded rather than completed, which is part of why the cost exceeds the handler.
Claim
“Interrupts only cost the operating system.”
Reality
The dominant cost is the cache and TLB state the handler displaces, which is paid by the interrupted user-space thread after control returns.
Claim
“More interrupts means the device is working harder, which is good.”
Reality
Past a certain rate the interrupts themselves consume the core. This is exactly the condition that makes polling faster, despite polling looking wasteful.

Where the rest of this lives

Concurrency & Parallelism
Asynchronous events and reentrancy

An interrupt handler runs in the middle of a thread that holds no lock and expects no concurrency, which is why handler code has such severe restrictions on what it may touch.