<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>eBPF maps Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/ebpf-maps/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/ebpf-maps/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Mon, 06 Jul 2026 03:36:07 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.4</generator>

<image>
	<url>https://linuxcent.com/wp-content/uploads/2026/04/favicon-512x512-1-150x150.png</url>
	<title>eBPF maps Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/ebpf-maps/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>eBPF Maps — The Persistent Data Layer Between Kernel and Userspace</title>
		<link>https://linuxcent.com/ebpf-maps-explained/</link>
					<comments>https://linuxcent.com/ebpf-maps-explained/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Thu, 16 Apr 2026 17:57:45 +0000</pubDate>
				<category><![CDATA[eBPF]]></category>
		<category><![CDATA[bpftool]]></category>
		<category><![CDATA[Cilium]]></category>
		<category><![CDATA[eBPF maps]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Observability]]></category>
		<category><![CDATA[SRE]]></category>
		<guid isPermaLink="false">https://linuxcent.com/ebpf-maps-explained/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 11</span> <span class="rt-label rt-postfix">minutes</span></span>eBPF maps are the persistent data layer between kernel and userspace — hash maps, ring buffers, and LRU maps explained for SREs running Cilium and Falco.</p>
<p>The post <a href="https://linuxcent.com/ebpf-maps-explained/">eBPF Maps — The Persistent Data Layer Between Kernel and Userspace</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 11</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>eBPF: From Kernel to Cloud, Episode 5</em><br />
<em><a href="https://linuxcent.com/what-is-ebpf-linux-kubernetes/">What Is eBPF?</a> · <a href="https://linuxcent.com/bpf-verifier-kubernetes-safety/">The BPF Verifier</a> · <a href="https://linuxcent.com/ebpf-vs-kernel-modules-kubernetes/">eBPF vs Kernel Modules</a> · <a href="https://linuxcent.com/ebpf-program-types-kubernetes/">eBPF Program Types</a> · </em><em>eBPF Maps</em>**</p>
<hr />
<p style="font-size:0.72em;font-weight:700;letter-spacing:0.12em;color:#f59e0b;text-transform:uppercase;margin:2em 0 0.75em 0;text-align:center;">Architecture Overview</p>
<figure class="wp-block-image size-full" style="margin:0 0 0.5em 0;">
<img fetchpriority="high" decoding="async" width="1190" height="2560" src="https://linuxcent.com/wp-content/uploads/2026/05/ep05-ebpf-maps-og-2-scaled.png" alt="eBPF Maps — the persistent data layer between kernel eBPF programs and userspace tools" class="wp-image-2112" style="width:100%;height:auto;display:block;border-radius:8px;" srcset="https://linuxcent.com/wp-content/uploads/2026/05/ep05-ebpf-maps-og-2-scaled.png 1190w, https://linuxcent.com/wp-content/uploads/2026/05/ep05-ebpf-maps-og-2-139x300.png 139w, https://linuxcent.com/wp-content/uploads/2026/05/ep05-ebpf-maps-og-2-476x1024.png 476w, https://linuxcent.com/wp-content/uploads/2026/05/ep05-ebpf-maps-og-2-768x1652.png 768w, https://linuxcent.com/wp-content/uploads/2026/05/ep05-ebpf-maps-og-2-714x1536.png 714w, https://linuxcent.com/wp-content/uploads/2026/05/ep05-ebpf-maps-og-2-952x2048.png 952w" sizes="(max-width: 1190px) 100vw, 1190px" /><figcaption style="text-align:center;font-size:0.85em;color:#6b7280;margin-top:0.75em;">eBPF maps are the shared memory between kernel programs and userspace — hash, array, ringbuf, and LRU variants shown.</figcaption></figure>
<hr style="border:none;border-top:1px solid #e5e7eb;margin:0.5em 0 2em 0;"/>
<h2 id="tldr">TL;DR</h2>
<ul>
<li>eBPF programs are stateless — maps are where all state lives, between invocations and between kernel and userspace<br />
<em>(&#8220;stateless&#8221; here means each program invocation starts with no memory of previous runs — like a function with no global variables)</em></li>
<li>Every production eBPF tool (Cilium, Falco, Tetragon, Datadog NPM) is a map-based architecture — <code class="" data-line="">bpftool map list</code> shows you what it&#8217;s actually holding</li>
<li>Per-CPU maps eliminate write contention for high-frequency counters; the tool aggregates per-CPU values at export time</li>
<li>LRU maps handle unbounded key spaces (IPs, PIDs, connections) without hard errors when full — but eviction is silent, so size generously</li>
<li>Ring buffer (kernel 5.8+) is the correct event streaming primitive — Falco and Tetragon both use it</li>
<li>Map memory is kernel-locked and invisible to standard memory metrics — account for it explicitly on eBPF-heavy nodes</li>
<li>Pinned maps survive restarts; Cilium uses this for zero-disruption connection tracking through upgrades</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">  HOW eBPF MAPS CONNECT KERNEL PROGRAMS TO USERSPACE TOOLS

  ┌─────────────────────────────────────────────────────────────┐
  │  Kernel space                                               │
  │                                                             │
  │  [XDP program]  [TC program]  [kprobe]  [tracepoint]        │
  │        │              │           │           │             │
  │        └──────────────┴───────────┴───────────┘             │
  │                              │                              │
  │                   bpf_map_update_elem()                     │
  │                              │                              │
  │                              ▼                              │
  │  ┌─────────────────────────────────────────────────────┐    │
  │  │             eBPF MAP (kernel object)                │    │
  │  │  hash · percpu_hash · lru_hash · ringbuf · lpm_trie │    │
  │  │  Lives outside program invocations.                 │    │
  │  │  Pinned maps (/sys/fs/bpf/) survive restarts.       │    │
  │  └────────────────────┬────────────────────────────────┘    │
  └───────────────────────│─────────────────────────────────────┘
                          │  read / write via file descriptor
                          ▼
  ┌─────────────────────────────────────────────────────────────┐
  │  Userspace tools                                            │
  │                                                             │
  │  Cilium agent  Falco engine  Tetragon  bpftool map dump     │
  └─────────────────────────────────────────────────────────────┘
</code></pre>
<hr />
<p>eBPF maps are the persistent data layer between kernel programs and the tools that consume their output. eBPF programs fire and exit — there&#8217;s no memory between invocations. Yet Cilium tracks TCP connections across millions of packets, and Falco correlates a process exec from five minutes ago with a suspicious network connection happening now. The mechanism between stateless kernel programs and the stateful production tools you depend on is what this episode is about — and understanding it changes what you see when you run <code class="" data-line="">bpftool map list</code>.</p>
<hr />
<p>I was trying to identify the noisy neighbor saturating a cluster&#8217;s egress link. I had an eBPF program loading cleanly, events firing, everything confirming it was working. But when I read back the per-port connection counters from userspace, everything was zero.</p>
<p>I spent an hour on it before posting to the BCC mailing list. The reply came back fast: eBPF programs don&#8217;t hold state between invocations. Every time the kprobe fires, the program starts fresh. The counter I was incrementing existed only for that single call — created, incremented to one, then discarded. On every single invocation. I was counting events one at a time, throwing the count away, and reading nothing.</p>
<p>That&#8217;s what eBPF maps solve.</p>
<h2 id="quick-check-what-maps-are-running-on-your-node">Quick Check: What Maps Are Running on Your Node?</h2>
<p>Before the map types walkthrough — see the live state of maps on any cluster node right now:</p>
<pre><code class="" data-line=""># SSH into a worker node, then:
bpftool map list
</code></pre>
<p>On a node running Cilium + Falco, you&#8217;ll see something like:</p>
<pre><code class="" data-line="">12: hash          name cilium_ct4_glo    key 24B  value 56B  max_entries 65536  memlock 5767168B
13: lpm_trie      name cilium_ipcache    key 40B  value 32B  max_entries 512000 memlock 327680B
14: percpu_hash   name cilium_metrics    key 8B   value 32B  max_entries 65536  memlock 2097152B
28: ringbuf       name falco_events      max_entries 8388608
</code></pre>
<p>Reading this output:<br />
&#8211; <code class="" data-line="">hash</code>, <code class="" data-line="">lpm_trie</code>, <code class="" data-line="">percpu_hash</code>, <code class="" data-line="">ringbuf</code> — the map <em>type</em> (each optimised for a different access pattern)<br />
&#8211; <code class="" data-line="">key 24B value 56B</code> — sizes of a single entry&#8217;s key and value in bytes<br />
&#8211; <code class="" data-line="">max_entries</code> — the hard ceiling; when the map is full, behaviour depends on type (see LRU section below)<br />
&#8211; <code class="" data-line="">memlock</code> — non-pageable kernel memory this map consumes (invisible to <code class="" data-line="">free</code> and container metrics)</p>
<blockquote>
<p><strong>Not running Cilium?</strong> On EKS with <code class="" data-line="">aws-vpc-cni</code> or GKE with <code class="" data-line="">kubenet</code>, there are far fewer maps here — primarily kube-proxy uses iptables rather than BPF maps. Running <code class="" data-line="">bpftool map list</code> still works; you&#8217;ll just see fewer entries. On a pure iptables-based cluster, most of the maps you see come from the system kernel itself, not a CNI.</p>
</blockquote>
<h2 id="maps-are-the-architecture-not-an-afterthought">Maps Are the Architecture, Not an Afterthought</h2>
<p>Maps are kernel objects that live outside any individual program invocation. They&#8217;re shared between multiple eBPF programs, readable and writable from userspace, and persistent for the lifetime of the map — which can outlive both the program that created them and the userspace process that loaded them.</p>
<p>Every production eBPF tool is fundamentally a map-based architecture:</p>
<ul>
<li>Cilium stores connection tracking state in BPF hash maps</li>
<li>Falco uses ring buffers to stream syscall events to its userspace rule engine</li>
<li>Tetragon maintains process tree state across exec events using maps</li>
<li>Datadog NPM stores per-connection flow stats in per-CPU maps for lock-free metric accumulation</li>
</ul>
<p>Run <code class="" data-line="">bpftool map list</code> on a Cilium node:</p>
<pre><code class="" data-line="">$ bpftool map list
ID 12: hash          name cilium_ct4_glo    key 24B  value 56B   max_entries 65536
#      ^^^^           ^^^^^^^^^^^^^^^^       ^^^^^^   ^^^^^^^     ^^^^^^^^^^^^^^^^
#      type           map name               key size value size  max concurrent entries

ID 13: lpm_trie      name cilium_ipcache    key 40B  value 32B   max_entries 512000
#      longest-prefix-match trie — for IP address + CIDR lookups

ID 14: percpu_hash   name cilium_metrics    key 8B   value 32B   max_entries 65536
#      one copy of this map per CPU — no write contention for high-frequency counters

ID 28: ringbuf       name falco_events      max_entries 8388608
#                                           ^^^^^^^^^^^ 8MB ring buffer for event streaming
</code></pre>
<p>Connection tracking, IP policy cache, per-CPU metrics, event stream. Every one of these is a different map type, chosen for a specific reason.</p>
<h2 id="map-types-and-what-theyre-actually-used-for">Map Types and What They&#8217;re Actually Used For</h2>
<h3 id="hash-maps">Hash Maps</h3>
<p>The general-purpose key-value store. A key maps to a value — lookup is O(1) average. Cilium&#8217;s connection tracking map (<code class="" data-line="">cilium_ct4_glo</code>) is a hash map: the key is a 5-tuple (source IP, destination IP, ports, protocol), the value is the connection state.</p>
<pre><code class="" data-line="">$ bpftool map show id 12
12: hash  name cilium_ct4_glo  flags 0x0
        key 24B  value 56B  max_entries 65536  memlock 5767168B
</code></pre>
<p>The <code class="" data-line="">key 24B</code> is the 5-tuple. The <code class="" data-line="">value 56B</code> is the connection state record. <code class="" data-line="">max_entries 65536</code> is the upper bound — Cilium can track 65,536 active connections in this map before hitting the limit.</p>
<p>Hash maps are shared across all CPUs on the node. When multiple CPUs try to update the same entry simultaneously — which happens constantly on busy nodes — writes need to be coordinated. For most use cases this is fine. For high-frequency counters updated on every packet, it&#8217;s a bottleneck. That&#8217;s when you reach for a per-CPU hash map.</p>
<p><strong>Where you see them:</strong> connection tracking, per-IP statistics, process-to-identity mapping, policy verdict caching.</p>
<h3 id="per-cpu-hash-maps">Per-CPU Hash Maps</h3>
<p>Per-CPU hash maps solve the write coordination problem by giving each CPU its own independent copy of every entry. There&#8217;s no sharing, no contention, no waiting — each CPU writes its own copy without touching any other.</p>
<p>The tradeoff: reading from userspace means collecting one value per CPU and summing them up. That aggregation happens in the tool, not the kernel.</p>
<pre><code class="" data-line=""># Cilium&#039;s per-CPU metrics map — one counter value per CPU
bpftool map dump id 14
key: 0x00000001
  value (CPU 00): 12345
  value (CPU 01): 8901
  value (CPU 02): 3421
  value (CPU 03): 7102
# total bytes for this metric: 31769
</code></pre>
<p>Cilium&#8217;s <code class="" data-line="">cilium_metrics</code> map uses this pattern for exactly this reason — it&#8217;s updated on every packet across every CPU on the node. Forcing all CPUs to coordinate writes to a single shared entry at that rate would hurt throughput. Instead: each CPU writes locally, Cilium&#8217;s userspace agent sums the values at export time.</p>
<p><strong>Where you see them:</strong> packet counters, byte counters, syscall frequency metrics — anywhere updates happen on every event at high volume.</p>
<h3 id="lru-hash-maps">LRU Hash Maps</h3>
<p>LRU hash maps add automatic eviction. Same key-value semantics as a regular hash map, but when the map hits its entry limit, the least recently accessed entry is dropped to make room for the new one.</p>
<p>This matters for any map tracking dynamic state with an unpredictable number of keys: TCP connections, process IDs, DNS queries, pod IPs. Without LRU semantics, a full map returns an error on insert — and in production, that means your tool silently stops tracking new entries. Not a crash, not an alert — just missing data.</p>
<p>Cilium&#8217;s connection tracking map is LRU-bounded at 65,536 entries. On a node handling high-connection-rate workloads, this can fill up. When it does, Cilium starts evicting old connections to make room for new ones — and if it&#8217;s evicting too aggressively, you&#8217;ll see connection resets.</p>
<pre><code class="" data-line=""># Check current CT map usage vs its limit
bpftool map show id 12
# max_entries tells you the ceiling
# count entries to see current usage
bpftool map dump id 12 | grep -c &quot;^key&quot;
</code></pre>
<p>Size LRU maps at 2× your expected concurrent active entries. Aggressive eviction under pressure introduces gaps — not crashes, but missing or incorrect state.</p>
<p><strong>Where you see them:</strong> connection tracking, process lineage, anything where the key space is dynamic and unbounded.</p>
<h3 id="ring-buffers">Ring Buffers</h3>
<p>Ring buffers are how eBPF tools stream events from the kernel to a userspace consumer. Falco reads syscall events from a ring buffer. Tetragon streams process execution and network events through ring buffers. The pattern is the same across all of them:</p>
<pre><code class="" data-line="">kernel eBPF program
  → sees event (syscall, network packet, process exec)
  → writes record to ring buffer
  → userspace tool reads it and processes (Falco rules, Tetragon policies)
</code></pre>
<p>What makes ring buffers the right primitive for event streaming:</p>
<ul>
<li><strong>Single buffer shared across all CPUs</strong> — unlike the older <code class="" data-line="">perf_event_array</code> approach which required one buffer per CPU, a ring buffer is one allocation, one file descriptor, one consumer</li>
<li><strong>Lock-free</strong> — the kernel writes, the userspace tool reads, they don&#8217;t block each other</li>
<li><strong>Backpressure when full</strong> — if the userspace tool can&#8217;t keep up, new events are dropped rather than queued indefinitely. The tool can detect and count drops. Falco reports these as <code class="" data-line="">Dropped events</code> in its stats output.</li>
</ul>
<pre><code class="" data-line=""># Falco&#039;s ring buffer — 8MB
bpftool map list | grep ringbuf
# ID 28: ringbuf  name falco_events  max_entries 8388608
</code></pre>
<p>8,388,608 bytes = 8MB. That&#8217;s the buffer between Falco&#8217;s kernel hooks and its rule engine. If there&#8217;s a burst of syscall activity and Falco&#8217;s rule evaluation can&#8217;t keep up, events drop into that window and are lost.</p>
<p>Sizing matters operationally. Too small and you drop events during normal burst. Too large and you&#8217;re holding non-pageable kernel memory that doesn&#8217;t show up in standard memory metrics.</p>
<pre><code class="" data-line=""># Check Falco&#039;s drop rate
falcoctl stats
# or check the Falco logs
journalctl -u falco | grep -i &quot;drop&quot;
</code></pre>
<p>Most production deployments run 8–32MB. Start at 8MB, monitor drop rates under load, size up if needed.</p>
<p><strong>Where you see them:</strong> Falco event streaming, Tetragon audit events, any tool that needs to move high-volume event data from kernel to userspace.</p>
<h3 id="array-maps">Array Maps</h3>
<p>Array maps are fixed-size, integer-indexed, and entirely pre-allocated at creation time. Think of them as lookup tables with integer keys — constant-time access, no hash overhead, no dynamic allocation.</p>
<p>Cilium uses array maps for policy configuration: a fixed set of slots indexed by endpoint identity number. When a packet arrives and Cilium needs to check policy, it indexes into the array directly rather than doing a hash lookup. For read-heavy, write-rare data, this is faster.</p>
<p>The constraint: you can&#8217;t delete entries from an array map. Every slot exists for the lifetime of the map. If you need to track state that comes and goes — connections, processes, pods — use a hash map instead.</p>
<p><strong>Where you see them:</strong> policy configuration, routing tables with fixed indices, per-CPU stats indexed by CPU number.</p>
<h3 id="lpm-trie-maps">LPM Trie Maps</h3>
<p>LPM (Longest Prefix Match) trie maps handle IP prefix lookups — the same operation that a hardware router does when deciding which interface to send a packet out of.</p>
<p>You can store a mix of specific host addresses (/32) and CIDR ranges (/16, /24) in the same map, and a lookup returns the most specific match. If <code class="" data-line="">10.0.1.15/32</code> and <code class="" data-line="">10.0.0.0/8</code> are both in the map, a lookup for <code class="" data-line="">10.0.1.15</code> returns the /32 entry.</p>
<p>Cilium&#8217;s <code class="" data-line="">cilium_ipcache</code> map is an LPM trie. It maps every IP in the cluster to its security identity — the identifier Cilium uses for policy enforcement. When a packet arrives, Cilium does a trie lookup on the source IP to find out which endpoint sent it, then checks policy against that identity.</p>
<pre><code class="" data-line=""># Inspect the ipcache map
bpftool map show id 13
# lpm_trie  name cilium_ipcache  key 40B  value 32B  max_entries 512000

# Look up which security identity owns a pod IP
bpftool map lookup id 13 key hex 20 00 00 00 0a 00 01 0f 00 00 00 00 00 00 00 00 00 00 00 00
</code></pre>
<p><strong>Where you see them:</strong> IP-to-identity mapping (Cilium), CIDR-based policy enforcement, IP blocklists.</p>
<hr />
<h2 id="pinned-maps-state-that-survives-restarts">Pinned Maps — State That Survives Restarts</h2>
<p>By default, a map&#8217;s lifetime is tied to the tool that created it. When the tool exits, the kernel garbage-collects the map.</p>
<p>Pinning writes a reference to the BPF filesystem at <code class="" data-line="">/sys/fs/bpf</code>, which keeps the map alive even after the creating process exits:</p>
<pre><code class="" data-line=""># See all maps Cilium has pinned
ls /sys/fs/bpf/tc/globals/
# cilium_ct4_global  cilium_ipcache  cilium_metrics  cilium_policy ...

# Inspect a pinned map directly — no Cilium process needed
bpftool map dump pinned /sys/fs/bpf/tc/globals/cilium_ct4_global

# Pin any map by ID for manual inspection
bpftool map pin id 12 /sys/fs/bpf/my_conn_tracker
bpftool map dump pinned /sys/fs/bpf/my_conn_tracker
</code></pre>
<p>Cilium pins all its maps under <code class="" data-line="">/sys/fs/bpf/tc/globals/</code>. When Cilium restarts — rolling upgrade, crash, OOM kill — it reopens its pinned maps and resumes with existing state intact. Pods maintain established TCP connections through a Cilium restart without disruption.</p>
<p>This is operationally significant: if you&#8217;re evaluating eBPF-based tools for production, check whether they pin their maps. A tool that doesn&#8217;t loses all its tracked state on every restart — connection tracking resets, process lineage gaps, policy state rebuilt from scratch.</p>
<hr />
<h2 id="map-memory-a-production-consideration">Map Memory: A Production Consideration</h2>
<p>Map memory is kernel-locked — it cannot be paged out, and it doesn&#8217;t show up in standard memory pressure metrics. Your node&#8217;s <code class="" data-line="">free</code> output and container memory limits don&#8217;t account for it.</p>
<blockquote>
<p><strong>Kernel-locked memory</strong> is memory the OS guarantees will never be swapped to disk — it stays in RAM permanently. The kernel requires this for eBPF maps because a kernel program running during a network interrupt cannot wait for a page fault. The side effect: it doesn&#8217;t appear in <code class="" data-line="">top</code>, <code class="" data-line="">free</code>, or container memory metrics, so it&#8217;s easy to accidentally provision nodes without accounting for it.</p>
</blockquote>
<pre><code class="" data-line=""># Total eBPF map memory locked on this node
bpftool map list -j | python3 -c &quot;
import json,sys
maps=json.load(sys.stdin)
total=sum(m.get(&#039;bytes_memlock&#039;,0) for m in maps)
print(f&#039;Total map memory: {total/1024/1024:.1f} MB&#039;)
&quot;

# Check system memlock limit (unlimited is correct for eBPF tools)
ulimit -l

# Check what Cilium&#039;s systemd unit sets
systemctl show cilium | grep -i memlock
</code></pre>
<p>On a node running Cilium + Falco + Datadog NPM, I&#8217;ve seen 200–400MB of map memory locked. That&#8217;s real, non-pageable kernel memory. If you&#8217;re sizing nodes for eBPF-heavy workloads, account for this separately from your pod workload memory.</p>
<p>If an eBPF tool fails to load with a permission error despite having enough free memory, the root cause is usually the <code class="" data-line="">memlock</code> ulimit for the process. Cilium, Falco, and most production tools set <code class="" data-line="">LimitMEMLOCK=infinity</code> in their systemd units. Verify this if you&#8217;re deploying a new eBPF-based tool and seeing unexpected load failures.</p>
<hr />
<h2 id="inspecting-maps-in-production">Inspecting Maps in Production</h2>
<pre><code class="" data-line=""># List all maps: type, name, key/value sizes, memory usage
bpftool map list

# Dump all entries in a map (careful with large maps)
bpftool map dump id 12

# Look up a specific entry by key
bpftool map lookup id 12 key hex 0a 00 01 0f 00 00 00 00

# Watch map stats live
watch -n1 &#039;bpftool map show id 12&#039;

# See all maps for a specific tool by checking its pinned path
ls /sys/fs/bpf/tc/globals/                    # Cilium
ls /sys/fs/bpf/falco/                         # Falco (if pinned)

# Cross-reference map IDs with the programs using them
bpftool prog list
bpftool map list
</code></pre>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>A full LRU map drops state silently, not loudly</strong><br />
When Cilium&#8217;s CT map fills up, it starts evicting the least recently used connections — not returning an error. You see connection resets, not a tool alert. Check map utilisation (<code class="" data-line="">bpftool map dump id X | grep -c key</code>) against <code class="" data-line="">max_entries</code> on nodes with high connection rates.</p>
<p><strong>Ring buffer drops don&#8217;t stop the tool — they create gaps</strong><br />
When Falco&#8217;s ring buffer fills up, events are dropped. Falco keeps running. The rule engine keeps processing. But you have gaps in your syscall visibility. Monitor <code class="" data-line="">Dropped events</code> in Falco&#8217;s stats and size the ring buffer accordingly.</p>
<p><strong>Map memory is invisible to standard monitoring</strong><br />
200–400MB of kernel-locked memory on a Cilium + Falco node doesn&#8217;t appear in <code class="" data-line="">top</code>, container memory metrics, or memory pressure alerts. Size eBPF-heavy nodes with this in mind and add explicit map memory monitoring via <code class="" data-line="">bpftool</code>.</p>
<p><strong>Tools that don&#8217;t pin their maps lose state on restart</strong><br />
A Cilium restart with pinned maps = zero-disruption connection tracking. A tool without pinning = all tracked state rebuilt from scratch. This matters for connection tracking tools and any tool maintaining process lineage.</p>
<p><strong><code class="" data-line="">perf_event_array</code> on kernel 5.8+ is the old way</strong><br />
Older eBPF tools use per-CPU <code class="" data-line="">perf_event_array</code> for event streaming. Ring buffer is strictly better — single allocation, lower overhead, simpler consumption. If you&#8217;re running a tool that still uses <code class="" data-line="">perf_event_array</code> on a 5.8+ kernel, it&#8217;s using a legacy path.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>eBPF programs are stateless — maps are where all state lives, between invocations and between kernel and userspace</li>
<li>Every production eBPF tool (Cilium, Falco, Tetragon, Datadog NPM) is a map-based architecture — <code class="" data-line="">bpftool map list</code> shows you what it&#8217;s actually holding</li>
<li>Per-CPU maps eliminate write contention for high-frequency counters; the tool aggregates per-CPU values at export time</li>
<li>LRU maps handle unbounded key spaces (IPs, PIDs, connections) without hard errors when full — but eviction is silent, so size generously</li>
<li>Ring buffer (kernel 5.8+) is the correct event streaming primitive — Falco and Tetragon both use it</li>
<li>Map memory is kernel-locked and invisible to standard memory metrics — account for it explicitly on eBPF-heavy nodes</li>
<li>Pinned maps survive restarts; Cilium uses this for zero-disruption connection tracking through upgrades</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>You know what program types run in the kernel, and you know how they hold state.</p>
<p>Get EP06 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a> But there&#8217;s a problem anyone running eBPF-based tools eventually runs into: a tool works on one kernel version and breaks on the next. Struct layouts shift between patch versions. Field offsets move. EP06 covers CO-RE (Compile Once, Run Everywhere) and libbpf — the mechanism that makes tools like Cilium and Falco survive your node upgrades without recompilation, and why kernel version compatibility is a solved problem for any tool built on this toolchain.</p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-maps-explained%2F&amp;linkname=eBPF%20Maps%20%E2%80%94%20The%20Persistent%20Data%20Layer%20Between%20Kernel%20and%20Userspace" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-maps-explained%2F&amp;linkname=eBPF%20Maps%20%E2%80%94%20The%20Persistent%20Data%20Layer%20Between%20Kernel%20and%20Userspace" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-maps-explained%2F&amp;linkname=eBPF%20Maps%20%E2%80%94%20The%20Persistent%20Data%20Layer%20Between%20Kernel%20and%20Userspace" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-maps-explained%2F&amp;linkname=eBPF%20Maps%20%E2%80%94%20The%20Persistent%20Data%20Layer%20Between%20Kernel%20and%20Userspace" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-maps-explained%2F&amp;linkname=eBPF%20Maps%20%E2%80%94%20The%20Persistent%20Data%20Layer%20Between%20Kernel%20and%20Userspace" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-maps-explained%2F&amp;linkname=eBPF%20Maps%20%E2%80%94%20The%20Persistent%20Data%20Layer%20Between%20Kernel%20and%20Userspace" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-maps-explained%2F&amp;linkname=eBPF%20Maps%20%E2%80%94%20The%20Persistent%20Data%20Layer%20Between%20Kernel%20and%20Userspace" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Febpf-maps-explained%2F&#038;title=eBPF%20Maps%20%E2%80%94%20The%20Persistent%20Data%20Layer%20Between%20Kernel%20and%20Userspace" data-a2a-url="https://linuxcent.com/ebpf-maps-explained/" data-a2a-title="eBPF Maps — The Persistent Data Layer Between Kernel and Userspace"></a></p><p>The post <a href="https://linuxcent.com/ebpf-maps-explained/">eBPF Maps — The Persistent Data Layer Between Kernel and Userspace</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/ebpf-maps-explained/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1481</post-id>	</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: linuxcent.com @ 2026-09-02 04:07:00 by W3 Total Cache
-->