Why architecture matters here

The architecture matters because it decouples when a node is unlinked from when its memory is freed, and that decoupling is the only thing that makes deletion safe without a lock. In a lock-free structure those two events cannot be atomic with respect to readers: a reader can hold a raw pointer to a node in the microscopic window between the writer's unlink and the writer's free. Hazard pointers insert a provable gap between the two — a node is only freed once the system can demonstrate that no reader's announced pointer references it — and that proof is the entire safety argument.

It matters because the alternatives all sacrifice the properties you chose lock-free code to get. Reference counting puts a shared atomic counter on every node and forces every reader to increment and decrement it, turning the read path — usually the hot path — into a cache-line ping-pong that scales negatively with core count. Epoch-based reclamation is faster but can defer freeing unboundedly if a single thread stalls inside a critical region, letting memory balloon. Hazard pointers occupy a deliberate middle ground: slightly more read-path work than epochs, but hard bounds on outstanding memory and no dependence on every thread making progress.

It matters because the guarantee is worst-case, not average-case. A hazard pointer bounds the number of un-reclaimed nodes to roughly the number of threads times the number of hazard slots per thread plus the retirement batch size — a small constant. That means a paused, preempted, or slow reader can delay the freeing of the specific nodes it has pinned, but it cannot delay anything else and cannot cause unbounded growth. For latency-sensitive systems that cannot tolerate a GC pause and cannot tolerate memory blowup, that bound is the whole reason to reach for hazard pointers over the alternatives.

It matters because the correctness hinges on memory ordering that is easy to get subtly wrong. The reader must publish its hazard pointer and then re-validate that the shared pointer still points where it thought — with the right fences — because a store that the CPU or compiler reorders after the dereference opens exactly the race the scheme exists to close. Understanding the architecture is understanding why the protect-then-recheck handshake is mandatory and why a single relaxed load in the wrong place reintroduces the use-after-free.

Finally it matters because hazard pointers are becoming infrastructure, not a research curiosity. They underpin production lock-free hash maps, queues, and RCU-like read paths, and C++26 standardized std::hazard_pointer, giving the technique a portable, well-specified home. Once you understand the model, you can reason about the memory safety of any lock-free container that uses it, and you can spot the designs that get it wrong.

Advertisement

The architecture: every piece explained

Start with the hazard slot, the atom of the whole scheme. Each thread owns a small fixed array of pointer-sized slots — often one or two, occasionally a handful — that only that thread writes and that any thread may read. A slot holds either null or the address of a node the owning thread is currently protecting. Because each slot has exactly one writer, publishing a hazard pointer is a single store with release semantics, not a contended atomic read-modify-write. That single-writer property is what keeps the read path cheap.

The reader's protect-then-validate handshake is the load-bearing sequence. The reader loads the shared pointer to the node it wants (say the head of a queue), writes that address into its hazard slot, and then re-reads the shared pointer to confirm it still holds the same value. If it changed, another thread swapped the node out between the two loads, the reader's announcement is stale, and it must loop and retry. Only when the re-read matches is the node safely pinned — because from that instant any writer scanning hazard slots will see the reader's announcement and refuse to free the node.

On the writer side sits the retire list, a per-thread (or sharded) list of nodes that have been logically unlinked from the structure but not yet physically freed. Retiring a node is cheap: unlink it with the usual lock-free compare-and-swap, then push its pointer onto the local retire list and return. No scanning, no freeing, no coordination happens on the unlink path itself, which keeps writers fast. The retire list is the buffer where unsafe-to-free nodes wait out the readers that might still hold them.

The reclamation scan is where memory is actually returned. When a thread's retire list grows past a threshold, it runs a pass: it gathers a snapshot of every thread's hazard slots into a set, then walks its retire list and, for each retired node, checks whether that node's address is in the hazard set. A node absent from the set is announced by no one and is freed; a node present in the set is still pinned by some reader and is kept on the list for the next pass. Batching the scan — reclaiming many nodes per hazard-slot snapshot — amortizes the cost of gathering all those slots.

Two supporting details make it robust. First, the number of hazard slots per thread caps how many nodes a single operation can pin at once, which is what bounds outstanding memory; a traversal that needs to hold three nodes simultaneously needs three slots. Second, the memory ordering must be honored on both sides: the reader's slot store must be visible before its dereference, and the writer's scan must observe slots with acquire semantics so it cannot miss a just-published announcement. Get those fences right and the retire-list-plus-scan machinery delivers safe reclamation with the memory bound the design promises.

Hazard pointers — a reader publishes the node it is about to touch so no other thread frees it out from under the readerReader threadreads shared pointer, wants node NHazard slotreader stores N into its own slotRe-validatestill the same pointer? use NWriter threadunlinks N from the structureRetire listN parked, not freed yetScan hazard slotsis N pinned by anyone?Not pinned -> free(N)safe to reclaim memoryPinned -> keep on retire listretry on next reclaim passBounded memory — each thread has K slots, retire list drains as readers move on; no reference counts on the hot pathpincheckunlinkscanclearsetdefer
A reader announces the node it is about to dereference by writing that node's address into a per-thread hazard slot, then re-reads the shared pointer to confirm nothing changed underneath it. A writer that unlinks a node does not free it immediately; it parks the node on a retire list and, on a later reclamation pass, scans every thread's hazard slots. A retired node whose address appears in no slot is provably untouchable by any reader and can be freed; one that is still pinned stays parked and is retried later. The result is safe reclamation with bounded, predictable memory and no atomic reference count on the read path.
Advertisement

End-to-end flow

Walk a read racing a delete on a lock-free stack. Thread R wants to read the top node. It loads the top pointer, sees node N, and writes N's address into its hazard slot with a release store. It then re-loads top: still N. The announcement is validated, so R now safely dereferences N, reads its value, and follows N's next pointer if it is traversing further. Throughout, R holds no lock and has performed no atomic read-modify-write — just two loads and one store.

Meanwhile thread W decides to pop the stack. It compare-and-swaps top from N to N's successor, succeeding — so N is now unlinked and no future reader can reach it through the structure. But R may still be inside N right now. So W does not free N. It pushes N onto its retire list and returns immediately, leaving the reclamation for later. The unlink is complete; the free is deferred.

Some time later W's retire list crosses its threshold and W runs a reclamation scan. It reads every thread's hazard slots into a set and finds R's slot still holding N's address — because R has not finished with N yet. So when W checks N against the hazard set, it finds a match and leaves N parked on the retire list. N's memory is not freed. This is the crucial moment: the scan saw R's announcement and honored it, and the use-after-free is averted purely because R announced N before touching it.

Now R finishes. It clears its hazard slot back to null, publishing that it no longer cares about N. On W's next reclamation pass, the hazard set no longer contains N's address; the check finds no match; W frees N and reclaims the memory. The node lived a little longer than its unlink, exactly long enough to outlast the one reader that had pinned it, and not a moment more than necessary.

Contrast the ordering failure that this dance prevents. Suppose R had dereferenced N first and only then written the hazard slot, or the store had been reordered after the dereference by a weakly ordered CPU. Then W's scan could run in the gap, see an empty slot, conclude N is unused, and free it — while R is mid-dereference. The re-validation after publishing, plus the release/acquire fences, are precisely what closes that window: R cannot legitimately use N until it has published the pin and confirmed the pointer is unchanged, and W cannot free N until it has observed the absence of that pin.