# MicroQuickJS: a JavaScript engine that runs in 10 kB of RAM

> Bellard's MQuickJS targets embedded systems with a stricter ES5 subset, a compacting garbage collector and a C API that never calls malloc. It is a real engine for constrained devices, not a drop-in QuickJS replacement.

**bellard/mquickjs** — Public repository of the Micro QuickJS Javascript Engine

- Repository: https://github.com/bellard/mquickjs
- Stars: 6,174 · Forks: 243
- Language: C
- License: NOASSERTION
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/bellard-mquickjs

## What MQuickJS solves, and for whom

MicroQuickJS, also written MQuickJS, is a JavaScript engine aimed at embedded systems. The README puts the numbers plainly: it compiles and runs JavaScript programs using as little as 10 kB of RAM, and the whole engine needs about 100 kB of ROM as ARM Thumb-2 code including the C library. Speed is described as comparable to QuickJS.

The audience follows from those numbers. This is for firmware developers who want scripting on a device where a full engine will not fit, and who accept that the language has to shrink to get there. MQuickJS supports only a subset of JavaScript close to ES5, and it is always in what the README calls stricter mode, where error prone or inefficient constructs are forbidden. If your scripts are written for a browser or Node.js, expect to rewrite parts of them. If you are choosing an engine for a Linux gateway with megabytes of RAM, the constraint MQuickJS is built around does not apply to you.

The project is public but carries no retrieved releases, so there is no version number to pin. The repository is not archived and the last push was on 2026-06-04.

## How the engine fits in 10 kB: tracing GC, no CPU stack, UTF-8 strings

The README states that MQuickJS shares much code with QuickJS but differs internally to consume less memory. Three choices carry most of that weight.

The first is a tracing garbage collector. QuickJS uses reference counting with a cycle collector, which spreads bookkeeping across every value. A tracing collector moves that cost into collection passes and, in MQuickJS, into a compacting collector whose objects can move whenever a JS allocation happens.

The second is that the VM does not use the CPU stack. Keeping interpreter state out of the native stack means the engine does not need to reserve stack space sized for the deepest script call chain, which matters on devices with small, fixed stacks.

The third is that strings are stored in UTF-8. That is a departure from engines that hold strings as UTF-16, and it shows through in the subset: the README notes that toLowerCase and toUpperCase handle only ASCII characters, and that regexp case folding works only with ASCII. Regexp matching is unicode in the sense that /./ matches a unicode code point rather than a UTF-16 character, as it would with the u flag elsewhere.

The memory model reaches into the C API. The engine has almost no dependency on the C library: it does not use malloc(), free() or printf(). You hand it a buffer when creating a context, and it allocates only inside that buffer. The README's example declares a uint8_t array of 8192 bytes and passes it to JS_NewContext together with &js_stdlib. JS_FreeContext is described as necessary only to run the finalizers of user objects, since the engine allocates no system memory of its own.

## Stricter mode: the language you actually get

Stricter mode is the part of MQuickJS that will decide whether your code ports. The README frames it as a subset of JavaScript, so scripts that pass here still work in ordinary engines, but the reverse is not true.

Only strict mode constructs are allowed. No with keyword, and globals must be declared with var. Arrays cannot have holes: writing past the end is a TypeError, though extending the array length by one element at the end is fine. Array literals with holes are a syntax error, so [1, , 3] will not parse. If you need a sparse structure, the README suggests a plain object, where a[0] = 1 and a[10] = 2 are both allowed. new Array(len) still works, but elements are initialized to undefined.

Direct eval is not supported; only indirect, global eval, so eval('1 + 2') is forbidden while (1, eval)('1 + 2') is accepted, and it cannot touch local variables. Value boxing is gone, so new Number(1) is not supported and, the README argues, never necessary. for in iterates only own properties, which the README recommends pairing with hasOwnProperty or replacing with for of over Object.keys(obj).

The global object exists but its use is discouraged: it cannot hold getter/setters, and properties created directly on it are not visible as global variables in the running script. C functions cannot have their own properties, though C constructors behave as expected.

There are extensions beyond ES5: for of (arrays only, no custom iterators), typed arrays, \u{hex} in string literals, the exponentiation operator, the dotall, sticky and unicode regexp flags, Math.imul, clz32, fround, trunc, log2 and log10, and String.codePointAt, replaceAll, trimStart and trimEnd. Date is nearly absent: only Date.now() is supported.

## Building mqjs and running your first script

The repository ships a Makefile with the build options at the top, all commented out except CONFIG_SMALL=y. CONFIG_SMALL selects -Os; leaving it unset gives -O2. Other switches cover CONFIG_ARM32, CONFIG_X86_32, CONFIG_WIN32, CONFIG_SOFTFLOAT, CONFIG_ASAN, CONFIG_GPROF and CONFIG_WERROR, and CONFIG_ARM32 sets CROSS_PREFIX to arm-linux-gnu-.

Build the REPL and the example program with the default target, which produces mqjs and example:

```bash
make
```

The README's own example runs a test program under a hard memory cap of 10 kB:

```bash
./mqjs --memory-limit 10k tests/mandelbrot.js
```

If the script exceeds the cap, the run fails rather than silently growing. To see where memory goes, the -d flag dumps memory usage stats.

For embedded deployment, compile the script ahead of time into bytecode that can live in ROM or on storage:

```bash
./mqjs -o mandelbrot.bin tests/mandelbrot.js
./mqjs -b mandelbrot.bin
```

The second command runs the compiled image. Note the constraint the README gives: the bytecode format depends on the endianness and word length of the CPU. On a 64 bit host you can pass -m32 to produce 32 bit bytecode for an embedded 32 bit target, and the Makefile does this automatically through MQJS_BUILD_FLAGS when CONFIG_X86_32 or CONFIG_ARM32 is set. If storage is tight, --no-column drops column numbers from debug information and keeps only line numbers.

For the C side, the README's initialization pattern is a fixed buffer plus a standard library pointer:

```c
JSContext *ctx;
uint8_t mem_buf[8192];
ctx = JS_NewContext(mem_buf, sizeof(mem_buf), &js_stdlib);
```

There is also an interactive mode, reached with -i, and -e for a single expression, both listed in the mqjs usage text.

## The C API trade-off: moving objects and no JS_FreeValue

The C API is where MQuickJS diverges most sharply from QuickJS, and the README is direct about why. Because the collector compacts, the address of objects can move each time a JS allocation is called. Two consequences follow.

First, explicitly freeing values is not necessary, so there is no JS_FreeValue(). Code that ports from QuickJS and keeps its free calls will not compile as written.

Second, the README states a general rule: avoid having variables of type JSValue in C. They may exist only as temporaries between MQuickJS API calls. Elsewhere, use a pointer to a JSValue. JS_PushGCRef() returns a pointer to a temporary opaque JSValue stored in a JSGCRef variable, and JS_PopGCRef() releases it. The opaque value is updated automatically when objects move, which is the mechanism that makes the pointer stable across collections. The README's example function signature, JSValue my_js_func(JSContext *ctx, JSValue *this_val, int argc, JSValue *argv), shows the pointer-based style, and the excerpt cuts off mid-declaration of a JSGCRef variable.

This is a genuine constraint, not a stylistic preference. Any C code that caches a JSValue across an allocation, or stores one in a struct without a GC reference, is a latent bug. The header mquickjs.h is the place to check the exact signatures, and the README recommends reading it alongside the QuickJS documentation because the APIs are similar.

## Where MQuickJS is the wrong choice

The clearest failure mode is a script that needs the language MQuickJS removed. Sparse arrays, direct eval, value boxing, Date beyond Date.now(), non-ASCII case conversion and custom iterators are all outside the subset. A codebase that depends on any of them either gets rewritten or gets a different engine.

A second boundary is tooling. The README documents a REPL, a bytecode compiler and a C API. It does not document a debugger, a profiler, or a package system. If your workflow assumes breakpoints and step execution, there is nothing here to work with. Note that the related search terms around QuickJS debugging do not map onto this project.

A third boundary is bytecode portability. The README says the format depends on endianness and word length, so a bytecode file built on one host is not a universal artifact. The -m32 flag covers the 64 bit to 32 bit case, but you still have to think about the target when you generate it.

Finally, the maintenance picture is worth stating plainly. There are no retrieved releases, so there is no changelog entry to read and no version to pin in a dependency manifest. The repository was last pushed on 2026-06-04, which is more than six months before today's date, so this is not a project to describe as actively developed. Vendoring the source, as embedded projects usually do, is the realistic approach.

## MQuickJS against QuickJS, and the licence question

The obvious alternative is QuickJS, the engine MQuickJS is derived from. The difference is not a matter of taste. QuickJS targets a much wider range of JavaScript, uses reference counting with a cycle collector rather than a tracing compacting collector, and keeps a C API where JSValue values are freed explicitly with JS_FreeValue(). Its memory floor is correspondingly higher, which is exactly why MQuickJS exists.

So the choice is not which engine is better but which constraint binds. If you have a 10 kB RAM budget, QuickJS will not fit and MQuickJS is the one built for that budget. If you have megabytes and want broad ES2020-era compatibility plus the established QuickJS API and its surrounding bindings ecosystem, MQuickJS will cost you a port and a language subset for no benefit you can spend. Related search terms point at QuickJS variants such as quickjs-ng, quickjs-emscripten and Rust bindings; those live in the larger-memory world and are not substitutes for MQuickJS on a constrained target.

On licensing: the repository's LICENSE file is the authority, and the repository metadata does not carry a standard SPDX identifier, reporting NOASSERTION instead. That means the licence terms are not machine-classified and you should read the file yourself, particularly if you plan to link the engine into firmware you distribute. Nothing here is legal advice, and the absence of an SPDX tag is not evidence of anything either way; it is simply a reason to look at the text.

## Conclusion

Adopt MicroQuickJS if you are shipping firmware with a hard RAM ceiling and your scripts fit the stricter subset, and start by building mqjs from the Makefile, running tests/mandelbrot.js under --memory-limit 10k, and compiling a bytecode image with -o for your target word length. Do not adopt it if you need full ES5 or later, a debugger, or the QuickJS C API unchanged, because the compacting collector moves JSValue addresses and removes JS_FreeValue. Before committing, verify the licence terms in LICENSE, since the repository does not offer a standard SPDX identifier, and confirm which of the subset restrictions your existing scripts violate.

## FAQ

### What is MicroQuickJS used for?

It is a JavaScript engine for embedded systems, compiling and running JavaScript in as little as 10 kB of RAM with about 100 kB of ROM for the engine as ARM Thumb-2 code including the C library. It is meant for devices where a larger engine will not fit, at the cost of supporting only a subset of JavaScript close to ES5.

### How much RAM does MicroQuickJS need?

The README gives 10 kB of RAM as the low end and about 100 kB of ROM for the engine including the C library on ARM Thumb-2. The mqjs REPL accepts a --memory-limit option, and the README's example runs a program with --memory-limit 10k.

### Can MicroQuickJS run on a 32 bit embedded target built from a 64 bit host?

Yes. The README states that the bytecode format depends on the endianness and word length of the CPU, and that on a 64 bit CPU the -m32 option generates 32 bit bytecode for an embedded 32 bit system. The Makefile sets MQJS_BUILD_FLAGS=-m32 automatically when CONFIG_X86_32 or CONFIG_ARM32 is enabled.

### Is the MicroQuickJS C API the same as the QuickJS C API?

It is very similar, but the README lists important differences caused by the compacting garbage collector. Explicitly freeing values is not necessary, so there is no JS_FreeValue(), and object addresses can move on each JS allocation, so JSValue variables should be avoided in C except as temporaries and JS_PushGCRef() and JS_PopGCRef() should be used instead.

## Sources

- [bellard/mquickjs on GitHub](https://github.com/bellard/mquickjs)
- [Issues](https://github.com/bellard/mquickjs/issues)
- [README](https://github.com/bellard/mquickjs/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bellard-mquickjs
