Using Emscripten Settings to Control Memory
This page answers one task: a C or C++ program compiled with Emscripten either runs out of memory, uses far more than it should, or starts slowly, and you need to choose its memory settings deliberately instead of accepting the defaults.
Prerequisites
- [ ] Emscripten (emsdk) 3.1.50 or newer.
- [ ] A representative workload, including the largest input you intend to support.
- [ ] A way to observe memory:
HEAP8.length, Emscripten’s--memoryprofiler, or browser memory tools.
The settings and what they control
An Emscripten program’s linear memory holds static data, the shadow stack, and the heap managed by malloc. Several link-time settings decide how that
memory is sized. INITIAL_MEMORY sets the size at startup (default 16 MiB). ALLOW_MEMORY_GROWTH decides whether the heap may grow beyond it (off by
default; with it off, running out of heap aborts). MAXIMUM_MEMORY caps growth (default 2 GiB, up to 4 GiB for 32-bit memories). MEMORY_GROWTH_GEOMETRIC_STEP
and MEMORY_GROWTH_LINEAR_STEP control how much each growth adds. STACK_SIZE sets the shadow stack (default 64 KiB). MALLOC chooses the allocator:
dlmalloc (default, faster, larger) or emmalloc (smaller, slower for heavy allocation).
The right combination depends on the workload. A fixed-size workload that always fits in 32 MiB runs fastest with a fixed memory of that size and no growth. A workload with unpredictable input sizes needs growth with a sensible maximum. Threads change the picture again, because shared memories must declare their maximum up front and growth is more expensive.
Step 1 — measure what the program actually uses
Build with growth enabled and a generous maximum, run the largest realistic workload, and record the peak:
emcc src/*.c -O2 -o build/app.js -sALLOW_MEMORY_GROWTH -sMAXIMUM_MEMORY=2gb --memoryprofiler
let peak = 0;
const sample = () => { peak = Math.max(peak, Module.HEAP8.length); };
setInterval(sample, 50);
// run the workload, then: console.log((peak / 2 ** 20).toFixed(1) + " MiB")
The --memoryprofiler overlay also shows allocation by call site and fragmentation, which helps distinguish a genuinely large working set from leaks or
waste. Measure on the largest supported input, not a typical one; the settings must accommodate the worst case or reject it cleanly.
Step 2 — choose fixed or growable memory
If the peak is modest and predictable — say under 64 MiB for every supported input — set INITIAL_MEMORY slightly above it and leave growth off:
emcc ... -sINITIAL_MEMORY=64mb # fixed: no growth checks, views never detached
Fixed memory avoids the cost of growth events and means HEAP8 and other views are never replaced, simplifying JavaScript code that holds views. Browsers
commit pages lazily, so a larger initial size costs little until it is used — but on phones the reservation itself can fail, so do not over-allocate.
If inputs vary widely, enable growth with an initial size that covers typical use and a maximum that bounds the worst case:
emcc ... -sALLOW_MEMORY_GROWTH -sINITIAL_MEMORY=32mb -sMAXIMUM_MEMORY=1gb
Step 3 — handle running out of memory
With growth off, exhausting the heap makes malloc abort the program by default (ABORTING_MALLOC is on when growth is off). With growth on, malloc
returns NULL when growth fails, if the C code checks for it. Decide which you want: for code that checks allocation results and can recover, build with
-sABORTING_MALLOC=0 so failures are reported instead of aborting. Either way, check input sizes before starting large operations, as described in
handling out-of-memory in Wasm.
Step 4 — size the stack and pick an allocator
The default 64 KiB shadow stack is small for code with large local arrays or deep recursion. Stack overflows corrupt the static data below the stack
silently unless -sSTACK_OVERFLOW_CHECK=2 is on (it is in -sASSERTIONS builds). Raise STACK_SIZE if debug builds report overflows — 1 MiB is common for
ported desktop code — and move large arrays to the heap where possible. For the allocator, keep dlmalloc for allocation-heavy programs; switch to
-sMALLOC=emmalloc to save roughly 5–10 KiB of code when allocation is light; and use -sMALLOC=mimalloc in threaded programs, where it scales better
across threads.
Step 5 — account for threads
Programs built with -pthread use a SharedArrayBuffer-backed memory that must declare its maximum when created. Growth of shared memory is supported
but has costs: every thread must refresh its views, and growth while threads run can be slow. For threaded builds, set INITIAL_MEMORY close to the
expected peak so growth is rare, and set MAXIMUM_MEMORY to a value the target devices can actually reserve — reserving 4 GiB of address space can fail
on 32-bit Android devices and some iOS versions. Threading setup is covered in
porting pthreads code with Emscripten.
Growth step tuning
When growth is enabled, Emscripten grows geometrically by default: each growth adds a fraction of the current size (MEMORY_GROWTH_GEOMETRIC_STEP, 0.2 by
default), capped by MEMORY_GROWTH_GEOMETRIC_CAP (96 MiB). Geometric growth keeps the number of growth events small for programs that grow steadily, but
overshoots: a program that needs 300 MiB may end up with 360 MiB. Linear growth (MEMORY_GROWTH_LINEAR_STEP) adds a fixed amount each time, which is more
predictable but causes more growth events. Each event replaces the JavaScript views and may copy memory in some engines, so programs that grow in many
small steps can see measurable pauses. For workloads that grow to a known size in one phase — loading a large file, building an index — the best option is
often to reserve the needed memory at the start of that phase with a single large allocation, so growth happens once.
Settings for libraries versus applications
The right settings also depend on whether the build is an application or a library embedded in someone else’s page. An application owns the page and can
choose generous memory for its own workload. A library — a codec, a parser, a document converter used by many sites — should be conservative: a modest
initial memory, growth enabled with a bounded maximum, and a way for the embedding application to pass in limits, because it shares the device with
everything else on the page. Expose those choices through the module’s factory options where possible, for example by letting callers provide their own
wasmMemory, created with the limits they want, using -sIMPORTED_MEMORY. That lets one build serve both a memory-hungry desktop tool and a lightweight
mobile page without separate compilation. Document the defaults and the peak usage for typical inputs in the library’s README, so integrators can budget.
Expected output
Measurement shows a 46 MiB peak for typical inputs and 610 MiB for the largest supported file; the build uses growth from 32 MiB with a 1 GiB maximum,
-sABORTING_MALLOC=0, a 1 MiB stack, and an input-size check that rejects files needing more than the maximum with a clear error.
Gotchas
- Leaving growth off for variable inputs. Large inputs abort. Measure and enable growth or reject inputs.
MAXIMUM_MEMORY=4gbon phones. Reserving it can fail. Choose a realistic cap.- Default 64 KiB stack for ported desktop code. Overflows corrupt data silently. Raise it and check in debug builds.
- Holding
HEAP8views across growth. They detach. Re-readModule.HEAP8after calls that allocate. - Over-allocating
INITIAL_MEMORY. Large reservations can fail on constrained devices. - Libraries claiming large memories. Embedded modules share the device. Keep defaults modest and let callers raise them.
Performance note
For an image-processing workload, a fixed 128 MiB memory ran 4% faster than the same build growing from 16 MiB, because 9 growth events and view refreshes
disappeared. Switching to emmalloc saved 8 KiB of code but slowed an allocation-heavy benchmark by 22%.
Frequently Asked Questions
Does a large INITIAL_MEMORY slow startup?
Slightly — the engine reserves and zero-maps the memory — but pages are committed lazily, so the cost is small on desktop.
Can JavaScript grow the memory for the program?
It can call wasmMemory.grow, but Emscripten’s allocator manages growth itself; prefer letting malloc grow it.
What about Memory64?
-sMEMORY64 lifts the 4 GiB limit at some performance and compatibility cost; see the Memory64 guide.
How do I see memory usage in production?
Sample HEAP8.length and report peaks, as in the memory monitoring guide.
Are these settings different for Node? The same settings apply; Node usually tolerates larger memories than phones.
Can the embedding page choose the memory limits?
Build with -sIMPORTED_MEMORY and let callers pass a WebAssembly.Memory with their own initial and maximum sizes.
Should I check these settings into the build scripts? Yes — keep them in the CMake or Makefile link flags with a comment recording the measurement that justified each value.
Related
- Sizing initial and maximum memory — the concepts behind the settings.
- Debugging Emscripten builds with assertions — stack overflow checks.
- Profiling memory in Emscripten with the memory profiler — measuring the peak.
- Why memory.grow invalidates pointers — views after growth.
← Back to C/C++ to Wasm with Emscripten