IRQ Affinity and /proc/irq/N/smp_affinity: Pinning Interrupts to Specific Cores

2026-08-28

By default, Linux distributes hardware interrupts across CPUs using the irqbalance daemon, which reshuffles IRQ affinity every 10 seconds based on load. This is fine for a web server, but disastrous for a low-latency trading application or a packet-processing workload where you want interrupts to land on predictable cores.

Each IRQ line has a bitmask at /proc/irq/N/smp_affinity (or smp_affinity_list for a friendlier format). Writing to it tells the APIC which cores are eligible to receive that interrupt. The kernel picks one, and — importantly — the top-half handler, the softirq, and often the driver's kthread all run on that core.

Concrete example: On a machine with a Mellanox ConnectX-6 NIC on NUMA node 0 (cores 0-15), you'd first stop irqbalance (or add IRQBALANCE_BANNED_CPUS to exclude your target cores), then:

for irq in $(grep mlx5 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
  echo $((cpu++)) > /proc/irq/$irq/smp_affinity_list
done

Combined with isolcpus=8-15 on the kernel command line and pinning your worker threads with taskset -c 8, you eliminate the biggest source of jitter: interrupt migrations.

The rule of thumb: IRQ core, softirq core, user-thread core, and packet-memory NUMA node should all be the same. Every mismatch costs you either a cache miss (~30 ns local, ~120 ns remote-socket) or a scheduler wakeup (~2 μs). For 10M pps, saving 100 ns per packet is 1 full core of headroom.

Gotcha: Some drivers ignore your affinity mask because MSI-X routing is a hint, not a contract — the APIC can deliver to any core in the mask. And per-CPU IRQs (LOC, RES, IPI) can't be moved at all; they're inherent to each core.

Key Takeaway: For latency-critical workloads, disable irqbalance and manually pin each device IRQ to the same core as its user-space consumer, so packet data, softirqs, and processing all stay in one core's cache.

All newsletters