DEV Community

Cover image for Three Numbers, One Alarm: Who Does vm.max_map_count Warn?
Mustafa ERBAY
Mustafa ERBAY

Posted on Originally published at mustafaerbay.com.tr

Three Numbers, One Alarm: Who Does vm.max_map_count Warn?

Yesterday, while tracing fs.file-max, I noticed a file in /etc/sysctl.d/ that I knew I had not written: 10-map-count.conf. Two comment lines and vm.max_map_count=1048576. The kernel's own default for this number is 65,530; somebody had raised the ceiling on VPS3 sixteenfold, and that somebody was not me. dpkg -S tied the file to the procps package, and its timestamp read 31 March 2024, 08:16 UTC, which matches the signature time in the changelog of Ubuntu's procps 2:4.0.4-4ubuntu3 to the minute. The file sits exactly as it came out of the package: dpkg leaves a conffile untouched on later upgrades (including the 4ubuntu3.2 installed today) if it has not been modified, and the timestamp carries the package's build time. Ubuntu 24.04 had moved this ceiling to a million on the day the server was installed, before I had even logged in.

Curious, I counted the /proc/PID/maps lines of every process on the machine. Across 706 process entries, 63,472 lines in total; the busiest process was a .NET API at 2,065 lines, and the Elasticsearch container came ninth with 855. Stack every mapping on the whole machine on top of each other and you still do not reach the ceiling the kernel set for one process. So what is this number protecting? The thesis of this post: vm.max_map_count is not a resource limit, it is a smoke detector. Over the last three years Fedora, then Ubuntu, then Arch swapped it for a less sensitive model, and the reason was games; in the same period Elasticsearch lived through a leak that the detector actually caught. Below I find the owner of each of the three numbers (65,530, 262,144, 1,048,576), then deliberately set off the alarm inside Docker Desktop's virtual machine.

What the counter counts

The kernel keeps a process's address space in pieces; each piece is a vm_area_struct, a VMA for short. Every line of /proc/PID/maps is one VMA. On VPS3, cat /proc/self/maps returned 43 lines: the cat binary itself takes five (read-only header, code, read-only data, relro, writable data), libc.so.6 another five, the heap one, locale-archive one, each LC_* file one. Every shared library you load adds roughly five lines; as the kernel documentation puts it, "most applications need less than a thousand maps".

The counter moves in both directions. Two adjacent anonymous regions with identical flags merge: in the lab I called mmap(4 KiB, rw, anonymous) 1,000 times in a row and maps grew by only 15 lines. The reverse is also true: change the permission of a single page in the middle of a region with mprotect and the region splits into three, the counter climbing by two at a time. Elasticsearch mapping every index file separately, the guard page placed under every thread stack, .NET double-mapping executable code with a writable twin: all of it lands on the same counter. Of the 2,065 lines in VPS3's .NET process, 600 are labelled /memfd:doublemapper; that is the runtime mapping code blocks with temporary writable copies for W^X (never writable and executable at the same time). RavenDB's lead developer counted 7,462 lines with the same label in 2023 and hit the mapping ceiling in production, as told in dotnet/runtime#89776.

Every VMA has a price in kernel memory: on VPS3 (6.8, x86_64) /sys/kernel/slab/vm_area_struct/object_size is 192 bytes; on Docker Desktop's 6.10 arm64 kernel it is 152. A process that really fills the one-million ceiling holds 192 MiB of kernel memory for this structure alone; anon_vma_chain and page tables come on top. The old 65,530 ceiling works out to 12 MiB. Let the size of those numbers sink in: this ceiling was not put there to protect memory.

65,530: a sixteen-bit echo

So why 65,530? The comment in include/linux/mm.h answers: DEFAULT_MAX_MAP_COUNT is USHRT_MAX - 5. When a process crashes the kernel writes a core file in ELF format and every VMA becomes a section; the section count in the ELF header lives in a 16-bit field, so no more than 65,535 sections fit, and five were kept in reserve for the informational sections the kernel adds. The rest of the comment is the interesting part: "ELF extended numbering allows more than 65535 sections, so 16-bit bound is not a hard limit any more. Although some userspace tools can be surprised by that." The technical reason behind the ceiling vanished years ago; the number stays out of habit.

Who hits the counter? In the kernel's mm/ directory sysctl_max_map_count is read in five places (mmap, brk, split, munmap, mremap), and the comparisons are not identical:

Diagram

On the mmap path the check is > (mm/mmap.c, "Too many mappings?"); on the split path it is >= (mm/vma.c, split_vma). The difference is one VMA: mmap lets you go one above the ceiling, mprotect stops you the moment you reach it. Since munmap removing a piece from the middle also requires a split, it goes through the same check; mm/vma.c annotates this with "let map_count go just above its limit temporarily, to help free resources as expected", but it returns -ENOMEM first. mremap asks for two splits' worth of room (VPS3's 6.8 kernel is more cautious still, max_map_count - 3; in that version the split and munmap checks had not yet moved to mm/vma.c and live in mm/mmap.c). fork never asks; the child copies the parent's counter as is. The error code is the same on every path: ENOMEM, "Cannot allocate memory" in the shell. Even with free memory on the machine.

Setting off the alarm

I did not try this on VPS3; lowering the ceiling touches every process. I used the linuxkit virtual machine of Docker Desktop on my Mac (kernel 6.10.14) through a privileged Python container. The VM shipped with 262,144; I set it to the kernel default with echo 65530 > /proc/sys/vm/max_map_count and restored it when the experiment ended. The script calls mmap/mprotect/munmap directly through ctypes:

kernel=6.10.14-linuxkit page=4096 max_map_count=65530 start_maps=102
[1] 1000 x mmap(4K, rw, anon)      -> maps +15
[2] single mmap of 67530 pages      -> maps=103
[2] mprotect loop: 32713 succeeded, first failure at page 65427: errno=12 (Cannot allocate memory), maps=65530
[3a] mmap(4K rw)                    -> OK (merged with neighbouring rw region)  maps=65530
[3b] mmap(4K r--, cannot merge)     -> OK  maps=65531
[3b2] mmap(4K rwx, cannot merge)    -> FAIL errno=12  maps=65531
[3c] munmap(middle page)            -> FAIL errno=12
[3d] munmap(first page of region)   -> OK
[3e] malloc(1 MiB)                  -> OK  maps=65531
[3e3] python bytearray(200 MiB)     -> MemoryError
[3f] thread start                   -> RuntimeError: can't start new thread
[3g] fork                           -> OK (child inherited the same VMA count)
Enter fullscreen mode Exit fullscreen mode

Reading the steps in order: After releasing the 1,000 regions from the first step with munmap, I opened one region of 67,530 pages with a single mmap, then made every second page PROT_NONE; each successful mprotect added two VMAs. At 103 + 2 × 32,713 = 65,529 the next mprotect failed, and even while failing it pushed the counter to 65,530: the first half of the split went through, the second hit the ceiling. At the ceiling, an rw mmap returned fine because it merged with the neighbouring rw region and the counter did not move; an r-- mmap could not merge but, thanks to the > check, settled in as VMA number 65,531; every new region after that: ENOMEM. munmap from the middle was refused, munmap from the edge went through. malloc(1 MiB) returned and maps did not change, meaning glibc served the request from the heap it already held; when I asked for 200 MiB, Python raised MemoryError. The line that gets talked about most is [3f]: I could not start a new thread. With the counter at 65,531 the stack's mmap is already refused by the > check; had we been exactly at the ceiling and had the stack merged with its neighbour, the mprotect for the guard page would still have wanted a split, and splitting is forbidden at the ceiling. Python says "can't start new thread"; in Elasticsearch logs the same condition showed up as "Native memory allocation (malloc) failed to allocate 2097152 bytes" or "You are running out of system memory on agent node when Java calls fork()" (from issue 119652 below). None of them contains the words max_map_count. That is the detector's most annoying habit: it rings in a different room.

262,144: Elasticsearch's precondition

The owner of the second number is obvious. The MaxMapCountCheck class in Elasticsearch's BootstrapChecks.java defines LIMIT = 1 << 18, that is 262,144; if /proc/sys/vm/max_map_count is below it, the node says "max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]" and exits with code 78 (ExitCodes.CONFIG). The check only runs when node.store.allow_mmap is on, and it is only enforced in "production mode": when the transport layer is bound to something other than loopback and discovery.type is not single-node. A single node on a developer laptop only gets a warning; the first real cluster gets the 78. The deb and rpm packages write vm.max_map_count=262144 through /usr/lib/sysctl.d/elasticsearch.conf; the Docker image cannot.

It cannot because vm.* is not among the namespaced sysctls. Docker's --sysctl flag only accepts keys in the IPC and network namespaces; in the documentation's words, "Docker does not support changing sysctls inside of a container that also modify the host system". Kubernetes calls the same thing a "node-level sysctl": it cannot be set through the pod's securityContext.sysctls, it is either configured in the node's operating system or written into the host by a privileged DaemonSet or init container. Hence the famous privileged init container in the ECK documentation (sysctl -w vm.max_map_count=…; today's page says 1,048,576 for 8.16 and later, 262,144 for "8.15 and earlier"); the official version of the privileged container writing sysctls into its host story I told yesterday.

The same number has a Docker Desktop story too. It is no coincidence that the VM on my Mac ships with 262,144: in October 2023, Docker Desktop 4.25 quietly lowered the VM's value to 65,530 and Elasticsearch containers started dying with 78 (docker/for-mac#7047); the 4.25.1 release note says in one sentence, "The sysctl vm.max_map_count is now set to 262144". A desktop tool picking a kernel sysctl to match the precondition of one application still strikes me as odd, but the reasoning is on the record.

1,048,576: what games asked for, what servers got

The third number did not come from servers; it came from games. Fedora 39's change proposal (IncreaseVmMaxMapCount) states its goal as "improve compatibility with Windows games through wine or steam"; DayZ, Hogwarts Legacy and the Windows build of Counter-Strike 2 were crashing at 65,530. The file went into Fedora's systemd package as 10-map-count.conf, containing two comment lines (a description and the wiki link) and vm.max_map_count=1048576. Arch put the same value into /usr/lib/sysctl.d/10-arch.conf on 7 April 2024 with the filesystem 2024.04.07-1 package. On Ubuntu the story starts with LP#2057792, opened on 13 March 2024; the reporter first asked for 2,147,483,642, the discussion settled on Fedora's value, and on 24 March Julian Andres Klode added the line "d/sysctl.d/10-map-count.conf: Set vm.max_map_count=1048576" to procps 2:4.0.4-4ubuntu2. Even the comment line in VPS3's file is a copy of Fedora's; only the link was pointed at Launchpad. On Ubuntu 26.04 the same file lives as /usr/lib/sysctl.d/55-map-count.conf (verified on VPS5: 7.0 kernel, 1,048,576). Debian trixie's procps and systemd packages carry no such file; Debian is still at 65,530.

So is the kernel. The same reporter opened 218616 on the kernel.org Bugzilla in March 2024 asking for the default to be raised; when the topic reached LKML, David Hildenbrand's answer was short: "Using a high VMA count usually implies that the application is doing something suboptimal... I don't think we should be raising the limit for everybody out there" (2 April 2024). Which leaves today's picture: the kernel at 65,530, Debian at 65,530, Fedora/Arch/Ubuntu at a million, Docker Desktop at 262,144, the Elasticsearch packages at 262,144. One sysctl with five owners and three "default" values, and only sysctl vm.max_map_count tells you which one you have.

The leak the detector really caught

I am not using the smoke-detector metaphor idly; there is an incident where the alarm genuinely rang. Elasticsearch 8.16 moved to Lucene 9.12, and Lucene 9.12 brought a new behaviour on JDK 21+: because closing a shared Arena makes the JVM deoptimize the top frames of every thread (JDK-8335480), files from the same segment started being grouped into a single shared arena. The change note describes it as "grouping files from the same segment to a single shared Arena" and leaves a knob: -Dorg.apache.lucene.store.MMapDirectory.sharedArenaMaxPermits. elastic/elasticsearch#119652, opened in January 2025, shows the consequence: in a cluster going from 8.15 to 8.17, the hot nodes restart with "heap allocation failure" within a couple of hours of each other, and the same thing repeats 12 to 24 hours later; the reporter noticed the 262,144 ceiling filling up, doubled it, watched the counter settle in the low 400 thousands, and estimated roughly a 60 percent increase in mappings. Another commenter sees the same thing across 15 clusters: an error every 30 to 60 minutes at 262,144, once a day at 1,048,576. Uwe Schindler's diagnosis (in his comment on the neighbouring issue #118623) was that the mappings of already deleted old files inside grouped arenas were not being released in time; the workaround, sharedArenaMaxPermits=1. In September 2025, #135012 made that workaround the default (its title: "Bypass MMap arena grouping"; the grouping stays in Lucene, Elasticsearch switches it off) and backported it to 9.2.0, 9.1.5, 9.0.8, 8.19.5 and 8.18.8. Issue 119652 is still open as I write this. The flag sits on the command line of the 9.4.2 container on VPS3: -Dorg.apache.lucene.store.MMapDirectory.sharedArenaMaxPermits=1.

Two lessons from that incident. First, the 262,144 detector did its job: the leak raised the alarm within a day, the mechanism was found, the workaround shipped in the product. The same leak would have shown once a day on a machine set to 1,048,576, and perhaps never on one set to 2,147,483,642; instead, kernel memory and page tables would have swelled in silence. Second, in December 2025 Elastic's documentation (docs-content#4295) moved the recommended value for "8.16+" to 1,048,576; today's page says "If the default value is lower than 1048576, configure the vm.max_map_count parameter to 1048576". But MaxMapCountCheck is still 1 << 18 and the deb package's sysctl file still says 262,144. The documentation is four times higher; the code and the package stayed put. To me that is removing the detector and building a bigger room instead; since the grouping is switched off, staying at 262,144 and watching wc -l /proc/$PID/maps is the more honest choice.

Do not raise it unmeasured; measure it once raised

Ask your own setup three questions. What is the ceiling: sysctl vm.max_map_count and grep -rs max_map_count /etc/sysctl.d /usr/lib/sysctl.d; your distribution may have decided for you. How high is the busiest process (as root; if another user's maps cannot be read, the loop silently returns empty): for p in /proc/[0-9]*; do echo "$(wc -l < $p/maps 2>/dev/null) $(cat $p/comm 2>/dev/null)"; done | sort -rn | head; on VPS3 that line answers 2,065, on VPS5 602. Is the number growing: wire the maps line count of the process you care about into a per-minute metric and alarm at half the ceiling. For a process whose mapping count stays flat, it does not matter whether the ceiling is 65,530 or a million; for a process that keeps growing, the ceiling decides how early you hear about the leak.

If you do need to raise it (Elasticsearch, OpenSearch, Steam), do it knowingly and name the file with your own prefix; 10-map-count.conf belongs to the distribution; on 24.04 it is a conffile under /etc/sysctl.d, so dpkg asks before touching your change, while on 26.04 it lives under /usr/lib/sysctl.d and a package update overwrites it silently. You cannot do it inside a container; you do it on the host. The reverse is worth knowing too: lowering the ceiling on a live machine kills no running process, it only refuses further splits and new mappings; that is exactly what I did in the lab. And wherever you see ENOMEM, run wc -l /proc/$PID/maps before free -m; "can't start new thread" or "Cannot allocate memory" sometimes means the counter is full, not the memory.

Conclusion

I left the file on VPS3 exactly as it was; it is Ubuntu's decision, not mine. But now I know what happened: Fedora at the end of 2023, Ubuntu and Arch in the spring of 2024, raised a kernel ceiling sixteenfold so that Windows games would stop crashing under Proton, and the decision arrived on servers that play no games, bundled with a package. Kernel developers turned the same request down with "then the application is doing something wrong", and a case where they were proven right happened in Elasticsearch. The number's origin is a sixteen-bit ELF field; its meaning today is a counter that tallies how many pieces a process's address space has been cut into and says "this far" at some point. When the counter speaks, it does not say "out of memory"; it says "this process is not giving its mappings back". The difference between those two sentences decides how a night goes.

Versions: VPS3 Ubuntu 24.04, kernel 6.8.0-139, procps 2:4.0.4-4ubuntu3.2, systemd 255.4-1ubuntu8.17, Elasticsearch 9.4.2 (Docker, bundled JDK 26.0.1). VPS5 Ubuntu 26.04, kernel 7.0.0-31. Lab: Docker Desktop 4.37.2, linuxkit 6.10.14 arm64, python:3.13-slim; max_map_count was restored to 262,144 at the end of the experiment, and no sysctl was changed on VPS3 or VPS5. Kernel source read on the torvalds/linux main branch (include/linux/mm.h, mm/mmap.c, mm/vma.c, mm/mremap.c); Elasticsearch source on the elastic/elasticsearch main branch. Fedora file from src.fedoraproject.org/rpms/systemd rawhide, Ubuntu changelog from changelogs.ubuntu.com, Arch file from gitlab.archlinux.org/archlinux/packaging/packages/filesystem. Sources outside the allowlist used inline in the body: the Elastic page Increase virtual memory, the Fedora change page, the Launchpad bug, the LKML archive.

Official Sources

Top comments (0)