Linear Memory: One Big Byte Array
WebAssembly's memory model centers on linear memory: a single contiguous, byte-addressable buffer represented on the host side as a WebAssembly.Memory object backed by an ArrayBuffer. Everything a Wasm module allocates — a Rust Vec, a C struct, a string — lives at some numeric offset inside this one buffer, and Wasm functions read and write it using explicit load/store instructions (i32.load, i32.store, etc.) at byte offsets, with no implicit pointer arithmetic magic beyond what the compiled language runtime provides.
Cricket analogy: It's like a single long innings scorebook where every run, wicket, and extra is recorded at a specific numbered entry — there's no separate ledger per player, just one contiguous record everyone writes into at known positions.
Pages, Growth, and the 4GB Ceiling
Wasm memory is allocated in units of pages, each exactly 64 KiB (65,536 bytes). A WebAssembly.Memory({ initial: 10 }) starts with 10 pages (640 KiB); calling memory.grow(n) appends n more pages and returns the previous page count, or -1 if growth would exceed the configured maximum or the engine's own limits. In classic 32-bit Wasm (memory64 is a separate, newer proposal), pointers are 32-bit integers, capping a single linear memory at 4 GiB — a real constraint for data-heavy applications like video encoders or large in-memory databases compiled to Wasm.
Cricket analogy: It's like a stadium that only expands in fixed 5,000-seat stand blocks — you can't add just 200 seats, you add a whole new stand, and eventually you hit the ground's total land-boundary limit no matter how many stands you bolt on.
const memory = new WebAssembly.Memory({ initial: 2, maximum: 100 }); // pages
console.log(memory.buffer.byteLength); // 131072 (2 * 65536)
const previousPages = memory.grow(3); // add 3 more pages
console.log(previousPages); // 2 (page count before growth)
console.log(memory.buffer.byteLength); // 327680 (5 * 65536)
// IMPORTANT: growing memory detaches the old ArrayBuffer reference
const view = new Uint8Array(memory.buffer); // must re-create views after grow()Who Manages Allocation? It Depends on the Source Language
WebAssembly itself has no built-in allocator, no garbage collector, and no concept of malloc/free — memory management is entirely the responsibility of whatever runtime the source language brings with it. Rust compiled to wasm32-unknown-unknown embeds its own allocator (or wee_alloc for smaller binaries) and uses ownership/borrowing at compile time with zero runtime GC overhead. Emscripten-compiled C/C++ embeds dlmalloc. Languages with GC (like a Blazor/.NET app or Go, prior to the WasmGC proposal) had to ship an entire GC runtime inside the linear memory itself, which historically bloated binary size and added pause-time overhead absent from Rust or C.
Cricket analogy: It's like the difference between a franchise that manages its own player contracts entirely in-house (Rust's compile-time ownership, no runtime overhead) versus one that outsources roster management to an external agency that periodically audits and reshuffles the squad (a GC runtime doing periodic sweeps).
The WasmGC proposal (shipped in major engines around 2023-2024) adds native garbage-collected reference types to the Wasm spec itself, letting languages like Java, Kotlin, and Dart compile to Wasm without shipping their own GC implementation inside linear memory — the host engine's GC handles it instead.
Calling memory.grow() can reallocate the underlying ArrayBuffer to a new location, which detaches any existing TypedArray views (Uint8Array, Int32Array, etc.) constructed over the old buffer. Any code holding onto a stale view after a grow() call will silently operate on a zero-length, detached buffer — always re-create views from memory.buffer after growth.
- Wasm linear memory is one contiguous, byte-addressable buffer backed by a WebAssembly.Memory / ArrayBuffer.
- Memory is allocated in 64 KiB pages; memory.grow(n) adds pages and returns the previous page count or -1 on failure.
- Classic 32-bit Wasm caps a single linear memory at 4 GiB due to 32-bit pointers.
- Wasm has no built-in allocator or GC — memory management is delegated entirely to the source language's runtime.
- Rust/C typically use manual or compile-time-checked allocators (dlmalloc, wee_alloc) with no runtime GC pause.
- GC languages historically had to bundle an entire garbage collector inside linear memory, adding size and pause overhead.
- growing memory detaches old ArrayBuffer views — TypedArray views must be re-created after every memory.grow() call.
Practice what you learned
1. What is the fixed unit size of a WebAssembly memory page?
2. Why is classic (non-memory64) WebAssembly linear memory limited to 4 GiB?
3. What does WebAssembly provide natively for memory allocation and deallocation?
4. What happens to an existing Uint8Array view over a WebAssembly.Memory buffer after memory.grow() is called?
5. What does the WasmGC proposal add to the WebAssembly specification?
Was this page helpful?
You May Also Like
The WebAssembly JavaScript API
A tour of the WebAssembly global object's core classes — Module, Instance, Memory, Table, and Global — and how JavaScript orchestrates Wasm.
Passing Data Between JS and Wasm
Practical techniques for moving numbers, strings, and structured data across the JavaScript/WebAssembly boundary via linear memory.
Wasm Tables and Functions
How WebAssembly represents functions as first-class values, why linear memory can't hold them, and how Table enables indirect calls and function pointers.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics