A compact handle often looks like an integer because an integer is cheap to store, copy, compare, and pass across an API boundary. In a table-backed resource manager, that integer may simply be an index into a slot array. The representation works until a slot is released and later reused. An old handle can then point at a new resource that happens to occupy the same index.

The failure is not an out-of-bounds access. The index can be perfectly valid. The problem is identity: the handle names a storage location, while the caller treats it as the identity of the resource that once occupied that location.

A generation counter adds a second identity component. The slot still supplies fast addressing, but the generation records which occupancy of that slot the handle belongs to.

Slot reuse creates a valid-looking stale handle

Consider a resource table with 1,024 slots. A handle contains slot index 37, and slot 37 currently stores connection A. When A closes, the allocator returns that slot to a free list. A later allocation places connection B in slot 37.

If the old handle is only the integer 37, a delayed operation for A now resolves to B. Range checks pass. The slot is occupied. Even a non-null check passes. Nothing in the index distinguishes the previous occupant from the current one.

This pattern appears in object pools, entity tables, descriptor tables, job registries, timer tables, and other systems that recycle bounded identifiers. The surrounding API may differ, but the identity error is the same whenever an address-like value outlives the resource instance it originally named.

Delaying reuse can reduce the chance of collision, but it does not establish an identity contract. A sufficiently old handle can survive beyond the delay.

A handle can carry index and generation

A generational table stores a counter beside each slot. Allocation returns both the slot index and the current counter value. Access succeeds only when the handle generation equals the generation stored in the slot.

A simplified representation is:

Handle {
    index: u32
    generation: u32
}

Slot {
    generation: u32
    value: Resource?
}

Lookup then has two independent checks:

slot = slots[handle.index]

if slot.value is empty:
    reject stale handle

if slot.generation != handle.generation:
    reject stale handle

return slot.value

When the resource is released, the table invalidates that occupancy. A common design increments the generation before the slot becomes available for a later allocation. The next handle for the same index therefore carries a different generation.

The index answers where to look. The generation answers whether the resource found there is the same occupancy named by the handle.

Counter advancement belongs to the reuse boundary

Generation updates need a precise place in the resource lifecycle. Incrementing the counter only after a new caller can observe the reused slot leaves a window in which an old handle may still match. The state transition should make the previous occupancy invalid before the slot is exposed as a new occupancy.

For a table protected by a mutex, the transition can be serialized inside the same critical section:

release(handle):
    lock table

    slot = slots[handle.index]
    require slot.generation == handle.generation

    destroy slot.value
    slot.value = empty
    slot.generation += 1
    free_list.push(handle.index)

    unlock table

The exact ordering can vary with the implementation, but publication matters. A caller must not receive a new handle until the slot state and generation associated with that handle are visible under the synchronization contract used by lookup.

A generation field does not provide that synchronization by itself. It detects an identity mismatch. Locks, atomics, ownership rules, or another concurrency mechanism still govern races on the table and the resource stored inside it.

Packing does not change the identity rule

APIs sometimes pack both fields into one machine word. A 64-bit handle might reserve the low 32 bits for the index and the high 32 bits for the generation:

raw = (u64(generation) << 32) | u64(index)

This keeps handles cheap to copy while retaining both identity components. It also permits equality comparison on the packed value when the bit layout is stable inside that API.

Packing is an encoding decision, not the safety mechanism. The protection comes from comparing the generation in the handle with authoritative generation state at lookup. A packed value that is decoded but never checked has the same stale-reference problem as a plain index.

Bit allocation also fixes limits. Fewer index bits cap the number of addressable slots; fewer generation bits make wraparound arrive sooner. Those limits should follow the resource population and reuse rate rather than an attractive bit split.

Wraparound defines a finite stale-handle horizon

A fixed-width generation counter eventually wraps. With an unsigned n-bit counter, a slot returns to the same generation value after 2^n generation advances for that slot.

At that point, a sufficiently old handle can become numerically identical to a current handle for the same index. Generation counters therefore do not create mathematically permanent uniqueness when their representation is finite.

The practical contract depends on counter width, maximum reuse rate, and the longest time a stale handle can remain reachable. A 32-bit generation has a far larger cycle than an 8-bit generation, but width alone is not a proof. Systems with unusually high churn or externally persisted handles need to account for the full lifetime model.

If stale handles can survive indefinitely, a wider counter only moves the boundary. Designs that require durable identity usually need an identifier whose reuse policy matches that requirement, rather than treating a small generational handle as a permanent object ID.

Validation must happen at every dereference boundary

A generation check is effective only where a handle becomes access to a resource. Checking once and then retaining an unchecked pointer can move the stale-reference window past the validation point.

For example, a lookup may validate a handle under a table lock and return a raw pointer. If another thread can release and recycle that slot immediately after the lock is dropped, the pointer’s lifetime now depends on a separate ownership rule. The generation check established identity at lookup time; it did not extend the resource lifetime.

That distinction separates two contracts:

  • identity validation rejects a handle whose slot has moved to another generation;
  • lifetime synchronization prevents the validated resource from disappearing while an operation still uses it.

Reference counting, scoped borrows, hazard-pointer schemes, epoch-based reclamation, locks, or explicit ownership transfer can provide lifetime rules, depending on the system. A generation counter can coexist with any of them, but it is not a substitute for them.

Generational handles make reuse observable

Recycling a slot is an implementation detail only while callers cannot confuse one occupancy with another. Once a caller can retain an identifier across release and reuse, the reuse policy becomes part of the API’s identity semantics.

Pairing an index with a generation makes that transition observable in a controlled form. The table can keep dense storage and cheap allocation while rejecting references to prior occupants. The cost is a larger handle, per-slot generation state, a comparison on access, and an explicit wraparound policy.

That trade is most useful when resources are intentionally recycled and stale references are plausible. The generation does not make the resource immortal and does not serialize concurrent access. It gives the access boundary enough information to tell a current resource from an earlier occupant of the same slot.