2026-06-19
You've built a lock-free queue. Threads can dequeue nodes without locks. But when do you actually free a dequeued node? If another thread is still reading it, you've got a use-after-free. Hazard pointers solve this, but they require every reader to publish what it's touching on every access — that's a store-load fence per pointer dereference. Epoch-based reclamation (EBR) trades per-access overhead for batched, cheap reclamation.
The core idea: Time is divided into epochs — a global counter that periodically advances. Before a thread touches shared memory, it enters the current epoch (writes the epoch number to a thread-local slot). When it's done, it exits. Retired memory is tagged with the epoch in which it was retired. Memory tagged with epoch e can be freed once no thread is still in epoch e or earlier.
The advancement rule: The global epoch can advance from e to e+1 only when every active thread is in epoch e. Since threads always enter the latest epoch, this means once advanced, no new entries to e-1 are possible. After two advancements, anything retired in epoch e is safe to free.
Concrete example: Crossbeam (Rust's concurrency library) uses EBR for its lock-free queue and skip list. When you call guard.defer_destroy(ptr), the pointer goes into a per-thread retirement list tagged with the current epoch. Every ~128 operations, the thread tries to advance the global epoch by scanning thread slots. When the epoch advances twice, the oldest retirement batch is freed. ConcurrentHashMap implementations in Java use a similar approach via the JVM's safepoint mechanism.
Rule of thumb: EBR's reclamation lag is typically 2–3 epoch transitions. If your global epoch advances every 100 operations across 8 threads, expect retired memory to sit for ~300 ops × 8 threads = ~2,400 operations before being freed. Budget 2–10× peak retirement rate as your steady-state memory overhead.
The catch: A thread that enters an epoch and then blocks (page fault, preempted, stuck on I/O) pins the global epoch. All retired memory after that point accumulates indefinitely. This is why EBR works beautifully for short, bounded critical sections — lock-free data structure operations that complete in microseconds — and falls apart for long-held references. If you might block, use hazard pointers or RCU's QSBR variant instead.
When to reach for EBR: Short critical sections, high throughput, many readers, you control the access pattern. When not to: Code paths that might block, irregular access, or where memory ceiling is hard.
