Why does a full BPF hash map reject new keys with E2BIG, while updating an existing key still succeed?
Short answer: For BPF_MAP_TYPE_HASH, max_entries is a hard cap on distinct keys, and the cap is enforced at element allocation time: a new key needs a fresh element, and when no element is available the update returns E2BIG (documented: "the number of elements in the map reached the max_entries limit"). An update of an existing key does not need a new element: the preallocated path swaps it with a per-CPU spare slot, and the lazy path explicitly permits allocation "when map is full and update() is replacing old element, it's ok to allocate, since old element will be freed immediately" — so both keep working at capacity. BPF_MAP_TYPE_LRU_HASH does not fail at capacity the same way: it evicts the least recently used entry to make room instead of returning E2BIG, which is why a key you never deleted can suddenly read back NULL.
Where the E2BIG comes from
The uapi contract is explicit. The bpf_map_update_elem(2) man page (generated from include/uapi/linux/bpf.h) lists E2BIG with the description "the number of elements in the map reached the max_entries limit specified at map creation time", alongside EEXIST (when BPF_NOEXIST is given for an existing key) and ENOENT (when BPF_EXIST is given for a missing key).
Two map families reach that limit in different code:
- Preallocated hash (the default).
prealloc_initallocates allmax_entrieselements in one contiguous pool plus a per-CPU freelist. When the freelist is empty,__pcpu_freelist_popinalloc_htab_elemfails and returnsERR_PTR(-E2BIG). - Lazy hash (
BPF_F_NO_PREALLOC).alloc_htab_elemcallsis_map_fullbefore allocating, andis_map_fullcompares the element count againstmax_entries(apercpu_counterwith batch 32 when the map is large, otherwise a plainatomic_t). Full map + no old element to replace →ERR_PTR(-E2BIG). A genuinely out-of-memory allocation in the lazy path is a different error:ENOMEM, notE2BIG.
E2BIG also appears at map creation time — key_size + value_size too large to be a kmalloc-able element, per-CPU value size over PCPU_MIN_UNIT_SIZE, or max_entries > 1UL << 31 — so the errno by itself does not tell you which of these you hit.
Why updating an existing key does not hit the cap
htab_map_update_elem looks up the key first. If l_old is found, the full-capacity question only matters for the allocation step:
- Preallocated:
alloc_htab_elemtakes the per-CPUextra_elemsslot, stashesl_oldinto it (*pl_new = old_elem), and swaps the new element in at the head of the bucket. No freelist pop, no capacity check, noE2BIG— the total element count does not grow. - Lazy:
is_map_fullreturns true, but the comment inalloc_htab_elemstates the rule: when the map is full and the update is replacing an old element, allocation is allowed because the old element is freed immediately (free_htab_elemafter the bucket-list swap, aftercheck_and_cancel_fieldscancels any embedded BPF object fields). The count stays flat:inc_elem_countfor the new element,dec_elem_countfor the old one.
The capacity check is_map_full is only consulted for new keys on a full map. That is the whole asymmetry: at max_entries, "new key" → E2BIG, "existing key" → success.
The spin-lock value exception
For a map whose value embeds bpf_spin_lock (updates pass the BPF_F_LOCK flag — the map's BTF record must carry BPF_SPIN_LOCK or the update is rejected with -EINVAL), htab_map_update_elem takes a shortcut: if the key already exists, it grabs the element's spin lock and calls copy_map_value_locked in place, returning before any allocation. Updating an existing spin-locked value therefore never allocates and cannot be blocked by capacity — bpf_spin_lock (kernel 5.1+) is the documented way to keep a value consistent while programs on different CPUs mutate it concurrently. New keys on such a map still fall through to the normal allocation path and are subject to the same E2BIG at capacity.
How the LRU variants instead
BPF_MAP_TYPE_LRU_HASH / LRU_PERCPU_HASH replace the E2BIG at capacity with eviction. The kernel docs state it directly: "An LRU hash will automatically evict the least recently used entries when the hash table reaches capacity." In the update path (htab_lru_map_update_elem / __htab_lru_percpu_map_update_elem), the element is obtained from the LRU before taking the bucket lock, because the eviction work it may need acquires the bucket lock itself: prealloc_lru_pop → bpf_lru_pop_free.
bpf_lru_pop_free (in kernel/bpf/bpf_lru_list.c) escalates in steps:
- Pop a node from this CPU's local free list (
free_llist); pending nodes are not popped here — they are flushed into the global lists during the fetch below. - If that is empty, fetch up to
target_freefree nodes from the global lists into the local free list — this first flushes this CPU's pending nodes into the global free/active/inactive lists by ref bit, then drains the global free list — and re-pop the local free list. - If the global free list is short, shrink: move ref-bit-cleared nodes from the global inactive list into the free list via
lru->del_from_htab(unlink from the hash table — the same unlink-then-free path asdelete, without zeroing value bytes). - If the inactive list yields nothing, do a force shrink that removes a node ignoring the ref bit, preferring the inactive list over the active list, so that even a hot map can be made to give up an element.
- If still nothing, steal a free (then pending) node from a local list — round-robin over CPUs starting at
next_steal, per the kernel comment: "the current CPU and remote CPU in RR" — skipping any victim whose lock cannot be acquired. - If all of that fails,
prealloc_lru_popyields nothing and the update returns-ENOMEM— the LRU variant fails at capacity as an allocation failure, not the plain hash's explicitE2BIGfull check.
The docs summarize the escalation as: use CPU-local state, fetch free nodes from the global lists, pull any node from a global list and remove it from the hash map, then pull any node from any CPU's list. One consequence worth remembering: an LRU map's "full" means evict now, so a lookup for a key you never deleted can return NULL after capacity pressure — a capacity eviction, not a delete. The map type field in bpf_map_info is how user-space confirms which family it is dealing with.
How to verify it
- Watch the errno, not the message. Fill a plain
BPF_MAP_TYPE_HASHtomax_entries, then issue two updates: a new key must fail withE2BIG(strerror(-ret)frombpf_map_update_elem/ libbpf'sbpf_map__update_elem), and an update to an existing key must return 0. Repeat after onebpf_map_delete_elem: a new key succeeds again. - Confirm the configured cap.
bpf_map_inforeportsmax_entriesand the map type, so a failure you expected at 1000 keys may actually be a map created with a smallermax_entries— check the fd's info before assuming capacity. - Distinguish LRU eviction from a bug. On a
BPF_MAP_TYPE_LRU_HASHat capacity, lookups for keys you never deleted can start failing after other updates evict them; verify withbpf_map_lookup_elemreturningNULLplus a new update that would have beenE2BIGon a plain hash. - Count elements without assuming a counter. For plain hash maps there is no stable user-space "current count" field in
bpf_map_info; estimate by iterating keys withbpf_map_get_next_key(batched variants for a stable sweep), and remember that iteration restarts from the first key if thecur_keyyou passed was just deleted. - Check which allocation family. If the map was created with
BPF_F_NO_PREALLOC, an update failure at capacity can beENOMEM(allocator exhaustion) rather thanE2BIG— theE2BIGpath there is the explicit full-check, andENOMEMisbpf_mem_cache_allocrunning out of memory.
Where the answer stops
- The
E2BIGon create (oversizedkey_size + value_size, per-CPU value overPCPU_MIN_UNIT_SIZE,max_entries > 1UL << 31) shares the errno with the full-map update; the return code alone does not say which. per-CPU LRUmaps withBPF_F_NO_COMMON_LRUgive each CPU its own LRU list, which changes which element evicts per CPU — not the eviction-instead-of-E2BIG behavior.- The element count used by
is_map_fullis apercpu_counterwith batch 32 on large maps, so the "full" comparison is approximate by design; the eviction guarantee on LRU maps is the LRU property, not an exact-size promise. - Whether a recycled element's value bytes persist is a separate boundary (documented in the 2026-09-23 entry): unlinking and evicting never zero the value region;
E2BIGand eviction are capacity decisions, not memory-scrubbing guarantees.
References
- BPF_MAP_TYPE_HASH, with PERCPU and LRU Variants (kernel docs) — preallocation by default,
BPF_F_NO_PREALLOC, "automatically evict the least recently used entries when the hash table reaches capacity", the LRU update escalation steps,bpf_spin_locksynchronization. - kernel/bpf/hashtab.c —
alloc_htab_elem(freelist pop →E2BIG; the "when map is full and update() is replacing old element" comment; theextra_elemsswap);htab_map_update_elem(lookup-first,BPF_F_LOCKin-place shortcut);htab_lru_map_update_elem/__htab_lru_percpu_map_update_elem(alloc-before-lock);is_map_full(percpu_counterbatch 32 vsatomic_t); the create-timeE2BIGchecks. - kernel/bpf/bpf_lru_list.c —
bpf_lru_pop_free(local free lists, cross-CPU steal, inactive shrink viadel_from_htab, force shrink ignoring the ref bit). - include/uapi/linux/bpf.h — the
bpf_map_update_elem(2)E2BIGcontract text,BPF_F_LOCK/BPF_F_NO_PREALLOC/BPF_F_NO_COMMON_LRUflags,struct bpf_map_info(max_entries, the maptypefield).
Community discussion today
Honest coverage note: the two watchlist-opted Slack archives were reachable this run but returned zero messages in the rolling window (snapshot ok, 0 bytes, 0 messages), and the allowlisted Discord channels, the public bpf mailing list, and r/eBPF remain visible-browser-only, with no visible-browser session available in this run. No private community material was available for 2026-09-25, so the question above is a fallback selection: a genuine, recurring practitioner boundary of the monitored eBPF development community ("my map rejects new keys at capacity, yet updating the same key works" / "a key disappeared from my LRU map without a delete"), grounded entirely in the public primary sources above (kernel docs and upstream kernel/bpf/hashtab.c, bpf_lru_list.c, and the uapi header) rather than in any thread.
The practitioner pattern that recurs around this boundary: a monitoring tool or eBPF program keyed on connection state (tuple → counter/state structs) silently stops recording once the key space outgrows max_entries, because every new key is dropped at the update with E2BIG while the hot existing keys keep working — the symptom reads like "new sessions are missing but old ones keep counting", and the fix is sizing max_entries (or switching to BPF_F_NO_PREALLOC or an LRU variant, accepting eviction), not a retry loop. The LRU side recurs as its own surprise: an evicted key that was never deleted reads back NULL, which people misdiagnose as a delete bug when it is capacity eviction. The public-doc-anchored rule that closes both: a plain hash map's max_entries is a hard cap enforced at allocation (new keys E2BIG, existing-key updates exempt, spin-locked values in-place); an LRU hash map turns that same capacity into an eviction decision, so "full" means "something older leaves now", not "the next insert fails". No private text, identity, channel, or link is reproduced here.
Continue exploring
Back to index
eBPF Q&A
Daily, source-grounded answers to recurring questions about eBPF, Linux observability, profiling, runtime extension, and security.
Previous
Why does a BPF LPM-trie lookup return a less specific stored prefix, and how does the key prefixlen field decide which entry wins?
Short answer: Set the lookup key prefixlen to the map's maximum prefix length (32 for a 4-byte IPv4 data field, 128 for a 16-byte IPv6 field).
Next
Why does a connect or accept probe miss a socket that changed owner through a file descriptor handoff?
Short answer: because connect and accept are process actions, whereas socket ownership is a kernel object that outlives any one process.
- Last updated
- Sep 25, 2026
- First published
- Sep 25, 2026
- Contributors
- Yunwei 123
Was this page helpful?