Using Multiple Memories in One Module
This page answers one question: a WebAssembly module can now have more than one linear memory — what does that look like in the binary and in WAT, what is it good for, and can you use it from today’s toolchains?
Prerequisites
- [ ] WABT 1.0.33+ (or
wasm-tools) to assemble the examples with multi-memory enabled. - [ ] A recent Chrome, Firefox or Node; check support with a feature test before relying on it in production.
- [ ] Familiarity with linear memory and memory instructions, as in reading and writing memory in WAT.
One memory was a simplification
The original WebAssembly allowed at most one memory per module, and every load, store and memory instruction implicitly used memory 0. That kept the instruction encoding small and the first engines simple. It also forced every kind of data — the program’s own heap, buffers shared with the host, scratch space for a plugin, data imported from another module — into one address space.
The multi-memory proposal lifts the limit: a module may declare or import several memories, and every memory instruction carries a memory
index. Each memory is a separate, independently sized and growable address space. Instructions that touch two memories, like
memory.copy, name both. Nothing about a single-memory module changes; memory 0 remains the default when no index is written.
The interesting consequence is isolation within one module. Data in one memory cannot be reached through a pointer into another — an offset is only meaningful relative to its memory — so a bug that overruns a buffer in one memory cannot corrupt the other.
Step 1 — declare and address multiple memories in WAT
(module
(import "host" "io" (memory $io 1)) ;; memory 0 is the first declared — here, the import
(memory $heap 4) ;; memory 1
(memory $scratch 1 16) ;; memory 2, max 16 pages
;; copy len bytes of input from the I/O buffer into scratch
(func (export "stage_input") (param $len i32)
(memory.copy $scratch $io (i32.const 0) (i32.const 0) (local.get $len)))
;; read a byte from scratch, write a result into the heap
(func (export "first_byte_to_heap")
(i32.store8 $heap (i32.const 0)
(i32.load8_u $scratch (i32.const 0)))))
wat2wasm --enable-multi-memory multi.wat -o multi.wasm
wasm-objdump -x -j Memory multi.wasm
Memory indices follow declaration order, imports first. Each load and store names its memory; memory.copy names destination then
source. Using names ($heap, $scratch) rather than raw indices keeps the WAT readable and survives reordering the declarations.
Step 2 — provide and inspect the memories from JavaScript
const io = new WebAssembly.Memory({ initial: 1 });
const { instance } = await WebAssembly.instantiateStreaming(fetch("multi.wasm"), { host: { io } });
new Uint8Array(io.buffer).set(new TextEncoder().encode("hello"));
instance.exports.stage_input(5);
instance.exports.first_byte_to_heap();
Exported memories appear under their export names, so a module that wants the host to read its heap exports it explicitly. JavaScript then
sees each memory as an ordinary WebAssembly.Memory with its own buffer, and views into one never alias another.
Step 3 — check engine support before shipping
Multi-memory is standardised and shipping in current engines, but older browsers and some embedders still reject it. Detect it with a tiny module that declares two memories:
const MULTI_MEMORY_PROBE = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // magic, version
0x05, 0x05, 0x02, 0x00, 0x01, 0x00, 0x01, // memory section: 2 memories, min 1 page each
]);
const hasMultiMemory = WebAssembly.validate(MULTI_MEMORY_PROBE);
Ship a single-memory build as the fallback where it is false, following the approach in detecting proposal support at runtime.
Step 4 — understand the toolchain gap
The binary format and engines support multi-memory; source languages mostly do not yet. C and Rust have one address space per program,
and their pointers carry no memory index, so compilers have no natural way to express “this pointer points into memory 2”. The practical
routes today are hand-written WAT for small, specialised modules; tools that merge modules, such as Binaryen’s wasm-merge, which can keep
each input module’s memory separate in the merged result; and the component model, whose implementation strategies can use multiple
memories to keep components isolated.
For application developers, that makes multi-memory mostly an infrastructure feature for now — something runtimes, linkers and component tooling use on your behalf — rather than something you write directly in Rust or C.
Step 5 — use it for isolation where you control the code
Where you do write the module by hand or generate it, two patterns are worth knowing. Host I/O separation: give the host a dedicated memory for input and output buffers, so JavaScript never needs a view into the module’s private heap, and growth of the heap never invalidates the host’s views. Untrusted parsing: parse untrusted input in a scratch memory with a small maximum, so a parser bug can at worst corrupt scratch data and hit the scratch limit, never reach the program’s state. Both reduce the blast radius of bugs in the same way separate processes would, without the cost of separate instances.
Comparing with separate instances
Before multi-memory, the way to get separate address spaces was separate module instances, each with its own memory, communicating through JavaScript or through imports and exports. That still works and remains the more portable option. Multi-memory is cheaper: one instance, direct instructions to copy between memories instead of host round trips, and a single compiled module. The trade-off is maturity and expressiveness — instances can be written in any language today, while multi-memory needs hand-written or tool-generated code. For a plugin host or a parser sandbox, separate instances are still the practical default, as in restricting what a module can import.
What to expect next
Multi-memory is one of the proposals whose value grows as tools catch up. Linkers that keep libraries in separate memories, sanitizers that place shadow memory in its own address space, and component tooling that isolates components inside one core module are all natural users, and several are already experimenting with it. For most application developers the practical advice is to know the feature exists, to recognise multiple memories in a disassembly, and to prefer tools that use it on your behalf over hand-written WAT — while keeping a single-memory fallback for engines that do not yet support it.
Expected output
Memory[3]:
- memory[0] pages: initial=1 <- host.io
- memory[1] pages: initial=4
- memory[2] pages: initial=1 max=16
After the JavaScript calls above, byte 0 of the heap holds 104 ("h"), copied through scratch, and the I/O buffer is untouched.
Gotchas
multiple memories not enabledfrom the assembler. Pass the feature flag towat2wasmor use a recentwasm-tools.- Index confusion. Imports come first in the index space. Use names in WAT to avoid off-by-one memory indices.
- Glue code assumes
exports.memory. Toolchain glue often expects a single exported memory namedmemory. Export the heap under that name or adapt the glue. - Debuggers show only one memory. Some tools still assume memory 0. Inspect other memories through their exports from the console.
- Expecting source-language support. Rust and C cannot target a second memory from ordinary code yet; plan for hand-written or tool-generated modules.
Performance note
Loads and stores to memory 0 compile exactly as before. Accesses to other memories may cost an extra register to hold that memory’s base
address; in a microbenchmark copying between two memories with memory.copy, throughput matched a same-memory copy, and a byte-wise loop
across memories was about 4% slower than within one memory. Isolation is close to free.
Frequently Asked Questions
Can memories have different index types? With memory64, a module can mix 32-bit and 64-bit memories, each addressed with its own index type; see Memory64 and large heaps.
Do multiple memories increase the module’s memory use? Each memory reserves its own virtual address range and commits its own pages, so three small memories use slightly more than one memory of the combined size. The overhead is modest and mostly virtual.
Can one memory be shared and another not?
Yes — each memory has its own shared flag. A threaded module could share one memory between workers and keep another per-thread.
Does this replace the component model’s isolation? No; it is one mechanism component implementations can use. The component model adds typed interfaces and copying rules on top.
Is multi-memory useful for garbage-collected languages? GC languages now use the GC proposal’s managed heap rather than linear memory for their objects, which is a different mechanism; see using Wasm GC for managed languages.
Related
- Post-MVP Wasm proposals in practice — the proposal family.
- Using bulk memory operations —
memory.copyand friends, now with indices. - Composing two Wasm components — isolation at the component level.
- Understanding Wasm linear memory limits — limits that apply per memory.
← Back to Post-MVP Wasm Proposals in Practice