A long-running eBPF tool keys a BPF_MAP_TYPE_HASH by connection and never observes some of those connections close, so entries accumulate. What happens once the map reaches max_entries, and what does switching to BPF_MAP_TYPE_LRU_HASH change?
answer
- capacity is fixed at creation, never grows
- new keys rejected, existing keys still update
- the failure nobody sees: an ignored return value
- eviction instead of rejection
- cache yes, ledger no
basics
~20 sA BPF hash map has a fixed max_entries; once full, inserting a new key fails with -E2BIG and, if the program ignores the return value, the event is silently dropped. BPF_MAP_TYPE_LRU_HASH instead evicts an approximately least-recently-used entry so inserts keep succeeding.
solid answer
~50 s`max_entries` is a hard capacity chosen at map creation, and a hash map never grows. Once it is full, `bpf_map_update_elem()` for a *new* key is rejected — the kernel returns `-E2BIG` — while updates to keys already present still work. Since most BPF programs discard helper return values, the failure is invisible: the tool simply stops seeing new connections while continuing to report on the old ones, which looks like the workload changed rather than like the tool broke. `BPF_MAP_TYPE_LRU_HASH` changes the full-map behaviour: instead of rejecting the insert it evicts an entry that has not been used recently, so writes keep succeeding and memory stays bounded. The eviction is approximate, not a strict LRU ordering, so an entry you still care about can disappear. That makes LRU right when the map is a cache and wrong when it is a ledger — for example holding a start timestamp that a later exit event must find, where an eviction silently destroys the correlation instead of the count.
go deeper
Know that a BPF hash map has a fixed max_entries set at creation and does not grow, and that BPF_MAP_TYPE_LRU_HASH exists to evict old entries when it is full.
Explain that a full hash map rejects new keys while existing keys still update, and that LRU trades rejection for approximate eviction so writes always succeed.
Diagnose it in the field: a tool that keeps reporting old entities and no new ones, a leaking key pattern from missed terminating events, and a failed-insert counter that makes exhaustion visible.
Own the accuracy contract — decide which maps are caches that may evict and which are ledgers that must not, size capacity against real workload concurrency, and require that exhaustion is exported rather than inferred.
## Fixed capacity is the starting fact A BPF hash map is created with a `max_entries` you choose up front, and it never resizes. By default the kernel preallocates all of that memory when the map is created — `BPF_F_NO_PREALLOC` opts out, trading a smaller resident footprint for allocation work on the insert path. Either way, capacity is a hard ceiling. ## What a leaking key pattern looks like The classic structure of a correlation tool is a map keyed by something with a lifecycle: ```c /* on entry */ bpf_map_update_elem(&start_ts, &key, &now, BPF_ANY); /* on exit */ __u64 *start = bpf_map_lookup_elem(&start_ts, &key); if (start) { emit_latency(now - *start); bpf_map_delete_elem(&start_ts, &key); } ``` The delete is what keeps the map bounded. Any path where the terminating event never fires — a process killed before its exit hook, a connection reset in a way your probe does not observe, a hook that simply is not attached on this kernel — leaves an entry behind forever. Slowly, the map fills with dead keys. ## Behaviour at max_entries When a `BPF_MAP_TYPE_HASH` holds `max_entries` elements, an update for a **new** key is rejected and the kernel returns `-E2BIG`. Updates to existing keys are unaffected. That distinction is what makes the failure so confusing operationally: the tool keeps producing output about the entries it already had, and simply never reports anything new. On the surface it looks like traffic stopped, or like a workload got quieter — not like an instrumentation failure. It is silent because almost nobody checks the return: ```c long err = bpf_map_update_elem(&start_ts, &key, &now, BPF_ANY); if (err) /* count it somewhere you will actually look */; ``` A tool that keeps a separate `BPF_MAP_TYPE_PERCPU_ARRAY` of failed-insert counts and exports it turns an invisible failure into a number on a graph. That is the single most valuable change you can make to a tool of this shape. ## What LRU changes `BPF_MAP_TYPE_LRU_HASH` has the same key/value interface but different full-map semantics: rather than rejecting the insert, it evicts an entry that has not been touched recently and reuses the space. Inserts therefore keep succeeding indefinitely and memory stays bounded by `max_entries`. There is a `BPF_MAP_TYPE_LRU_PERCPU_HASH` variant where the values are per-CPU as well. Two properties matter when you rely on it. **The eviction is approximate.** The implementation keeps its bookkeeping cheap so it does not serialise every lookup across CPUs, which means the entry evicted is *an old one*, not provably *the oldest one*. You cannot reason about exactly which key disappears. **Eviction is silent to the program.** Your later lookup for that key simply returns NULL, indistinguishable from an event you never saw the start of. ## Cache versus ledger That is the whole decision. If an entry disappearing costs you a little accuracy, LRU is the right structure: a hot-IP rate counter, a resolved-name cache, per-connection statistics where a missing connection is a rounding error. The map is a cache, and eviction is the designed behaviour. If an entry disappearing breaks correctness, LRU only converts a loud failure into a quiet one. A start-timestamp map is the canonical example: under exactly the load where latency matters most, the map is fullest, entries get evicted, and the slowest requests — the ones whose start is oldest and therefore least recently touched — are the ones whose measurements vanish. Your histogram loses precisely its tail. Plain hash with instrumented failures is more honest there. ## What to actually do In order: make sure the delete happens on every terminating path, including error paths, and where the terminating event can genuinely be missed, add a sweep — user space walking the map and deleting entries older than a threshold, since it can read timestamps that BPF wrote. Size `max_entries` for the real concurrency of the workload with headroom, not for a comfortable-looking number. Export a failed-insert or eviction count so exhaustion is visible. Then choose the type on the cache-versus-ledger test. Checking a map's current occupancy from outside — dumping or counting its entries with the standard BPF inspection tooling — is how you confirm the diagnosis when a tool has gone quiet.
- Why is a full BPF_MAP_TYPE_HASH so often misdiagnosed as the workload going quiet?Because updates to keys already in the map keep working — only insertions of new keys fail. The tool goes on reporting the entities it already tracks and never reports a new one, which reads as a traffic change rather than an instrumentation failure. Exporting a failed-insert counter is what makes the difference visible.
- Why is BPF_MAP_TYPE_LRU_HASH a poor choice for a map holding request start timestamps?Eviction is silent and approximate, and it bites hardest when the map is fullest — that is, under peak load. The entries least recently touched are the longest-running requests, so the measurements you lose are exactly the slow tail you built the tool to find. The histogram then looks better than reality.
- How would you bound a correlation map when the terminating event genuinely cannot always be observed?Store a timestamp in the value and have the user-space agent periodically walk the map and delete entries older than a plausible maximum lifetime. That keeps the map a ledger with a garbage collector rather than a cache, and it gives you a count of abandoned entries — a useful signal in its own right about missed exit events.
saying these in an interview costs you the question
- Assumes a full hash map starts overwriting old entries
- Thinks max_entries grows on demand under memory pressure
- Ignores the return value of bpf_map_update_elem()
- Treats LRU eviction as exact least-recently-used ordering
- Uses LRU for correlation state and calls the missing entries noise