Photo by Laura Ockel on Unsplash

NUMA: Why Memory Location Matters on Modern Servers


Modern multi-socket servers don’t work the way most developers imagine. When you have two CPUs in a server, each with its own attached memory, you’re running on a Non-Uniform Memory Access (NUMA) architecture. That architectural detail has real performance consequences.

How NUMA Works

In a traditional Uniform Memory Access system, every CPU core accesses memory through a single shared bus. Performance is predictable but limited by bus contention. NUMA solves the scaling problem by giving each CPU socket its own local memory controller and RAM banks.

A CPU can still access memory attached to another socket—this is called remote memory access—but the path goes through an interconnect like Intel’s Ultra Path Interconnect (UPI) or AMD’s Infinity Fabric. Remote access latency is typically 1.5 to 2.5 times higher than local access, and bandwidth is constrained by interconnect limits rather than memory controller speed.

The operating system and hardware expose topology information so processes can be scheduled on the same NUMA node as their data. Each node is essentially a complete CPU-memory subsystem that happens to be connected to other nodes in the same physical machine.

Why It Matters for Applications

Database performance is the most obvious victim of poor NUMA placement. If a Postgres backend process runs on socket 0 but its shared buffer pool lives in socket 1’s memory, every query pays a remote access penalty. Redis and other in-memory stores show similar sensitivity—a misconfigured NUMA policy can cut throughput by 30% or more.

Application servers with large heaps face the same problem. If a Java or Go process allocates memory across multiple nodes and the scheduler migrates threads between sockets, you end up with threads constantly accessing remote memory. Garbage collection becomes especially expensive when the collector has to chase pointers across interconnects.

Network-intensive workloads benefit from NUMA awareness too. Modern NICs support multiple queues that can be bound to specific CPU cores. Keeping the interrupt handling, kernel network stack processing, and application logic all on the same NUMA node eliminates cross-socket traffic for packet processing.

The Kernel’s NUMA Balancing

Linux includes automatic NUMA balancing that tries to migrate pages closer to the threads accessing them. The kernel tracks page faults, identifies hot pages being accessed remotely, and migrates them to the local node. It also tries to move tasks to the node where their memory resides.

This works reasonably well for general-purpose workloads, but the heuristics can’t always predict access patterns correctly. High-churn workloads may trigger constant migration overhead. For performance-critical services, explicit NUMA policies usually beat the automatic approach.

Configuring for NUMA

The numactl command lets you bind processes to specific nodes and control memory allocation policy. The local allocation policy keeps memory on the same node where the process runs. The preferred policy tries one node first but falls back to others under pressure. Interleave spreads allocations round-robin across nodes—useful for bandwidth-bound workloads but rarely for latency-sensitive ones.

Most databases expose NUMA configuration. Postgres benefits from interleaving shared memory across nodes while keeping backends local. MySQL and MongoDB have similar tuning knobs. Misconfigurations are common: default installs often ignore NUMA entirely, leaving performance on the table.

Container runtimes complicate NUMA management. If you run a database in a container without pinning, the kernel may migrate it between nodes freely. CPU pinning alone isn’t enough—you need to pin both the CPUs and constrain memory allocation to matching nodes. Kubernetes added topology manager support to handle this correctly, but it requires explicit configuration.

When NUMA Doesn’t Matter

Single-socket systems don’t have remote memory, so NUMA is irrelevant. Cloud instances—even large ones—typically present a single NUMA node to the guest because the hypervisor abstracts the underlying hardware topology. Checking /sys/devices/system/node/ will show whether your system has multiple nodes.

Workloads with small working sets that fit in cache see minimal NUMA impact. The latency difference matters when you’re actually hitting DRAM, not when everything stays in L3.

Observability

The numastat command shows per-node memory allocation and hit/miss ratios. High remote access counts indicate poor placement. The numa_maps file in /proc/<pid>/ shows exactly where a process’s memory lives. Performance monitoring tools like perf can break down memory access by node and quantify the remote access penalty for specific workloads.

Understanding NUMA is essential for anyone running performance-sensitive workloads on multi-socket hardware. The architecture isn’t going away—server CPUs keep adding cores, and NUMA is how vendors scale memory bandwidth to feed them. Ignoring locality in a two-socket server means leaving 30-50% of your hardware’s capability unused.